atlas-iac/services/hermes/scripts/cli_lane_evidence.py

295 lines
10 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Immutable terminal evidence creation and identity-safe retirement."""
from __future__ import annotations
import ctypes
2026-08-17 08:16:35 -03:00
import contextlib
import errno
import hashlib
import json
import os
import stat
import uuid
from pathlib import Path
from typing import Any
from cli_lane_config import MAX_TERMINAL_RECORD_BYTES, TerminalIdentity, utc_now
from cli_lane_files import _terminal_evidence_identity, state_path
from cli_lane_records import _load_small_json, _terminal_record_valid
def _rename_noreplace(
source: str,
destination: str,
*,
source_dir: int,
destination_dir: int,
) -> None:
"""Atomically rename a directory entry without replacing a collision."""
renameat2 = getattr(ctypes.CDLL(None, use_errno=True), "renameat2", None)
if renameat2 is None:
raise OSError(errno.ENOSYS, "renameat2 is unavailable")
renameat2.argtypes = (
ctypes.c_int,
ctypes.c_char_p,
ctypes.c_int,
ctypes.c_char_p,
ctypes.c_uint,
)
renameat2.restype = ctypes.c_int
result = renameat2(
source_dir,
ctypes.c_char_p(os.fsencode(source)),
destination_dir,
ctypes.c_char_p(os.fsencode(destination)),
1, # RENAME_NOREPLACE
)
if result != 0:
error = ctypes.get_errno()
raise OSError(error, os.strerror(error), source, destination)
def _retire_terminal_entry(
path: Path,
source_stat: os.stat_result,
*,
board_descriptor: int,
quarantine_descriptor: int,
authority_name: str | None = None,
source_descriptor: int | None = None,
) -> str:
"""Retire only the inode previously inspected; preserve any replacement."""
pinned = os.fstat(source_descriptor) if source_descriptor is not None else source_stat
if pinned.st_dev != source_stat.st_dev or pinned.st_ino != source_stat.st_ino:
return "replacement"
try:
current = os.stat(
path.name,
dir_fd=board_descriptor,
follow_symlinks=False,
)
except FileNotFoundError:
return "missing"
if current.st_dev != pinned.st_dev or current.st_ino != pinned.st_ino:
return "replacement"
# A replacement can itself be staged repeatedly while recovery races a
# writer. Keep every generation bound to the original canonical pending
# name instead of hashing an intermediate .retire name and hiding it from
# the next recovery pass.
path_digest = hashlib.sha256(
(authority_name or path.name).encode("utf-8")
).hexdigest()[:16]
source_digest = hashlib.sha256(
f"{pinned.st_dev:x}\0{pinned.st_ino:x}".encode()
).hexdigest()[:16]
for sequence in range(32):
staging = f".retire.{path_digest}.{source_digest}.{sequence}"
try:
_rename_noreplace(
path.name,
staging,
source_dir=board_descriptor,
destination_dir=quarantine_descriptor,
)
except FileExistsError:
continue
break
else:
return "collision"
staged = os.stat(
staging,
dir_fd=quarantine_descriptor,
follow_symlinks=False,
)
if staged.st_dev == pinned.st_dev and staged.st_ino == pinned.st_ino:
os.unlink(staging, dir_fd=quarantine_descriptor)
os.fsync(quarantine_descriptor)
os.fsync(board_descriptor)
return "retired"
# The path changed after the pre-check but before the rename. Put the
# replacement back without overwriting an even newer entry.
try:
_rename_noreplace(
staging,
path.name,
source_dir=quarantine_descriptor,
destination_dir=board_descriptor,
)
except FileExistsError:
return "replacement-staged"
os.fsync(quarantine_descriptor)
os.fsync(board_descriptor)
return "replacement"
def _write_json_noreplace(path: Path, value: dict[str, Any]) -> bool:
"""Durably create one immutable JSON artifact without replacing a peer."""
payload = (json.dumps(value, indent=2, sort_keys=True) + "\n").encode("utf-8")
if len(payload) > MAX_TERMINAL_RECORD_BYTES + 4096:
raise ValueError("terminal evidence exceeds the bounded artifact limit")
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
os.chmod(path.parent, 0o700, follow_symlinks=False)
directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
directory_flags |= getattr(os, "O_NOFOLLOW", 0)
create_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
create_flags |= getattr(os, "O_NOFOLLOW", 0)
directory = os.open(path.parent, directory_flags)
temporary = f".{path.name}.{uuid.uuid4().hex}.tmp"
temporary_stat = None
descriptor = None
cleanup_descriptor = None
try:
descriptor = os.open(temporary, create_flags, 0o600, dir_fd=directory)
temporary_stat = os.fstat(descriptor)
with os.fdopen(descriptor, "wb") as stream:
descriptor = None
stream.write(payload)
stream.flush()
os.fsync(stream.fileno())
written = os.fstat(stream.fileno())
if stat.S_IMODE(written.st_mode) != 0o600 or written.st_nlink != 1:
raise OSError("terminal evidence is not private and singly linked")
cleanup_descriptor = os.dup(stream.fileno())
try:
_rename_noreplace(
temporary,
path.name,
source_dir=directory,
destination_dir=directory,
)
except FileExistsError:
return False
os.fsync(directory)
return True
finally:
if descriptor is not None:
os.close(descriptor)
if temporary_stat is not None:
2026-08-17 08:16:35 -03:00
with contextlib.suppress(OSError):
_retire_terminal_entry(
Path(temporary),
temporary_stat,
board_descriptor=directory,
quarantine_descriptor=directory,
source_descriptor=cleanup_descriptor,
)
if cleanup_descriptor is not None:
os.close(cleanup_descriptor)
os.close(directory)
def _terminal_document_digest(document: dict[str, Any]) -> str:
"""Return the stable identity for one exact accepted result document."""
immutable = {
key: document.get(key)
for key in (
"board",
"task_id",
"expected_run_id",
"result",
"summary",
"metadata",
)
}
canonical = json.dumps(immutable, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:32]
def _terminal_evidence_path(
identity: TerminalIdentity,
state: str,
document: dict[str, Any],
) -> Path:
"""Return a deterministic immutable prepared or conflict evidence path."""
if state not in {"prepared", "conflict"}:
raise ValueError(f"invalid terminal evidence state: {state}")
base = state_path(identity.board, identity.task_id)
digest = _terminal_document_digest(document)
return base.with_name(
f"{base.stem}.run-{identity.run_id}.terminal.{state}-{digest}.json"
)
def _same_terminal_document(left: dict[str, Any], right: dict[str, Any]) -> bool:
"""Compare the immutable result-bearing fields of terminal evidence."""
keys = ("board", "task_id", "expected_run_id", "result", "summary", "metadata")
return all(left.get(key) == right.get(key) for key in keys)
def _terminal_evidence_valid(
document: dict[str, Any],
identity: TerminalIdentity,
state: str,
) -> bool:
"""Validate semantic terminal content plus its immutable evidence state."""
if document.get("kanban_state") != state:
return False
semantic = dict(document)
semantic_identity = identity
if state == "conflict":
semantic["kanban_state"] = "prepared"
semantic_identity = TerminalIdentity(
identity.board,
identity.task_id,
identity.run_id,
"prepared",
)
return _terminal_record_valid(semantic, semantic_identity)
def _persist_prepared_evidence(
identity: TerminalIdentity,
document: dict[str, Any],
) -> Path:
"""Fsync an immutable accepted result before entering the DB boundary."""
prepared = dict(document)
prepared["kanban_state"] = "prepared"
prepared["prepared_at"] = utc_now()
path = _terminal_evidence_path(identity, "prepared", document)
if _write_json_noreplace(path, prepared):
return path
if _terminal_evidence_identity(path) != TerminalIdentity(
identity.board, identity.task_id, identity.run_id, "prepared"
):
raise OSError("prepared terminal evidence identity is invalid")
existing = _load_small_json(path)
prepared_identity = TerminalIdentity(
identity.board,
identity.task_id,
identity.run_id,
"prepared",
)
if not _terminal_evidence_valid(
existing,
prepared_identity,
"prepared",
) or not _same_terminal_document(existing, prepared):
raise OSError("prepared terminal evidence path contains a conflicting result")
return path
def _persist_conflict_evidence(
identity: TerminalIdentity,
document: dict[str, Any],
reason: str,
) -> Path:
"""Preserve a losing valid result in full under deterministic retention."""
conflict = dict(document)
conflict["kanban_state"] = "conflict"
conflict["conflict_reason"] = reason
conflict["conflicted_at"] = utc_now()
path = _terminal_evidence_path(identity, "conflict", document)
if _write_json_noreplace(path, conflict):
return path
if _terminal_evidence_identity(path) != TerminalIdentity(
identity.board, identity.task_id, identity.run_id, "conflict"
):
raise OSError("terminal conflict evidence identity is invalid")
existing = _load_small_json(path)
conflict_identity = TerminalIdentity(
identity.board,
identity.task_id,
identity.run_id,
"conflict",
)
if not _terminal_evidence_valid(
existing,
conflict_identity,
"conflict",
) or not _same_terminal_document(existing, conflict):
raise OSError("terminal conflict evidence path contains a different result")
return path