#!/usr/bin/env python3 """Measure and safely enforce Blacksite's bounded Cargo build storage policy.""" from __future__ import annotations import argparse import contextlib import hashlib import io import json import os import re import shutil import subprocess import sys import tempfile from collections import defaultdict from dataclasses import asdict, dataclass from datetime import datetime, timezone from pathlib import Path from typing import Any, Iterable, Mapping, Sequence try: import tomllib except ModuleNotFoundError as error: # pragma: no cover - Python < 3.11 raise SystemExit("build_storage.py requires Python 3.11 or newer") from error from cargo_lane import ( SENTINEL_NAME, LaneError, cache_root, canonical_path, cargo_home, load_workflow, read_sentinel, sentinel_path, toolchain_info, utc_now, validate_lane_name, validate_sentinel, workspace_hash, workspace_root, ) GIB = 1024**3 LEDGER_SCHEMA_VERSION = 1 CANDIDATE_PRESERVATION_SCHEMA_VERSION = 1 CANDIDATE_PRESERVATION_PATH = Path(".codex/session/candidate-prune-safe.json") DEFAULT_POLICY: dict[str, Any] = { "enabled": True, "soft_total_gib": 55, "hard_total_gib": 80, "min_free_gib": 40, "min_free_percent": 15, "persistent_lane_max_gib": 40, "disposable_lane_max_gib": 35, "candidate_retention_hours": 24, "hot_reload_retention_hours": 24, "full_debug_retention_hours": 12, "failed_lane_retention_hours": 24, "auto_prune_disposable": True, "delete_only_marked_lanes": True, } class StorageError(RuntimeError): """Storage state cannot be measured or changed safely.""" def _policy(workflow: Mapping[str, Any]) -> dict[str, Any]: result = dict(DEFAULT_POLICY) configured = workflow.get("build_storage", {}) if isinstance(configured, Mapping): result.update(configured) return result def human_bytes(value: int) -> str: amount = float(max(0, value)) for suffix in ("B", "KiB", "MiB", "GiB", "TiB"): if amount < 1024 or suffix == "TiB": return f"{amount:.1f} {suffix}" amount /= 1024 return f"{amount:.1f} TiB" def _allocated_bytes(stat_result: os.stat_result) -> int: blocks = getattr(stat_result, "st_blocks", 0) return blocks * 512 if blocks else stat_result.st_size @dataclass class PathUsage: path: str allocated_bytes: int = 0 apparent_bytes: int = 0 files: int = 0 directories: int = 0 symlinks: int = 0 top_level: dict[str, int] | None = None error: str | None = None def measure_tree(path: Path) -> PathUsage: path = canonical_path(path) usage = PathUsage(path=str(path), top_level={}) if not path.exists(): return usage if path.is_symlink(): usage.symlinks = 1 usage.error = "root is a symlink" return usage seen: set[tuple[int, int]] = set() try: root_stat = path.lstat() root_device = root_stat.st_dev usage.allocated_bytes += _allocated_bytes(root_stat) usage.apparent_bytes += root_stat.st_size usage.directories += 1 seen.add((root_stat.st_dev, root_stat.st_ino)) except OSError as error: usage.error = str(error) return usage for directory, names, filenames in os.walk(path, topdown=True, followlinks=False): current = Path(directory) relative = current.relative_to(path) current_bucket = relative.parts[0] if relative.parts else None kept_names: list[str] = [] for name in names: child = current / name try: stat_result = child.lstat() except OSError: continue if child.is_symlink(): usage.symlinks += 1 usage.allocated_bytes += _allocated_bytes(stat_result) usage.apparent_bytes += stat_result.st_size continue if stat_result.st_dev != root_device: continue kept_names.append(name) inode = (stat_result.st_dev, stat_result.st_ino) if inode in seen: continue seen.add(inode) allocated = _allocated_bytes(stat_result) usage.allocated_bytes += allocated usage.apparent_bytes += stat_result.st_size usage.directories += 1 bucket = current_bucket or name usage.top_level[bucket] = usage.top_level.get(bucket, 0) + allocated names[:] = kept_names for name in filenames: child = current / name try: stat_result = child.lstat() except OSError: continue if child.is_symlink(): usage.symlinks += 1 inode = (stat_result.st_dev, stat_result.st_ino) if inode in seen: continue seen.add(inode) allocated = _allocated_bytes(stat_result) usage.allocated_bytes += allocated usage.apparent_bytes += stat_result.st_size usage.files += 1 bucket = current_bucket or name usage.top_level[bucket] = usage.top_level.get(bucket, 0) + allocated usage.top_level = dict( sorted(usage.top_level.items(), key=lambda item: item[1], reverse=True) ) return usage def _nearest_existing(path: Path) -> Path: current = canonical_path(path) while not current.exists() and current != current.parent: current = current.parent return current def _mount_point(path: Path) -> Path: current = _nearest_existing(path) while current != current.parent and not os.path.ismount(current): current = current.parent return current def _filesystem_status(paths: Iterable[Path], policy: Mapping[str, Any]) -> list[dict[str, Any]]: by_device: dict[int, dict[str, Any]] = {} for requested in paths: probe = _nearest_existing(requested) try: device = probe.stat().st_dev usage = shutil.disk_usage(probe) except OSError: continue if device in by_device: by_device[device]["paths"].append(str(canonical_path(requested))) continue percentage_floor = int(usage.total * float(policy["min_free_percent"]) / 100) fixed_floor = int(float(policy["min_free_gib"]) * GIB) floor = max(fixed_floor, percentage_floor) by_device[device] = { "device": device, "mount": str(_mount_point(probe)), "paths": [str(canonical_path(requested))], "total_bytes": usage.total, "used_bytes": usage.used, "free_bytes": usage.free, "minimum_free_bytes": floor, } return list(by_device.values()) def _path_within(path: Path, parent: Path) -> bool: try: canonical_path(path).relative_to(canonical_path(parent)) return True except ValueError: return False def _read_proc_value(path: Path, *, binary: bool = False) -> str: try: if binary: return path.read_bytes().replace(b"\0", b" ").decode(errors="replace").strip() return path.read_text(encoding="utf-8", errors="replace").strip() except (OSError, PermissionError): return "" BUILD_TOOL_NAMES = { "cargo", "rustc", "rustdoc", "clang", "clang++", "cc", "c++", "gcc", "g++", "ld", "ld.lld", "lld", "mold", } def active_processes(root: Path, watched_paths: Sequence[Path]) -> list[dict[str, Any]]: proc = Path("/proc") if not proc.is_dir(): return [] root = canonical_path(root) watched = [canonical_path(path) for path in watched_paths] results: list[dict[str, Any]] = [] own_pid = os.getpid() for entry in proc.iterdir(): if not entry.name.isdigit() or int(entry.name) == own_pid: continue pid = int(entry.name) comm = _read_proc_value(entry / "comm") command = _read_proc_value(entry / "cmdline", binary=True) executable = "" cwd = "" try: executable = os.readlink(entry / "exe") except OSError: pass try: cwd = os.readlink(entry / "cwd") except OSError: pass command_name = Path(command.split(" ", 1)[0]).name if command else comm is_build_tool = comm in BUILD_TOOL_NAMES or command_name in BUILD_TOOL_NAMES is_packager = any( token in command for token in ("package-project", "process-assets", "cargo package") ) is_editor = comm in {"editor", "project_launcher", "game"} or command_name in { "editor", "project_launcher", "game", } environment = _read_proc_value(entry / "environ", binary=True) references = " ".join((command, executable, cwd, environment)) used = [str(path) for path in watched if str(path) in references] workspace_related = bool(cwd and _path_within(Path(cwd), root)) if is_editor and not used: maps = _read_proc_value(entry / "maps") used = [str(path) for path in watched if str(path) in maps] if not used and not ((is_build_tool or is_packager) and workspace_related): continue results.append( { "pid": pid, "comm": comm, "command": command, "cwd": cwd, "exe": executable, "build_tool": is_build_tool, "packager": is_packager, "editor": is_editor, "workspace_related": workspace_related, "uses": used, } ) return sorted(results, key=lambda item: item["pid"]) def _sccache_status(root: Path) -> dict[str, Any]: wrapper = os.environ.get("RUSTC_WRAPPER", "") config_path = root / ".cargo" / "config.toml" if not wrapper and config_path.is_file(): try: with config_path.open("rb") as handle: config = tomllib.load(handle) build = config.get("build", {}) if isinstance(build, Mapping): wrapper = str(build.get("rustc-wrapper", "")) except (OSError, tomllib.TOMLDecodeError): pass enabled = Path(wrapper).name == "sccache" if wrapper else False result: dict[str, Any] = { "enabled": enabled, "wrapper": wrapper or None, "executable": shutil.which("sccache"), "allocated_bytes": 0, } if not enabled: return result configured_dir = os.environ.get("SCCACHE_DIR") if configured_dir: usage = measure_tree(Path(configured_dir)) result["cache_dir"] = usage.path result["allocated_bytes"] = usage.allocated_bytes executable = result["executable"] if executable: completed = subprocess.run( [executable, "--show-stats", "--stats-format=json"], check=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, ) if completed.returncode == 0: try: result["stats"] = json.loads(completed.stdout) except json.JSONDecodeError: result["stats_text"] = completed.stdout.strip() return result def _marker_record(path: Path, root: Path, role: str) -> dict[str, Any]: document = read_sentinel(path) lane = path.name errors: list[str] = [] if document is None: errors.append(f"missing or malformed {SENTINEL_NAME}") else: lane = str(document.get("lane", lane)) errors.extend( validate_sentinel(path, document, root=root, lane=lane, role=role) ) usage = measure_tree(path) if usage.error: errors.append(usage.error) return { "path": str(canonical_path(path)), "role": role, "lane": lane, "valid_sentinel": not errors, "sentinel_errors": errors, "sentinel": document, "usage": asdict(usage), } def _discover_lanes(root: Path, workflow: Mapping[str, Any]) -> tuple[list[dict[str, Any]], Path, Path]: storage = workflow["build_storage"] external_partition = cache_root(root, workflow) / workspace_hash(root) exceptional_root = canonical_path( root / str(storage.get("exceptional_target_root", "target/lanes")) ) records: list[dict[str, Any]] = [] if external_partition.is_dir() and not external_partition.is_symlink(): for child in sorted(external_partition.iterdir()): if child.is_dir() and not child.is_symlink(): records.append(_marker_record(child, root, "build")) if exceptional_root.is_dir() and not exceptional_root.is_symlink(): for child in sorted(exceptional_root.iterdir()): if child.is_dir() and not child.is_symlink(): records.append(_marker_record(child, root, "target")) return records, external_partition, exceptional_root def _group_lanes(records: Sequence[dict[str, Any]]) -> list[dict[str, Any]]: grouped: dict[str, list[dict[str, Any]]] = defaultdict(list) for record in records: grouped[str(record["lane"])].append(record) result: list[dict[str, Any]] = [] for lane, artifacts in sorted(grouped.items()): total = sum(item["usage"]["allocated_bytes"] for item in artifacts) last_use_values = [ item["sentinel"].get("last_use") for item in artifacts if isinstance(item.get("sentinel"), Mapping) and item["sentinel"].get("last_use") ] result.append( { "lane": lane, "allocated_bytes": total, "last_use": max(last_use_values) if last_use_values else None, "valid": all(item["valid_sentinel"] for item in artifacts), "artifacts": artifacts, } ) return result def storage_snapshot(root: Path, workflow: Mapping[str, Any]) -> dict[str, Any]: root = canonical_path(root) policy = _policy(workflow) repository_target = canonical_path( root / str(policy.get("dev_target_dir", "target")) ) target_usage = measure_tree(repository_target) records, external_partition, exceptional_root = _discover_lanes(root, workflow) external_usage = measure_tree(external_partition) lanes = _group_lanes(records) sccache = _sccache_status(root) total = ( target_usage.allocated_bytes + external_usage.allocated_bytes + int(sccache.get("allocated_bytes", 0)) ) watched = [repository_target, external_partition, exceptional_root] filesystems = _filesystem_status( [repository_target, cache_root(root, workflow)], policy ) debug = repository_target / "debug" legacy_paths = { "deps": debug / "deps", "incremental": debug / "incremental", "build": debug / "build", "fingerprint": debug / ".fingerprint", } legacy_intermediates = { name: asdict(measure_tree(path)) for name, path in legacy_paths.items() if path.exists() } return { "schema_version": 1, "timestamp": utc_now(), "workspace": str(root), "workspace_hash": workspace_hash(root), "toolchain": toolchain_info(), "policy": policy, "repository_target": asdict(target_usage), "external_partition": asdict(external_usage), "exceptional_target_root": str(exceptional_root), "lanes": lanes, "legacy_intermediates": legacy_intermediates, "sccache": sccache, "filesystems": filesystems, "active_processes": active_processes(root, watched), "total_allocated_bytes": total, } def _parse_simulated_lanes(values: Sequence[str]) -> dict[str, int]: result: dict[str, int] = {} for value in values: lane, separator, amount = value.partition("=") if not separator: raise StorageError("--simulate-lane-gib expects LANE=GIB") validate_lane_name(lane) result[lane] = int(float(amount) * GIB) return result def apply_simulation(snapshot: dict[str, Any], arguments: argparse.Namespace) -> bool: simulated = False if arguments.simulate_total_gib is not None: snapshot["total_allocated_bytes"] = int(arguments.simulate_total_gib * GIB) simulated = True if arguments.simulate_free_gib is not None: for filesystem in snapshot["filesystems"]: filesystem["free_bytes"] = int(arguments.simulate_free_gib * GIB) simulated = True lane_sizes = _parse_simulated_lanes(arguments.simulate_lane_gib or []) if lane_sizes: simulated = True existing = {lane["lane"]: lane for lane in snapshot["lanes"]} for lane, size in lane_sizes.items(): if lane in existing: existing[lane]["allocated_bytes"] = size else: snapshot["lanes"].append( { "lane": lane, "allocated_bytes": size, "last_use": None, "valid": True, "artifacts": [], "simulated": True, } ) snapshot["simulated"] = simulated return simulated def evaluate_budget(snapshot: Mapping[str, Any]) -> dict[str, list[str]]: policy = snapshot["policy"] total = int(snapshot["total_allocated_bytes"]) warnings: list[str] = [] violations: list[str] = [] soft = int(float(policy["soft_total_gib"]) * GIB) hard = int(float(policy["hard_total_gib"]) * GIB) if total > hard: violations.append( f"total build artifacts {human_bytes(total)} exceed hard budget {human_bytes(hard)}" ) elif total > soft: warnings.append( f"total build artifacts {human_bytes(total)} exceed soft budget {human_bytes(soft)}" ) persistent = str(policy.get("persistent_lane", "dev")) for lane in snapshot["lanes"]: maximum_gib = ( policy["persistent_lane_max_gib"] if lane["lane"] == persistent else policy["disposable_lane_max_gib"] ) maximum = int(float(maximum_gib) * GIB) if int(lane["allocated_bytes"]) > maximum: warnings.append( f"lane {lane['lane']} uses {human_bytes(lane['allocated_bytes'])}, " f"above {human_bytes(maximum)}" ) for filesystem in snapshot["filesystems"]: free = int(filesystem["free_bytes"]) floor = int(filesystem["minimum_free_bytes"]) if free < floor: violations.append( f"filesystem {filesystem['mount']} has {human_bytes(free)} free; " f"minimum is {human_bytes(floor)}" ) return {"warnings": warnings, "violations": violations} def _parse_timestamp(value: str | None) -> datetime | None: if not value: return None try: parsed = datetime.fromisoformat(value) except ValueError: return None if parsed.tzinfo is None: parsed = parsed.replace(tzinfo=timezone.utc) return parsed def _retention_hours(lane: str, policy: Mapping[str, Any]) -> float: if lane == str(policy.get("candidate_lane", "candidate")): return float(policy["candidate_retention_hours"]) if lane == str(policy.get("hot_reload_lane", "hot-reload")): return float(policy["hot_reload_retention_hours"]) if lane == str(policy.get("full_debug_lane", "full-debug")): return float(policy["full_debug_retention_hours"]) return float(policy["failed_lane_retention_hours"]) def prune_plan(snapshot: Mapping[str, Any]) -> list[dict[str, Any]]: policy = snapshot["policy"] persistent = str(policy.get("persistent_lane", "dev")) now = datetime.now(timezone.utc) plans: list[dict[str, Any]] = [] for lane in snapshot["lanes"]: if lane["lane"] == persistent: continue last_use = _parse_timestamp(lane.get("last_use")) age_hours = ( (now - last_use).total_seconds() / 3600 if last_use is not None else None ) retention = _retention_hours(lane["lane"], policy) eligible = bool(lane["valid"] and age_hours is not None and age_hours >= retention) plans.append( { "lane": lane["lane"], "allocated_bytes": lane["allocated_bytes"], "age_hours": age_hours, "retention_hours": retention, "eligible": eligible, "reason": ( "expired disposable lane" if eligible else "invalid sentinel" if not lane["valid"] else "missing last-use timestamp" if age_hours is None else "retention window active" ), "artifacts": [artifact["path"] for artifact in lane["artifacts"]], } ) return plans def _contains_symlink(path: Path) -> bool: if path.is_symlink(): return True for directory, names, filenames in os.walk(path, topdown=True, followlinks=False): current = Path(directory) for name in [*names, *filenames]: try: if (current / name).is_symlink(): return True except OSError: return True return False def _tracked_files(root: Path, path: Path) -> list[str]: if not _path_within(path, root): return [] relative = canonical_path(path).relative_to(root) completed = subprocess.run( ["git", "ls-files", "--", str(relative)], cwd=root, check=False, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, ) return [line for line in completed.stdout.splitlines() if line] def _candidate_lane_name(workflow: Mapping[str, Any]) -> str: verification = workflow.get("verification", {}) if isinstance(verification, Mapping): value = verification.get("candidate_lane", "candidate") if isinstance(value, str) and value: return value return "candidate" def _candidate_preservation_path(root: Path) -> Path: return canonical_path(root) / CANDIDATE_PRESERVATION_PATH def _lane_generation(lane: Mapping[str, Any]) -> str | None: """Bind preservation evidence to the exact last-used lane generation.""" sentinels: list[dict[str, Any]] = [] artifacts = sorted( lane.get("artifacts", []), key=lambda item: (str(item.get("role", "")), str(item.get("path", ""))), ) for artifact in artifacts: marker = read_sentinel(Path(str(artifact["path"]))) if marker is None: return None sentinels.append(marker) if not sentinels: return None encoded = json.dumps( sentinels, sort_keys=True, separators=(",", ":"), ensure_ascii=True ).encode("utf-8") return hashlib.sha256(encoded).hexdigest() def _sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: while chunk := handle.read(1024 * 1024): digest.update(chunk) return digest.hexdigest() def _preserved_path(root: Path, raw: object) -> Path | None: if not isinstance(raw, str) or not raw: return None path = Path(os.path.expandvars(os.path.expanduser(raw))) if not path.is_absolute(): path = root / path return path def _preserved_record_errors( record: object, *, label: str, root: Path, lane_paths: Sequence[Path], ) -> list[str]: if not isinstance(record, Mapping): return [f"{label} must be an object with path, bytes, and sha256"] raw_path = _preserved_path(root, record.get("path")) if raw_path is None: return [f"{label} has no valid path"] if raw_path.is_symlink(): return [f"{label} is a symlink: {raw_path}"] path = canonical_path(raw_path) errors: list[str] = [] if any(_path_within(path, lane_path) for lane_path in lane_paths): errors.append(f"{label} remains inside the disposable candidate lane: {path}") if not path.is_file(): errors.append(f"{label} is not a preserved file: {path}") return errors expected_bytes = record.get("bytes") if not isinstance(expected_bytes, int) or expected_bytes < 0: errors.append(f"{label} has an invalid byte count") else: actual_bytes = path.stat().st_size if actual_bytes != expected_bytes: errors.append( f"{label} byte count changed: expected {expected_bytes}, found {actual_bytes}" ) expected_digest = record.get("sha256") if not isinstance(expected_digest, str) or not re.fullmatch( r"[0-9a-f]{64}", expected_digest ): errors.append(f"{label} has an invalid sha256") else: try: actual_digest = _sha256_file(path) except OSError as error: errors.append(f"{label} cannot be hashed: {error}") else: if actual_digest != expected_digest: errors.append(f"{label} sha256 no longer matches: {path}") return errors def _candidate_preservation_errors( lane: Mapping[str, Any], *, root: Path, workflow: Mapping[str, Any] ) -> list[str]: if str(lane.get("lane")) != _candidate_lane_name(workflow): return [] marker_path = _candidate_preservation_path(root) if marker_path.is_symlink(): return [f"candidate preservation marker is a symlink: {marker_path}"] if not marker_path.is_file(): return [ "candidate preservation marker is missing; preserve the candidate evidence " "and artifacts, then run mark-candidate-preserved" ] try: marker = json.loads(marker_path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as error: return [f"candidate preservation marker is unreadable: {error}"] if not isinstance(marker, Mapping): return ["candidate preservation marker must be a JSON object"] root = canonical_path(root) generation = _lane_generation(lane) expected = { "schema_version": CANDIDATE_PRESERVATION_SCHEMA_VERSION, "workspace": str(root), "workspace_hash": workspace_hash(root), "lane": _candidate_lane_name(workflow), "lane_generation": generation, "prune_safe": True, } errors: list[str] = [] for key, value in expected.items(): if marker.get(key) != value: errors.append( f"candidate preservation {key}: expected {value!r}, found {marker.get(key)!r}" ) lane_paths = [canonical_path(Path(str(item["path"]))) for item in lane["artifacts"]] evidence = marker.get("evidence") errors.extend( _preserved_record_errors( evidence, label="candidate evidence", root=root, lane_paths=lane_paths ) ) artifacts = marker.get("preserved_artifacts") if not isinstance(artifacts, list) or not artifacts: errors.append("candidate preservation must name at least one preserved artifact") else: for index, record in enumerate(artifacts): errors.extend( _preserved_record_errors( record, label=f"preserved candidate artifact {index}", root=root, lane_paths=lane_paths, ) ) evidence_path = ( canonical_path(_preserved_path(root, evidence.get("path"))) if isinstance(evidence, Mapping) and _preserved_path(root, evidence.get("path")) is not None else None ) artifact_paths = { canonical_path(path) for record in artifacts if isinstance(record, Mapping) and (path := _preserved_path(root, record.get("path"))) is not None } if evidence_path is not None and artifact_paths == {evidence_path}: errors.append("candidate evidence alone is not a preserved candidate artifact") return errors def deletion_safety( artifact: Mapping[str, Any], *, root: Path, workflow: Mapping[str, Any], processes: Sequence[Mapping[str, Any]], ) -> list[str]: path = Path(str(artifact["path"])) lane = str(artifact["lane"]) role = str(artifact["role"]) errors: list[str] = [] if not path.exists(): return ["path no longer exists"] if path.is_symlink(): errors.append("lane root is a symlink") path = canonical_path(path) root = canonical_path(root) policy = _policy(workflow) external_parent = canonical_path(cache_root(root, workflow) / workspace_hash(root)) exceptional_parent = canonical_path( root / str(policy.get("exceptional_target_root", "target/lanes")) ) expected_parent = external_parent if role == "build" else exceptional_parent if path.parent != expected_parent: errors.append(f"path is not a direct child of managed {role} root") protected = [Path("/"), Path.home(), cargo_home(), root, root / ".git"] for protected_path in protected: protected_path = canonical_path(protected_path) if path == protected_path or _path_within(protected_path, path): errors.append(f"path is or contains protected path {protected_path}") # Never recursively inspect a path that already failed containment or # protected-root checks. This keeps even a malicious dry run bounded. if errors: return errors marker = read_sentinel(path) if marker is None: errors.append(f"missing or malformed {SENTINEL_NAME}") else: errors.extend( validate_sentinel(path, marker, root=root, lane=lane, role=role) ) if _contains_symlink(path): errors.append("lane contains a symlink") tracked = _tracked_files(root, path) if tracked: errors.append(f"lane contains tracked files: {', '.join(tracked[:5])}") for process in processes: build_activity = bool( process.get("build_tool") or process.get("packager") or process.get("workspace_related") ) direct_use = any( _path_within(Path(used), path) or _path_within(path, Path(used)) for used in process.get("uses", []) ) if direct_use or (build_activity and process.get("workspace_related")): errors.append( f"active process {process.get('pid')} ({process.get('comm')}) may use the lane" ) return errors def _lane_by_name(snapshot: Mapping[str, Any], lane: str) -> dict[str, Any] | None: return next((item for item in snapshot["lanes"] if item["lane"] == lane), None) def _delete_lane( lane: Mapping[str, Any], *, root: Path, workflow: Mapping[str, Any], apply: bool, ) -> dict[str, Any]: paths = [Path(item["path"]) for item in lane["artifacts"]] processes = active_processes(root, paths) checks: list[dict[str, Any]] = [] for artifact in lane["artifacts"]: errors = deletion_safety( artifact, root=root, workflow=workflow, processes=processes ) checks.append({"path": artifact["path"], "errors": errors}) if str(lane.get("lane")) == _candidate_lane_name(workflow): checks.append( { "path": str(_candidate_preservation_path(root)), "kind": "candidate-preservation", "errors": _candidate_preservation_errors( lane, root=root, workflow=workflow ), } ) blockers = [error for check in checks for error in check["errors"]] result = { "lane": lane["lane"], "apply": apply, "allocated_bytes": lane["allocated_bytes"], "checks": checks, "deleted": [], "blocked": blockers, } if blockers or not apply: return result # Delete only complete, independently marked lane roots. rmtree does not # follow directory symlinks, and the preflight above rejects every symlink. for artifact in sorted(lane["artifacts"], key=lambda item: item["role"] == "build"): path = Path(artifact["path"]) shutil.rmtree(path) result["deleted"].append(str(path)) return result def _execute_checked_deletions( lanes: Sequence[Mapping[str, Any]], *, root: Path, workflow: Mapping[str, Any], action: str, apply: bool, output: Any | None = None, ) -> tuple[dict[str, Any], list[dict[str, Any]]]: """Print the complete checked plan before any requested deletion occurs.""" checked = [ _delete_lane(lane, root=root, workflow=workflow, apply=False) for lane in lanes ] plan = { "timestamp": utc_now(), "action": f"{action}-checked-plan", "apply_requested": apply, "total_allocated_bytes": sum( int(result["allocated_bytes"]) for result in checked ), "total": human_bytes( sum(int(result["allocated_bytes"]) for result in checked) ), "lanes": checked, } if not apply: return plan, checked # `flush=True` is part of the safety contract: the complete byte-counted, # sentinel/process/candidate-evidence checked plan is externally visible # before the first recursive deletion can begin. print( json.dumps(plan, indent=2, sort_keys=True), file=output if output is not None else sys.stdout, flush=True, ) results = [ _delete_lane(lane, root=root, workflow=workflow, apply=True) for lane in lanes ] return plan, results def _ledger_path(root: Path) -> Path: return root / ".codex" / "session" / "build-storage.json" def _atomic_json(path: Path, document: Mapping[str, Any]) -> None: if path.is_symlink(): raise StorageError(f"refusing to replace symlinked JSON state: {path}") path.parent.mkdir(parents=True, exist_ok=True) descriptor, temporary = tempfile.mkstemp( prefix=f".{path.name}.", suffix=".tmp", dir=path.parent ) try: with os.fdopen(descriptor, "w", encoding="utf-8") as handle: json.dump(document, handle, indent=2, sort_keys=True) handle.write("\n") handle.flush() os.fsync(handle.fileno()) os.replace(temporary, path) finally: try: os.unlink(temporary) except FileNotFoundError: pass def write_ledger(root: Path, event: Mapping[str, Any]) -> None: path = _ledger_path(root) path.parent.mkdir(parents=True, exist_ok=True) document: dict[str, Any] = {"schema_version": LEDGER_SCHEMA_VERSION, "events": []} if path.is_file() and not path.is_symlink(): try: with path.open("r", encoding="utf-8") as handle: existing = json.load(handle) if isinstance(existing, dict) and isinstance(existing.get("events"), list): document = existing except (OSError, json.JSONDecodeError): pass events = list(document.get("events", []))[-499:] events.append(dict(event)) document["schema_version"] = LEDGER_SCHEMA_VERSION document["events"] = events document["updated_at"] = utc_now() _atomic_json(path, document) def _summary(snapshot: Mapping[str, Any], evaluation: Mapping[str, Any]) -> dict[str, Any]: return { "timestamp": snapshot["timestamp"], "workspace": snapshot["workspace"], "total_allocated_bytes": snapshot["total_allocated_bytes"], "total": human_bytes(snapshot["total_allocated_bytes"]), "repository_target": { "path": snapshot["repository_target"]["path"], "allocated_bytes": snapshot["repository_target"]["allocated_bytes"], "human": human_bytes(snapshot["repository_target"]["allocated_bytes"]), "largest": [ {"name": name, "bytes": size, "human": human_bytes(size)} for name, size in list( (snapshot["repository_target"].get("top_level") or {}).items() )[:10] ], }, "external_partition": { "path": snapshot["external_partition"]["path"], "allocated_bytes": snapshot["external_partition"]["allocated_bytes"], "human": human_bytes(snapshot["external_partition"]["allocated_bytes"]), }, "lanes": [ { "lane": lane["lane"], "bytes": lane["allocated_bytes"], "human": human_bytes(lane["allocated_bytes"]), "last_use": lane["last_use"], "valid": lane["valid"], } for lane in snapshot["lanes"] ], "legacy_intermediates": { name: { "path": usage["path"], "bytes": usage["allocated_bytes"], "human": human_bytes(usage["allocated_bytes"]), } for name, usage in snapshot["legacy_intermediates"].items() }, "filesystems": [ { **filesystem, "free": human_bytes(filesystem["free_bytes"]), "minimum_free": human_bytes(filesystem["minimum_free_bytes"]), } for filesystem in snapshot["filesystems"] ], "active_processes": snapshot["active_processes"], "sccache": snapshot["sccache"], "warnings": evaluation["warnings"], "violations": evaluation["violations"], "simulated": snapshot.get("simulated", False), } def _print_human(summary: Mapping[str, Any]) -> None: print(f"Build artifacts: {summary['total']}") repository = summary["repository_target"] print(f"Repository target: {repository['human']} ({repository['path']})") for item in repository["largest"][:6]: print(f" {item['name']}: {item['human']}") external = summary["external_partition"] print(f"Managed intermediates: {external['human']} ({external['path']})") if summary["lanes"]: print("Lanes:") for lane in summary["lanes"]: print( f" {lane['lane']}: {lane['human']} " f"({'valid' if lane['valid'] else 'INVALID SENTINEL'})" ) if summary["legacy_intermediates"]: print("Legacy intermediates still in repository target:") for name, usage in summary["legacy_intermediates"].items(): print(f" {name}: {usage['human']}") for filesystem in summary["filesystems"]: print( f"Free on {filesystem['mount']}: {filesystem['free']} " f"(floor {filesystem['minimum_free']})" ) if summary["active_processes"]: print("Active build/artifact users:") for process in summary["active_processes"]: print(f" PID {process['pid']} {process['comm']}: {process['command']}") for warning in summary["warnings"]: print(f"WARNING: {warning}") for violation in summary["violations"]: print(f"BLOCK: {violation}") def _snapshot_with_simulation( root: Path, workflow: Mapping[str, Any], arguments: argparse.Namespace ) -> tuple[dict[str, Any], dict[str, list[str]]]: snapshot = storage_snapshot(root, workflow) apply_simulation(snapshot, arguments) return snapshot, evaluate_budget(snapshot) def _make_preserved_record( path: Path, *, root: Path, lane_paths: Sequence[Path], label: str ) -> dict[str, Any]: original = path.expanduser() if not original.is_absolute(): original = root / original if original.is_symlink(): raise StorageError(f"{label} must not be a symlink: {original}") resolved = canonical_path(original) if any(_path_within(resolved, lane_path) for lane_path in lane_paths): raise StorageError( f"{label} must be copied outside the disposable candidate lane: {resolved}" ) if not resolved.is_file(): raise StorageError(f"{label} is not a file: {resolved}") return { "path": str(resolved), "bytes": resolved.stat().st_size, "sha256": _sha256_file(resolved), } def _mark_candidate_preserved( root: Path, workflow: Mapping[str, Any], arguments: argparse.Namespace ) -> int: snapshot = storage_snapshot(root, workflow) lane_name = _candidate_lane_name(workflow) lane = _lane_by_name(snapshot, lane_name) if lane is None: raise StorageError(f"managed candidate lane does not exist: {lane_name}") if not lane.get("valid"): raise StorageError("candidate lane has an invalid sentinel") generation = _lane_generation(lane) if generation is None: raise StorageError("candidate lane generation cannot be established") lane_paths = [canonical_path(Path(item["path"])) for item in lane["artifacts"]] evidence = _make_preserved_record( arguments.evidence, root=root, lane_paths=lane_paths, label="candidate evidence manifest", ) artifacts = [ _make_preserved_record( path, root=root, lane_paths=lane_paths, label=f"preserved candidate artifact {index}", ) for index, path in enumerate(arguments.artifact) ] evidence_path = evidence["path"] if all(record["path"] == evidence_path for record in artifacts): raise StorageError( "candidate evidence alone is insufficient; preserve at least one distinct artifact" ) document = { "schema_version": CANDIDATE_PRESERVATION_SCHEMA_VERSION, "workspace": str(canonical_path(root)), "workspace_hash": workspace_hash(root), "lane": lane_name, "lane_generation": generation, "prune_safe": True, "preserved_at": utc_now(), "evidence": evidence, "preserved_artifacts": artifacts, } marker_path = _candidate_preservation_path(root) _atomic_json(marker_path, document) errors = _candidate_preservation_errors(lane, root=root, workflow=workflow) if errors: raise StorageError("candidate preservation marker failed validation: " + "; ".join(errors)) write_ledger( root, { "timestamp": utc_now(), "action": "mark-candidate-preserved", "lane": lane_name, "lane_generation": generation, "marker": str(marker_path), "evidence": evidence, "preserved_artifacts": artifacts, }, ) print(json.dumps(document, indent=2, sort_keys=True)) return 0 def _status(root: Path, workflow: Mapping[str, Any], arguments: argparse.Namespace) -> int: snapshot, evaluation = _snapshot_with_simulation(root, workflow, arguments) summary = _summary(snapshot, evaluation) if arguments.json: print(json.dumps(summary, indent=2, sort_keys=True)) else: _print_human(summary) return 0 def _plan(root: Path, workflow: Mapping[str, Any], arguments: argparse.Namespace) -> int: snapshot, evaluation = _snapshot_with_simulation(root, workflow, arguments) document = { "summary": _summary(snapshot, evaluation), "prune": prune_plan(snapshot), "legacy_migration": { "required": bool(snapshot["legacy_intermediates"]), "automatic": False, "reason": ( "unmanaged repository intermediates require the separately authorized " "one-time migration; they are never removed by prune" ), }, } if arguments.json: print(json.dumps(document, indent=2, sort_keys=True)) else: _print_human(document["summary"]) print("Prune plan:") for item in document["prune"]: disposition = "eligible" if item["eligible"] else "keep" print(f" {item['lane']}: {disposition} ({item['reason']})") if document["legacy_migration"]["required"]: print("Legacy target migration is required and will not be auto-pruned.") return 0 def _prune(root: Path, workflow: Mapping[str, Any], arguments: argparse.Namespace) -> int: snapshot, evaluation = _snapshot_with_simulation(root, workflow, arguments) if snapshot.get("simulated") and arguments.apply: raise StorageError("--apply is forbidden with simulated inputs") candidates = {item["lane"]: item for item in prune_plan(snapshot) if item["eligible"]} lanes: list[dict[str, Any]] = [] for lane_name in sorted(candidates): lane = _lane_by_name(snapshot, lane_name) if lane is not None: lanes.append(lane) checked_plan, results = _execute_checked_deletions( lanes, root=root, workflow=workflow, action="prune", apply=bool(arguments.apply), ) event = { "timestamp": utc_now(), "action": "prune", "apply": bool(arguments.apply), "before_bytes": snapshot["total_allocated_bytes"], "checked_plan": checked_plan, "results": results, "violations": evaluation["violations"], } if arguments.apply: write_ledger(root, event) print(json.dumps(event, indent=2, sort_keys=True)) return 2 if any(result["blocked"] for result in results) else 0 def _reset_lane(root: Path, workflow: Mapping[str, Any], arguments: argparse.Namespace) -> int: lane_name = validate_lane_name(arguments.lane) snapshot, _ = _snapshot_with_simulation(root, workflow, arguments) if snapshot.get("simulated") and arguments.apply: raise StorageError("--apply is forbidden with simulated inputs") lane = _lane_by_name(snapshot, lane_name) if lane is None: raise StorageError(f"managed lane does not exist: {lane_name}") checked_plan, results = _execute_checked_deletions( [lane], root=root, workflow=workflow, action="reset-lane", apply=bool(arguments.apply), ) result = results[0] event = { "timestamp": utc_now(), "action": "reset-lane", "apply": bool(arguments.apply), "checked_plan": checked_plan, "result": result, } if arguments.apply: write_ledger(root, event) print(json.dumps(event, indent=2, sort_keys=True)) return 2 if result["blocked"] else 0 def _enforce(root: Path, workflow: Mapping[str, Any], arguments: argparse.Namespace) -> int: snapshot, evaluation = _snapshot_with_simulation(root, workflow, arguments) before_bytes = int(snapshot["total_allocated_bytes"]) before_warnings = list(evaluation["warnings"]) before_violations = list(evaluation["violations"]) pressure = bool(evaluation["violations"] or evaluation["warnings"]) eligible = [item for item in prune_plan(snapshot) if item["eligible"]] checked_plan: dict[str, Any] | None = None prune_results: list[dict[str, Any]] = [] policy = _policy(workflow) auto_prune = bool(policy.get("auto_prune_disposable", True)) if pressure and eligible and auto_prune and not snapshot.get("simulated"): lanes = [ lane for item in eligible if (lane := _lane_by_name(snapshot, str(item["lane"]))) is not None ] checked_plan, prune_results = _execute_checked_deletions( lanes, root=root, workflow=workflow, action=f"enforce-{arguments.phase}", apply=True, # Keep --json stdout machine-readable while still making the # mandatory pre-delete plan visible before recursive deletion. output=sys.stderr if arguments.json else sys.stdout, ) snapshot = storage_snapshot(root, workflow) evaluation = evaluate_budget(snapshot) event = { "timestamp": utc_now(), "action": "enforce", "phase": arguments.phase, "before_bytes": before_bytes, "total_allocated_bytes": snapshot["total_allocated_bytes"], "reclaimed_bytes": max( 0, before_bytes - int(snapshot["total_allocated_bytes"]) ), "warnings_before_prune": before_warnings, "violations_before_prune": before_violations, "warnings": evaluation["warnings"], "violations": evaluation["violations"], "auto_prune_enabled": auto_prune, "auto_prune_would_apply": bool( pressure and eligible and auto_prune and snapshot.get("simulated") ), "destructive": any(result["deleted"] for result in prune_results), "checked_plan": checked_plan, "prune_results": prune_results, "eligible_prune_bytes": sum( int(item["allocated_bytes"]) for item in eligible ), "eligible_prune_lanes": [item["lane"] for item in eligible], "prune_recommended": pressure and bool(eligible), "simulated": snapshot.get("simulated", False), } if not snapshot.get("simulated"): write_ledger(root, event) if arguments.json: print(json.dumps(event, indent=2, sort_keys=True)) else: _print_human(_summary(snapshot, evaluation)) if pressure and eligible: if auto_prune and snapshot.get("simulated"): print("Simulation only: expired disposable lanes would be pruned.") elif not auto_prune: print( "Automatic pruning is disabled. Review `build_storage.py prune " "--dry-run` before any explicit apply." ) return 2 if evaluation["violations"] else 0 def _write_test_marker( path: Path, root: Path, lane: str, role: str, *, last_use: str | None = None, ) -> None: path.mkdir(parents=True, exist_ok=True) document = { "schema_version": 1, "workspace": str(canonical_path(root)), "workspace_hash": workspace_hash(root), "lane": lane, "role": role, "path": str(canonical_path(path)), "created_at": utc_now(), "last_use": last_use or utc_now(), } sentinel_path(path).write_text(json.dumps(document), encoding="utf-8") def _self_test() -> dict[str, Any]: checks: list[str] = [] fake = { "policy": dict(DEFAULT_POLICY), "total_allocated_bytes": 81 * GIB, "lanes": [], "filesystems": [ {"mount": "/test", "free_bytes": 100 * GIB, "minimum_free_bytes": 40 * GIB} ], } assert evaluate_budget(fake)["violations"] checks.append("simulated-hard-budget-crossing") fake["total_allocated_bytes"] = 56 * GIB soft = evaluate_budget(fake) assert not soft["violations"] and soft["warnings"] checks.append("simulated-soft-budget-crossing") fake["total_allocated_bytes"] = 1 * GIB fake["filesystems"][0]["free_bytes"] = 39 * GIB assert evaluate_budget(fake)["violations"] checks.append("simulated-free-space-floor") with tempfile.TemporaryDirectory(prefix="blacksite-storage-self-test-") as temporary: base = Path(temporary) root = base / "workspace" (root / ".git").mkdir(parents=True) (root / "Cargo.toml").write_text("[workspace]\nmembers=[]\n", encoding="utf-8") cache = base / "cache" workflow = { "build_storage": { **DEFAULT_POLICY, "cache_root": str(cache), "workspace_partition": "canonical-path-hash", "exceptional_target_root": "target/lanes", } } lane_path = cache / workspace_hash(root) / "candidate" _write_test_marker(lane_path, root, "candidate", "build") artifact = { "path": str(lane_path), "lane": "candidate", "role": "build", } assert not deletion_safety( artifact, root=root, workflow=workflow, processes=[] ) checks.append("valid-sentinel-path-accepted") link = lane_path / "escape" link.symlink_to(root) errors = deletion_safety( artifact, root=root, workflow=workflow, processes=[] ) assert any("symlink" in error for error in errors) checks.append("internal-symlink-rejected") link.unlink() errors = deletion_safety( artifact, root=root, workflow=workflow, processes=[ { "pid": 42, "comm": "cargo", "uses": [str(lane_path)], "build_tool": True, "packager": False, "workspace_related": True, } ], ) assert any("active process" in error for error in errors) checks.append("active-process-rejected") unmarked = cache / workspace_hash(root) / "unmarked" unmarked.mkdir(parents=True) errors = deletion_safety( {"path": str(unmarked), "lane": "unmarked", "role": "build"}, root=root, workflow=workflow, processes=[], ) assert any("missing or malformed" in error for error in errors) checks.append("unmarked-lane-rejected") for protected_path, label in ( (root, "workspace-root"), (root.parent, "workspace-parent"), (Path.home(), "home"), (cargo_home(), "cargo-home"), ): errors = deletion_safety( { "path": str(protected_path), "lane": "candidate", "role": "build", }, root=root, workflow=workflow, processes=[], ) assert errors checks.append(f"{label}-rejected") # Candidate deletion requires a generation-bound marker whose evidence # and preserved artifacts still exist outside the disposable lane. dev_path = cache / workspace_hash(root) / "dev" _write_test_marker(dev_path, root, "dev", "build") evidence = base / "evidence" / "candidate-manifest.json" evidence.parent.mkdir(parents=True) evidence.write_text('{"candidate":"test"}\n', encoding="utf-8") preserved_artifact = base / "preserved" / "editor-package.tar.zst" preserved_artifact.parent.mkdir(parents=True) preserved_artifact.write_bytes(b"preserved candidate package") lane = { "lane": "candidate", "allocated_bytes": measure_tree(lane_path).allocated_bytes, "artifacts": [artifact], } blocked_output = io.StringIO() _, blocked_results = _execute_checked_deletions( [lane], root=root, workflow=workflow, action="self-test-candidate", apply=True, output=blocked_output, ) assert blocked_results[0]["blocked"] and lane_path.exists() assert any( "preservation marker is missing" in error for error in blocked_results[0]["blocked"] ) checks.append("candidate-without-preservation-marker-rejected") mark_arguments = argparse.Namespace( evidence=evidence, artifact=[preserved_artifact] ) with contextlib.redirect_stdout(io.StringIO()): assert _mark_candidate_preserved(root, workflow, mark_arguments) == 0 assert not _candidate_preservation_errors( lane, root=root, workflow=workflow ) checks.append("candidate-preservation-marker-validated") class FlushProbe(io.StringIO): def __init__(self, guarded_path: Path) -> None: super().__init__() self.guarded_path = guarded_path self.flushed_before_delete = False def flush(self) -> None: assert self.guarded_path.exists() self.flushed_before_delete = True super().flush() output = FlushProbe(lane_path) checked_plan, results = _execute_checked_deletions( [lane], root=root, workflow=workflow, action="self-test-candidate", apply=True, output=output, ) deletion = results[0] assert output.flushed_before_delete assert checked_plan["total_allocated_bytes"] == lane["allocated_bytes"] assert "self-test-candidate-checked-plan" in output.getvalue() assert not deletion["blocked"] and deletion["deleted"] assert not lane_path.exists() assert dev_path.exists() and evidence.is_file() and preserved_artifact.is_file() checks.append("checked-plan-flushed-before-candidate-prune") checks.append("candidate-pruned-artifact-evidence-and-dev-preserved") # Recreating the candidate lane invalidates the old generation-bound # marker, so a stale preservation decision cannot authorize deletion. _write_test_marker( lane_path, root, "candidate", "build", last_use="2099-01-01T00:00:00+00:00", ) lane["allocated_bytes"] = measure_tree(lane_path).allocated_bytes stale = _delete_lane(lane, root=root, workflow=workflow, apply=False) assert any("lane_generation" in error for error in stale["blocked"]) checks.append("stale-candidate-preservation-marker-rejected") # Enforcement reports pressure and eligible lanes but never performs a # deletion. Auto-prune must emit its complete checked plan first. hot_path = cache / workspace_hash(root) / "hot-reload" _write_test_marker( hot_path, root, "hot-reload", "build", last_use="2000-01-01T00:00:00+00:00", ) workflow["build_storage"].update( { "soft_total_gib": 0, "hard_total_gib": 100000, "min_free_gib": 0, "min_free_percent": 0, "hot_reload_retention_hours": 0, "auto_prune_disposable": True, } ) enforce_arguments = argparse.Namespace( phase="pre", json=True, simulate_total_gib=None, simulate_free_gib=None, simulate_lane_gib=[], ) enforce_output = io.StringIO() enforce_plan_output = io.StringIO() with contextlib.redirect_stdout(enforce_output), contextlib.redirect_stderr( enforce_plan_output ): assert _enforce(root, workflow, enforce_arguments) == 0 enforce_event = json.loads(enforce_output.getvalue()) enforce_plan = json.loads(enforce_plan_output.getvalue()) assert enforce_plan["action"] == "enforce-pre-checked-plan" assert enforce_plan["total_allocated_bytes"] > 0 assert enforce_event["destructive"] is True assert "hot-reload" in enforce_event["eligible_prune_lanes"] assert not hot_path.exists() checks.append("enforce-pressure-prunes-after-checked-plan") return {"ok": True, "checks": checks} def _add_simulation_options(parser: argparse.ArgumentParser) -> None: parser.add_argument("--simulate-total-gib", type=float) parser.add_argument("--simulate-free-gib", type=float) parser.add_argument( "--simulate-lane-gib", action="append", default=[], metavar="LANE=GIB", ) def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--workspace", type=Path, help="workspace root override") subparsers = parser.add_subparsers(dest="action", required=True) for name in ("status", "plan"): command = subparsers.add_parser(name) command.add_argument("--json", action="store_true") _add_simulation_options(command) enforce = subparsers.add_parser("enforce") enforce.add_argument("--phase", choices=("pre", "post"), required=True) enforce.add_argument("--json", action="store_true") _add_simulation_options(enforce) prune = subparsers.add_parser("prune") mode = prune.add_mutually_exclusive_group(required=True) mode.add_argument("--dry-run", action="store_true") mode.add_argument("--apply", action="store_true") _add_simulation_options(prune) reset = subparsers.add_parser("reset-lane") reset.add_argument("lane") mode = reset.add_mutually_exclusive_group(required=True) mode.add_argument("--dry-run", action="store_true") mode.add_argument("--apply", action="store_true") _add_simulation_options(reset) preserved = subparsers.add_parser("mark-candidate-preserved") preserved.add_argument( "--evidence", type=Path, required=True, help="preserved candidate evidence manifest outside the disposable lane", ) preserved.add_argument( "--artifact", type=Path, action="append", required=True, help="preserved candidate binary/package file; repeat for multiple artifacts", ) subparsers.add_parser("self-test") return parser def main(argv: Sequence[str] | None = None) -> int: arguments = build_parser().parse_args(argv) if arguments.action == "self-test": print(json.dumps(_self_test(), indent=2, sort_keys=True)) return 0 try: root = workspace_root(arguments.workspace) workflow = load_workflow(root) if arguments.action == "status": return _status(root, workflow, arguments) if arguments.action == "plan": return _plan(root, workflow, arguments) if arguments.action == "enforce": return _enforce(root, workflow, arguments) if arguments.action == "prune": return _prune(root, workflow, arguments) if arguments.action == "reset-lane": return _reset_lane(root, workflow, arguments) if arguments.action == "mark-candidate-preserved": return _mark_candidate_preserved(root, workflow, arguments) except (LaneError, StorageError, OSError, ValueError) as error: print(f"build-storage: {error}", file=sys.stderr) return 2 return 2 if __name__ == "__main__": raise SystemExit(main())