This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
orchestrator/agent-team/agent_team/state_store.py

176 lines
6.4 KiB
Python

"""Atomic state-store utilities and integrity checking (design §6.7).
All durable state on the R720 (LangGraph SQLite checkpoint, the
``pending_questions`` ledger, the budget ledger, the Plane-1 rotation/coverage
pointer) is written atomically (write-temp-then-fsync-then-rename) and
integrity-checked on load. "Integrity-checked" is concrete here: a
schema-version match plus a stored content hash. On any mismatch the loader
refuses to proceed silently and raises :class:`IntegrityError` so the
coordinator can park the affected task with an ALARM rather than acting on
corrupt state.
This module is pure stdlib (``os``, ``tempfile``, ``hashlib``, ``pathlib``)
and depends on no other ``agent_team`` module. The leaf builders import these
signatures verbatim, so they are intentionally explicit and final.
"""
from __future__ import annotations
import hashlib
import json
import os
import tempfile
from pathlib import Path
__all__ = [
"IntegrityError",
"atomic_write",
"compute_content_hash",
"read_checked",
]
# Sidecar files sit next to the protected payload and carry the integrity
# metadata (schema version + content hash). Keeping them separate from the
# payload means the payload bytes round-trip unchanged.
_META_SUFFIX = ".meta.json"
# Algorithm used for the stored content hash. Recorded in the sidecar so a
# future algorithm change stays backward-readable.
_HASH_ALGO = "sha256"
class IntegrityError(Exception):
"""Raised when durable state fails its integrity check on load.
Signals a schema-version mismatch, a missing/garbled integrity sidecar, or
a stored-content-hash mismatch (corruption or tampering). Callers treat
this as "refuse to proceed silently": park the task and ALARM rather than
restart blindly (§6.7).
"""
def compute_content_hash(data: bytes) -> str:
"""Return the hex content hash for ``data`` (sha256).
The same routine is used when writing the sidecar and when verifying on
load, so the two are guaranteed consistent.
"""
return hashlib.new(_HASH_ALGO, data).hexdigest()
def _meta_path(path: Path) -> Path:
"""Return the sidecar metadata path for a payload ``path``."""
return path.with_name(path.name + _META_SUFFIX)
def atomic_write(path: Path, data: bytes) -> None:
"""Atomically write ``data`` to ``path`` (write-temp -> fsync -> rename).
The bytes are written to a temporary file in the same directory, flushed
and ``fsync``-ed to durable storage, then ``os.replace``-d onto the final
path. ``os.replace`` is atomic on POSIX within a filesystem, so a reader
never observes a half-written file and a crash mid-write leaves either the
old payload or the new one, never a torn one. The containing directory is
``fsync``-ed afterward so the rename itself is durable.
This writes only the payload; integrity metadata is written by callers via
:func:`write_checked` / read back by :func:`read_checked`. (The sidecar is
written through this same primitive, so it is equally crash-safe.)
"""
path = Path(path)
directory = path.parent
directory.mkdir(parents=True, exist_ok=True)
# delete=False so we control the rename; same dir guarantees same fs.
fd, tmp_name = tempfile.mkstemp(
prefix=path.name + ".", suffix=".tmp", dir=directory
)
tmp_path = Path(tmp_name)
try:
with os.fdopen(fd, "wb") as handle:
handle.write(data)
handle.flush()
os.fsync(handle.fileno())
os.replace(tmp_path, path)
except BaseException:
# Best-effort cleanup of the temp file on any failure.
try:
os.unlink(tmp_path)
except FileNotFoundError:
pass
raise
# Make the rename itself durable by fsync-ing the directory.
dir_fd = os.open(directory, os.O_RDONLY)
try:
os.fsync(dir_fd)
except OSError:
# Some filesystems disallow directory fsync; the rename is still
# atomic, only its durability across power-loss is weakened.
pass
finally:
os.close(dir_fd)
def write_checked(path: Path, data: bytes, *, schema_version: int) -> None:
"""Atomically write ``data`` plus its integrity sidecar.
Writes the payload first, then the sidecar carrying ``schema_version`` and
the content hash. :func:`read_checked` verifies both. Both writes go
through :func:`atomic_write`, so each is crash-safe; if a crash lands
between them the sidecar is simply stale/absent and :func:`read_checked`
fails closed with :class:`IntegrityError`, which is the intended
refuse-to-proceed behaviour.
"""
path = Path(path)
atomic_write(path, data)
meta = {
"schema_version": int(schema_version),
"hash_algo": _HASH_ALGO,
"content_hash": compute_content_hash(data),
}
atomic_write(_meta_path(path), json.dumps(meta, sort_keys=True).encode("utf-8"))
def read_checked(path: Path, *, schema_version: int) -> bytes:
"""Read and integrity-check ``path``, returning its bytes.
Verifies the integrity sidecar exists, that its recorded
``schema_version`` matches the expected ``schema_version``, and that the
stored content hash matches a freshly computed hash of the payload bytes.
Any mismatch (missing/garbled sidecar, schema drift, corruption/tampering)
raises :class:`IntegrityError`.
"""
path = Path(path)
try:
data = path.read_bytes()
except FileNotFoundError as exc:
raise IntegrityError(f"state payload missing: {path}") from exc
meta_path = _meta_path(path)
try:
raw_meta = meta_path.read_bytes()
except FileNotFoundError as exc:
raise IntegrityError(f"integrity sidecar missing: {meta_path}") from exc
try:
meta = json.loads(raw_meta)
except (ValueError, UnicodeDecodeError) as exc:
raise IntegrityError(f"integrity sidecar unreadable: {meta_path}") from exc
stored_version = meta.get("schema_version")
if stored_version != schema_version:
raise IntegrityError(
f"schema-version mismatch for {path}: "
f"stored={stored_version!r} expected={schema_version!r}"
)
stored_hash = meta.get("content_hash")
actual_hash = compute_content_hash(data)
if stored_hash != actual_hash:
raise IntegrityError(
f"content-hash mismatch for {path}: "
f"stored={stored_hash!r} actual={actual_hash!r}"
)
return data