Blacksite/scripts/codex/build_storage.py

1746 lines
62 KiB
Python
Executable File

#!/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())