231 lines
11 KiB
Python
231 lines
11 KiB
Python
# #region Tooling.LabReset.Archive [C:4] [TYPE Module] [SEMANTICS maintenance,archive,private,typed]
|
|
# @BRIEF Private reversible selected-state archives and safe phase receipts for the two reviewed laboratories.
|
|
# @INVARIANT Protected row bodies never enter an archive; unsupported values refuse rather than stringify.
|
|
# @RATIONALE Separate reviewed phase digests prevent inventory from authorizing deletion or automatic recovery.
|
|
# @REJECTED Pickle, whole database dumps, public exception strings and implicit archive overwrite.
|
|
import base64
|
|
from datetime import date, datetime, time, timedelta
|
|
from decimal import Decimal
|
|
from hashlib import sha256
|
|
import json
|
|
import math
|
|
import os
|
|
from pathlib import Path
|
|
import stat
|
|
import traceback
|
|
from uuid import UUID, uuid4
|
|
|
|
|
|
# #region Tooling.LabReset.Archive.Stop [C:1] [TYPE Class]
|
|
class Stop(RuntimeError):
|
|
"""Fixed public cause; the original exception is retained only privately."""
|
|
# #endregion Tooling.LabReset.Archive.Stop
|
|
|
|
|
|
# #region Tooling.LabReset.Archive.Require [C:1] [TYPE Function]
|
|
def require(condition, code):
|
|
if not condition:
|
|
raise Stop(code)
|
|
# #endregion Tooling.LabReset.Archive.Require
|
|
|
|
|
|
# #region Tooling.LabReset.Archive.Json [C:1] [TYPE Function]
|
|
def json_bytes(value):
|
|
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False,
|
|
allow_nan=False).encode("utf-8")
|
|
# #endregion Tooling.LabReset.Archive.Json
|
|
|
|
|
|
# #region Tooling.LabReset.Archive.Digest [C:1] [TYPE Function]
|
|
def digest(value):
|
|
return sha256(json_bytes(value)).hexdigest()
|
|
# #endregion Tooling.LabReset.Archive.Digest
|
|
|
|
|
|
# #region Tooling.LabReset.Archive.EncodeScalar [C:2] [TYPE Function]
|
|
def scalar(value):
|
|
converters = {bytes: lambda v: base64.b64encode(v).decode("ascii"),
|
|
UUID: str, Decimal: str, datetime: lambda v: v.isoformat(),
|
|
date: lambda v: v.isoformat(), time: lambda v: v.isoformat(),
|
|
timedelta: lambda v: [v.days, v.seconds, v.microseconds]}
|
|
converter = converters.get(type(value))
|
|
require(converter is not None, "LAB_RESET_CODEC_UNSUPPORTED")
|
|
if isinstance(value, Decimal):
|
|
require(value.is_finite(), "LAB_RESET_CODEC_UNSUPPORTED")
|
|
return [type(value).__name__, converter(value)]
|
|
# #endregion Tooling.LabReset.Archive.EncodeScalar
|
|
|
|
|
|
# #region Tooling.LabReset.Archive.Encode [C:3] [TYPE Function]
|
|
# @POST Every supported SQL value has a reversible tagged representation; dict keys remain exact strings.
|
|
def encode(value):
|
|
if value is None or type(value) in (str, bool, int):
|
|
return [type(value).__name__, value]
|
|
if type(value) is float:
|
|
require(math.isfinite(value), "LAB_RESET_CODEC_UNSUPPORTED")
|
|
return ["float", value.hex()]
|
|
if type(value) in (list, tuple):
|
|
return [type(value).__name__, [encode(item) for item in value]]
|
|
if type(value) is dict:
|
|
require(all(type(key) is str for key in value), "LAB_RESET_CODEC_UNSUPPORTED")
|
|
return ["dict", [[key, encode(value[key])] for key in sorted(value)]]
|
|
return scalar(value)
|
|
# #endregion Tooling.LabReset.Archive.Encode
|
|
|
|
|
|
# #region Tooling.LabReset.Archive.Decode [C:3] [TYPE Function]
|
|
def decode(value):
|
|
require(isinstance(value, list) and len(value) == 2, "LAB_RESET_ARCHIVE_INVALID")
|
|
tag, body = value
|
|
converters = {"NoneType": lambda v: None, "str": str, "bool": bool, "int": int,
|
|
"float": float.fromhex, "bytes": lambda v: base64.b64decode(v, validate=True),
|
|
"UUID": UUID, "Decimal": Decimal, "datetime": datetime.fromisoformat,
|
|
"date": date.fromisoformat, "time": time.fromisoformat,
|
|
"timedelta": lambda v: timedelta(days=v[0], seconds=v[1], microseconds=v[2]),
|
|
"list": lambda v: [decode(item) for item in v],
|
|
"tuple": lambda v: tuple(decode(item) for item in v),
|
|
"dict": lambda v: {key: decode(item) for key, item in v}}
|
|
require(tag in converters, "LAB_RESET_ARCHIVE_INVALID")
|
|
result = converters[tag](body)
|
|
require(encode(result) == value, "LAB_RESET_ARCHIVE_INVALID")
|
|
return result
|
|
# #endregion Tooling.LabReset.Archive.Decode
|
|
|
|
|
|
# #region Tooling.LabReset.Archive.SafePath [C:3] [TYPE Function]
|
|
# @POST Every existing ancestor is a real directory; no symlink or lexical traversal is adopted.
|
|
def safe_path(root, relative):
|
|
root, relative = Path(root), Path(relative)
|
|
require(root.is_absolute() and not relative.is_absolute() and relative.parts
|
|
and all(part not in (".", "..") for part in relative.parts), "LAB_RESET_PATH_INVALID")
|
|
for current in [root, *root.parents]:
|
|
require(not current.is_symlink(), "LAB_RESET_SYMLINK_REFUSED")
|
|
current = root
|
|
for part in relative.parts:
|
|
current = current / part
|
|
require(not current.is_symlink(), "LAB_RESET_SYMLINK_REFUSED")
|
|
return current
|
|
# #endregion Tooling.LabReset.Archive.SafePath
|
|
|
|
|
|
# #region Tooling.LabReset.Archive.Read [C:3] [TYPE Function]
|
|
def read_bytes(path, private=False, owner_uid=None):
|
|
path = safe_path(Path(path).parent, Path(path).name)
|
|
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
|
|
with os.fdopen(fd, "rb") as stream:
|
|
info = os.fstat(stream.fileno())
|
|
require(stat.S_ISREG(info.st_mode), "LAB_RESET_FILE_INVALID")
|
|
if private:
|
|
expected = os.geteuid() if owner_uid is None else owner_uid
|
|
require(stat.S_IMODE(info.st_mode) == 0o600 and info.st_uid == expected,
|
|
"LAB_RESET_PRIVATE_MODE_INVALID")
|
|
return stream.read()
|
|
# #endregion Tooling.LabReset.Archive.Read
|
|
|
|
|
|
# #region Tooling.LabReset.Archive.ReadJson [C:2] [TYPE Function]
|
|
def read_json(path, expected_sha, private=False, owner_uid=None):
|
|
raw = read_bytes(path, private, owner_uid)
|
|
require(len(expected_sha) == 64 and sha256(raw).hexdigest() == expected_sha,
|
|
"LAB_RESET_DIGEST_CONFLICT")
|
|
return json.loads(raw)
|
|
# #endregion Tooling.LabReset.Archive.ReadJson
|
|
|
|
|
|
# #region Tooling.LabReset.Archive.PrivateDirectory [C:2] [TYPE Function]
|
|
def private_directory(path, owner_uid=None):
|
|
path = Path(path)
|
|
require(path.is_absolute() and path.is_dir() and not path.is_symlink(),
|
|
"LAB_RESET_PRIVATE_MODE_INVALID")
|
|
require(stat.S_IMODE(path.stat().st_mode) == 0o700, "LAB_RESET_PRIVATE_MODE_INVALID")
|
|
if owner_uid is not None:
|
|
require(path.stat().st_uid == owner_uid, "LAB_RESET_PRIVATE_MODE_INVALID")
|
|
for ancestor in path.parents:
|
|
require(not ancestor.is_symlink(), "LAB_RESET_SYMLINK_REFUSED")
|
|
return path
|
|
# #endregion Tooling.LabReset.Archive.PrivateDirectory
|
|
|
|
|
|
# #region Tooling.LabReset.Archive.Sync [C:1] [TYPE Function]
|
|
def sync_directory(path):
|
|
fd = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
try:
|
|
os.fsync(fd)
|
|
finally:
|
|
os.close(fd)
|
|
# #endregion Tooling.LabReset.Archive.Sync
|
|
|
|
|
|
# #region Tooling.LabReset.Archive.Write [C:2] [TYPE Function]
|
|
# @POST Exclusive private durable member is reread byte-for-byte before its digest is returned.
|
|
def write_bytes(root, name, raw):
|
|
root = private_directory(root)
|
|
target = safe_path(root, name)
|
|
fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
|
with os.fdopen(fd, "wb") as stream:
|
|
stream.write(raw)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
sync_directory(root)
|
|
require(read_bytes(target, private=True) == raw, "LAB_RESET_ARCHIVE_READBACK_FAILED")
|
|
return sha256(raw).hexdigest()
|
|
# #endregion Tooling.LabReset.Archive.Write
|
|
|
|
|
|
# #region Tooling.LabReset.Archive.Journal [C:4] [TYPE Class]
|
|
# @RELATION DEPENDS_ON -> [Std.Semantics.MolecularCoTLogging]
|
|
# @INVARIANT Public events contain only fixed causes, phase/count/hash facts; original causes stay in a separate 0600 file.
|
|
class Journal:
|
|
# #region Tooling.LabReset.Archive.Journal.Init [C:1] [TYPE Function]
|
|
def __init__(self, root, phase):
|
|
self.root, self.phase, self.trace = private_directory(root), phase, uuid4().hex
|
|
self.database_commit_status = "not_requested"
|
|
self.name = f"journal-{self.trace}.jsonl"
|
|
self.fd = os.open(self.root / self.name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
|
# #endregion Tooling.LabReset.Archive.Journal.Init
|
|
|
|
# #region Tooling.LabReset.Archive.Journal.Event [C:2] [TYPE Function]
|
|
def event(self, marker, intent, *, code=None, loc=None, **facts):
|
|
from src.core.cot_logger import build_cot_event
|
|
record = build_cot_event(src="Tooling.LabReset", marker=marker, intent=intent,
|
|
trace_id=self.trace, claim="PRE: exact reviewed laboratory state",
|
|
error_code=code, loc=loc, payload={"phase": self.phase, **facts})
|
|
raw = json_bytes(record) + b"\n"
|
|
require(os.write(self.fd, raw) == len(raw), "LAB_RESET_JOURNAL_WRITE_FAILED")
|
|
os.fsync(self.fd)
|
|
print(raw.decode("utf-8"), end="", flush=True)
|
|
# #endregion Tooling.LabReset.Archive.Journal.Event
|
|
|
|
# #region Tooling.LabReset.Archive.Journal.Failure [C:2] [TYPE Function]
|
|
def failure(self, exc):
|
|
cause = exc.__cause__
|
|
details = f"database_commit_status={self.database_commit_status}\n" + "".join(traceback.format_exception(exc))
|
|
if cause is not None and hasattr(cause, "stderr"):
|
|
details += f"\nstdout:\n{cause.stdout}\nstderr:\n{cause.stderr}"
|
|
write_bytes(self.root, f"cause-{self.trace}.txt",
|
|
details.encode("utf-8"))
|
|
code = str(exc) if isinstance(exc, Stop) else "LAB_RESET_PRIVATE_CAUSE"
|
|
frames = traceback.extract_tb(exc.__traceback__)
|
|
loc = f"{Path(frames[-1].filename).name}:{frames[-1].lineno}" if frames else None
|
|
try:
|
|
self.event("EXPLORE", "Laboratory phase stopped; reviewed recovery required",
|
|
code=code, loc=loc, failure_type=type(exc).__name__,
|
|
database_commit_status=self.database_commit_status)
|
|
except (OSError, Stop) as journal_error:
|
|
if isinstance(journal_error, Stop) and str(journal_error) != "LAB_RESET_JOURNAL_WRITE_FAILED":
|
|
raise
|
|
from src.core.cot_logger import build_cot_event
|
|
print(json_bytes(build_cot_event(src="Tooling.LabReset", marker="EXPLORE",
|
|
intent="Private cause retained; phase journal unavailable", trace_id=self.trace,
|
|
error_code="LAB_RESET_JOURNAL_WRITE_FAILED", payload={"phase": self.phase,
|
|
"database_commit_status": self.database_commit_status})).decode(), flush=True)
|
|
# #endregion Tooling.LabReset.Archive.Journal.Failure
|
|
|
|
# #region Tooling.LabReset.Archive.Journal.Close [C:1] [TYPE Function]
|
|
def close(self):
|
|
os.close(self.fd)
|
|
sync_directory(self.root)
|
|
# #endregion Tooling.LabReset.Archive.Journal.Close
|
|
# #endregion Tooling.LabReset.Archive.Journal
|
|
# #endregion Tooling.LabReset.Archive
|