357 lines
11 KiB
Python
357 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""Bounded inode-pinned reads and validation for Hermes terminal records."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import stat
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import cli_lane_goal
|
|
from cli_lane_config import (
|
|
MAX_TERMINAL_RECORD_BYTES,
|
|
QUARANTINE_HASH_BYTES,
|
|
RESULT_SCHEMA,
|
|
Route,
|
|
TerminalIdentity,
|
|
TerminalRecoverySnapshot,
|
|
TerminalSnapshot,
|
|
canonical_run_id,
|
|
utc_now,
|
|
)
|
|
from cli_lane_files import (
|
|
_candidate_path,
|
|
_terminal_identity,
|
|
_terminal_path,
|
|
atomic_json,
|
|
)
|
|
|
|
|
|
def _read_bounded(descriptor: int, limit: int) -> bytes:
|
|
"""Read at most ``limit`` bytes from a regular file descriptor."""
|
|
chunks: list[bytes] = []
|
|
remaining = max(0, limit)
|
|
while remaining:
|
|
chunk = os.read(descriptor, min(64 * 1024, remaining))
|
|
if not chunk:
|
|
break
|
|
chunks.append(chunk)
|
|
remaining -= len(chunk)
|
|
return b"".join(chunks)
|
|
|
|
def _open_small_json_snapshot(path: Path) -> TerminalSnapshot | None:
|
|
"""Open and pin one bounded, singly-linked JSON document."""
|
|
directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
|
|
directory_flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
file_flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
|
|
directory = None
|
|
descriptor = None
|
|
try:
|
|
directory = os.open(path.parent, directory_flags)
|
|
descriptor = os.open(path.name, file_flags, dir_fd=directory)
|
|
opened = os.fstat(descriptor)
|
|
if (
|
|
not stat.S_ISREG(opened.st_mode)
|
|
or opened.st_nlink != 1
|
|
or opened.st_size > MAX_TERMINAL_RECORD_BYTES
|
|
):
|
|
return None
|
|
payload = _read_bounded(descriptor, MAX_TERMINAL_RECORD_BYTES + 1)
|
|
if len(payload) > MAX_TERMINAL_RECORD_BYTES:
|
|
return None
|
|
current = os.stat(
|
|
path.name,
|
|
dir_fd=directory,
|
|
follow_symlinks=False,
|
|
)
|
|
if (
|
|
current.st_dev != opened.st_dev
|
|
or current.st_ino != opened.st_ino
|
|
or current.st_size != opened.st_size
|
|
or len(payload) != opened.st_size
|
|
):
|
|
return None
|
|
value = json.loads(payload.decode("utf-8"))
|
|
if not isinstance(value, dict):
|
|
return None
|
|
snapshot = TerminalSnapshot(value, opened, descriptor, directory)
|
|
descriptor = None
|
|
directory = None
|
|
return snapshot
|
|
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
return None
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
if directory is not None:
|
|
os.close(directory)
|
|
|
|
def _open_terminal_recovery_snapshot(path: Path) -> TerminalRecoverySnapshot | None:
|
|
"""Pin the entry recovery inspected, even when its payload is malformed."""
|
|
directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
|
|
directory_flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
file_flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
|
|
directory = None
|
|
descriptor = None
|
|
try:
|
|
directory = os.open(path.parent, directory_flags)
|
|
observed = os.stat(
|
|
path.name,
|
|
dir_fd=directory,
|
|
follow_symlinks=False,
|
|
)
|
|
if not stat.S_ISREG(observed.st_mode):
|
|
snapshot = TerminalRecoverySnapshot(
|
|
None,
|
|
observed,
|
|
None,
|
|
directory,
|
|
b"",
|
|
"non-regular",
|
|
)
|
|
directory = None
|
|
return snapshot
|
|
try:
|
|
descriptor = os.open(path.name, file_flags, dir_fd=directory)
|
|
except OSError:
|
|
snapshot = TerminalRecoverySnapshot(
|
|
None,
|
|
observed,
|
|
None,
|
|
directory,
|
|
b"",
|
|
"open-failed",
|
|
)
|
|
directory = None
|
|
return snapshot
|
|
opened = os.fstat(descriptor)
|
|
if opened.st_dev != observed.st_dev or opened.st_ino != observed.st_ino:
|
|
os.close(descriptor)
|
|
descriptor = None
|
|
snapshot = TerminalRecoverySnapshot(
|
|
None,
|
|
observed,
|
|
None,
|
|
directory,
|
|
b"",
|
|
"identity-changed-during-open",
|
|
)
|
|
directory = None
|
|
return snapshot
|
|
observed = opened
|
|
if observed.st_nlink != 1:
|
|
snapshot = TerminalRecoverySnapshot(
|
|
None,
|
|
observed,
|
|
descriptor,
|
|
directory,
|
|
b"",
|
|
"hardlinked",
|
|
)
|
|
descriptor = None
|
|
directory = None
|
|
return snapshot
|
|
if observed.st_size > MAX_TERMINAL_RECORD_BYTES:
|
|
prefix = _read_bounded(descriptor, QUARANTINE_HASH_BYTES)
|
|
snapshot = TerminalRecoverySnapshot(
|
|
None,
|
|
observed,
|
|
descriptor,
|
|
directory,
|
|
prefix,
|
|
"oversized",
|
|
)
|
|
descriptor = None
|
|
directory = None
|
|
return snapshot
|
|
payload = _read_bounded(descriptor, MAX_TERMINAL_RECORD_BYTES + 1)
|
|
after_read = os.fstat(descriptor)
|
|
unchanged = (
|
|
after_read.st_dev == observed.st_dev
|
|
and after_read.st_ino == observed.st_ino
|
|
and after_read.st_size == observed.st_size
|
|
and after_read.st_mtime_ns == observed.st_mtime_ns
|
|
and after_read.st_ctime_ns == observed.st_ctime_ns
|
|
and len(payload) == observed.st_size
|
|
)
|
|
document = None
|
|
invalid_reason = "unstable-payload"
|
|
if unchanged:
|
|
try:
|
|
parsed = json.loads(payload.decode("utf-8"))
|
|
if isinstance(parsed, dict):
|
|
document = parsed
|
|
invalid_reason = None
|
|
else:
|
|
invalid_reason = "non-object-payload"
|
|
except (UnicodeError, json.JSONDecodeError):
|
|
invalid_reason = "malformed-payload"
|
|
snapshot = TerminalRecoverySnapshot(
|
|
document,
|
|
after_read,
|
|
descriptor,
|
|
directory,
|
|
payload[:QUARANTINE_HASH_BYTES],
|
|
invalid_reason,
|
|
)
|
|
descriptor = None
|
|
directory = None
|
|
return snapshot
|
|
except OSError:
|
|
return None
|
|
finally:
|
|
if descriptor is not None:
|
|
os.close(descriptor)
|
|
if directory is not None:
|
|
os.close(directory)
|
|
|
|
def _open_terminal_snapshot(
|
|
path: Path,
|
|
identity: TerminalIdentity,
|
|
) -> TerminalSnapshot | None:
|
|
"""Open a terminal journal only after its lexical identity is verified."""
|
|
if _terminal_identity(path) != identity:
|
|
return None
|
|
return _open_small_json_snapshot(path)
|
|
|
|
def _load_terminal_json(path: Path, identity: TerminalIdentity) -> dict[str, Any]:
|
|
"""Read one small, singly-linked journal through its validated directory."""
|
|
snapshot = _open_terminal_snapshot(path, identity)
|
|
if snapshot is None:
|
|
return {}
|
|
try:
|
|
return snapshot.document
|
|
finally:
|
|
snapshot.close()
|
|
|
|
def _load_small_json(path: Path) -> dict[str, Any]:
|
|
"""Read one bounded immutable evidence document without following links."""
|
|
snapshot = _open_small_json_snapshot(path)
|
|
if snapshot is None:
|
|
return {}
|
|
try:
|
|
return snapshot.document
|
|
finally:
|
|
snapshot.close()
|
|
|
|
def _persist_candidate(
|
|
state: dict[str, Any],
|
|
state_file: Path,
|
|
structured: dict[str, Any],
|
|
*,
|
|
route: Route,
|
|
returncode: int,
|
|
goal_turn: int,
|
|
) -> Path:
|
|
"""Persist every structured response before any judge can supersede it."""
|
|
sequence = max(0, int(state.get("candidate_sequence", 0) or 0)) + 1
|
|
path = _candidate_path(state_file, state.get("run_id"), sequence)
|
|
while path.exists():
|
|
sequence += 1
|
|
path = _candidate_path(state_file, state.get("run_id"), sequence)
|
|
atomic_json(
|
|
path,
|
|
{
|
|
"board": state.get("board"),
|
|
"task_id": state.get("task_id"),
|
|
"expected_run_id": state.get("run_id"),
|
|
"goal_turn": goal_turn,
|
|
"provider": route.provider,
|
|
"model": route.model,
|
|
"effort": route.effort,
|
|
"returncode": returncode,
|
|
"structured": structured,
|
|
"recorded_at": utc_now(),
|
|
},
|
|
)
|
|
state["candidate_sequence"] = sequence
|
|
state["last_candidate_file"] = str(path)
|
|
atomic_json(state_file, state)
|
|
return path
|
|
|
|
def _write_terminal_record(
|
|
state_file: Path,
|
|
*,
|
|
board: str,
|
|
task_id: str,
|
|
run_id: Any,
|
|
structured: dict[str, Any],
|
|
summary: str,
|
|
metadata: dict[str, Any],
|
|
) -> tuple[Path, dict[str, Any]]:
|
|
"""Journal an accepted result before attempting the Kanban transaction."""
|
|
if type(run_id) is not int or canonical_run_id(run_id) is None:
|
|
raise ValueError("terminal run ID must be a positive SQLite int64")
|
|
path = _terminal_path(state_file, run_id)
|
|
record = {
|
|
"board": board,
|
|
"task_id": task_id,
|
|
"expected_run_id": run_id,
|
|
"result": json.dumps(structured, sort_keys=True),
|
|
"summary": summary,
|
|
"metadata": metadata,
|
|
"kanban_state": "pending",
|
|
"recorded_at": utc_now(),
|
|
}
|
|
atomic_json(path, record)
|
|
return path, record
|
|
|
|
def _terminal_record_valid(
|
|
record: dict[str, Any],
|
|
identity: TerminalIdentity | None = None,
|
|
) -> bool:
|
|
"""Reject malformed or non-terminal replay journals without side effects."""
|
|
if not isinstance(record, dict):
|
|
return False
|
|
required_strings = ("board", "task_id", "result", "summary")
|
|
if not all(
|
|
isinstance(record.get(key), str) and record[key]
|
|
for key in required_strings
|
|
):
|
|
return False
|
|
expected_run_id = record.get("expected_run_id")
|
|
if (
|
|
type(expected_run_id) is not int
|
|
or canonical_run_id(expected_run_id) is None
|
|
):
|
|
return False
|
|
if not isinstance(record.get("metadata"), dict):
|
|
return False
|
|
try:
|
|
structured = json.loads(record["result"])
|
|
except (TypeError, json.JSONDecodeError):
|
|
return False
|
|
if not isinstance(structured, dict):
|
|
return False
|
|
required = set(RESULT_SCHEMA["required"])
|
|
if set(structured) != required:
|
|
return False
|
|
if (
|
|
type(structured["status"]) is not str
|
|
or structured["status"] not in cli_lane_goal.RESULT_STATUSES
|
|
or type(structured["summary"]) is not str
|
|
or not structured["summary"].strip()
|
|
):
|
|
return False
|
|
for key in ("changed_files", "tests_run", "artifacts", "findings", "blockers"):
|
|
value = structured[key]
|
|
if type(value) is not list or any(type(item) is not str for item in value):
|
|
return False
|
|
valid = (
|
|
structured["status"] == "completed"
|
|
and structured["blockers"] == []
|
|
and cli_lane_goal.unfinished_result_reason(structured) is None
|
|
and record["summary"] == structured["summary"]
|
|
and record.get("kanban_state") in {"pending", "prepared", "committed"}
|
|
)
|
|
if not valid or identity is None:
|
|
return valid
|
|
return (
|
|
record.get("board") == identity.board
|
|
and record.get("task_id") == identity.task_id
|
|
and record.get("expected_run_id") == identity.run_id
|
|
and record.get("kanban_state") == identity.state
|
|
)
|