Close completed date evidence custody without replaying operations
Some checks failed
Use-case regression gate / usecase-regressions (push) Has been cancelled
Some checks failed
Use-case regression gate / usecase-regressions (push) Has been cancelled
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
This commit is contained in:
parent
fa5177d89e
commit
803203a332
5 changed files with 384 additions and 13 deletions
|
|
@ -248,7 +248,7 @@ class Node:
|
|||
return point
|
||||
|
||||
def prepare_root(self, root):
|
||||
root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
root.mkdir(mode=0o700, exist_ok=True)
|
||||
info = root.lstat()
|
||||
if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.geteuid() or info.st_mode & 0o077
|
||||
or any(p.is_symlink() for p in root.parents)):
|
||||
|
|
@ -285,6 +285,68 @@ class Node:
|
|||
sync_directory(path)
|
||||
sync_directory(path.parent)
|
||||
|
||||
@contextmanager
|
||||
def helper_evidence_files(self, path):
|
||||
"""Open only the closed helper's known evidence; preserve crash recovery."""
|
||||
with ExitStack() as stack:
|
||||
def opened(name, flags, parent=None):
|
||||
fd = os.open(name, flags | os.O_NOFOLLOW, dir_fd=parent)
|
||||
stack.callback(os.close, fd)
|
||||
return fd
|
||||
|
||||
directory = opened("/", os.O_RDONLY | os.O_DIRECTORY)
|
||||
for component in path.parts[1:]:
|
||||
directory = opened(component, os.O_RDONLY | os.O_DIRECTORY, directory)
|
||||
info = os.fstat(directory)
|
||||
if (info.st_uid not in {os.geteuid(), 10001}
|
||||
or info.st_gid not in {os.getegid(), 10001}
|
||||
or stat.S_IMODE(info.st_mode) != 0o700):
|
||||
raise Refused("private_helper_evidence_required")
|
||||
names = sorted(os.listdir(directory))
|
||||
if set(names) - {"execute.lock", "journal.json", "result.json"}:
|
||||
raise Refused("unexpected_helper_evidence")
|
||||
files, evidence = {}, {}
|
||||
for name in names:
|
||||
fd = opened(name, os.O_RDONLY | os.O_NONBLOCK, directory)
|
||||
info = os.fstat(fd)
|
||||
if (not stat.S_ISREG(info.st_mode) or info.st_nlink != 1
|
||||
or info.st_uid not in {os.geteuid(), 10001}
|
||||
or info.st_gid not in {os.getegid(), 10001}
|
||||
or stat.S_IMODE(info.st_mode) != 0o600):
|
||||
raise Refused("private_helper_evidence_required")
|
||||
if name == "execute.lock":
|
||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
digest = hashlib.sha256()
|
||||
while chunk := os.read(fd, 1024 * 1024):
|
||||
digest.update(chunk)
|
||||
after = os.fstat(fd)
|
||||
if any(getattr(info, key) != getattr(after, key) for key in (
|
||||
"st_dev", "st_ino", "st_mode", "st_uid", "st_gid", "st_nlink",
|
||||
"st_size", "st_mtime_ns", "st_ctime_ns")):
|
||||
raise Refused("helper_evidence_changed")
|
||||
files[name] = fd
|
||||
evidence[name] = {"sha256": digest.hexdigest(), "bytes": info.st_size,
|
||||
"device": info.st_dev, "inode": info.st_ino,
|
||||
"mtime_ns": info.st_mtime_ns}
|
||||
yield directory, files, evidence
|
||||
|
||||
def helper_evidence(self, path):
|
||||
with self.helper_evidence_files(path) as (_, _, evidence):
|
||||
return evidence
|
||||
|
||||
def seal_helper_evidence(self, path, expected):
|
||||
# Mixed helper/root custody is allowed only to resume this recorded step.
|
||||
with self.helper_evidence_files(path) as (directory, files, evidence):
|
||||
if evidence != expected:
|
||||
raise Refused("helper_evidence_changed")
|
||||
for fd in [*files.values(), directory]:
|
||||
os.fchown(fd, os.geteuid(), os.getegid())
|
||||
os.fsync(fd)
|
||||
info = os.fstat(fd)
|
||||
if (info.st_uid, info.st_gid) != (os.geteuid(), os.getegid()):
|
||||
raise Refused("helper_evidence_custody_unconfirmed")
|
||||
sync_directory(path.parent)
|
||||
|
||||
def read_state(self, path):
|
||||
return json.loads(private_file(path))
|
||||
|
||||
|
|
@ -492,7 +554,27 @@ class Supervisor:
|
|||
self.node.command(["kubectl", "rollout", "status", f"deployment/{NS}", "-n", NS,
|
||||
"--timeout=180s"])
|
||||
self.deployment(1)
|
||||
self.record("completed", application_resumed=True)
|
||||
return self.complete()
|
||||
|
||||
def complete(self, **changes):
|
||||
"""Seal evidence only after resume, when helper recovery is no longer needed."""
|
||||
self.deployment(1)
|
||||
path = self.directory / "private"
|
||||
for pod in self.node.doc("get", "pods", "-A")["items"]:
|
||||
if pod["metadata"]["name"] == self.helper:
|
||||
raise Refused("helper_still_present_at_closeout")
|
||||
for volume in pod["spec"].get("volumes", []):
|
||||
if "hostPath" in volume:
|
||||
mount = Path(volume["hostPath"]["path"])
|
||||
if mount == path or mount in path.parents or path in mount.parents:
|
||||
raise Refused("helper_evidence_still_mounted")
|
||||
evidence = self.state.get("helper_evidence")
|
||||
if evidence is None:
|
||||
evidence = self.node.helper_evidence(path)
|
||||
self.record("custody_closeout_started", application_resumed=True,
|
||||
helper_evidence=evidence, **changes)
|
||||
self.node.seal_helper_evidence(path, evidence)
|
||||
self.record("completed", helper_evidence_root_custody_verified=True)
|
||||
return self.state
|
||||
|
||||
def execute(self):
|
||||
|
|
@ -559,19 +641,23 @@ class Supervisor:
|
|||
if (self.state.get("schema") != "coulomb.supervised-date-attempt.v1"
|
||||
or self.state["proposal_digest"] != self.capsule.digest):
|
||||
raise Refused("original_attempt_required")
|
||||
if self.state["phase"] == "completed":
|
||||
raise Refused("completed_attempt_must_not_be_replayed")
|
||||
self.identity()
|
||||
dep = self.node.doc("get", "deployment", NS, "-n", NS)
|
||||
if dep["spec"]["replicas"] == 1:
|
||||
# A lost response after scale-up must never undo later writes.
|
||||
if (self.state.get("failed_phase", self.state["phase"]) != "resume_started"
|
||||
phase = (self.state["failed_phase"] if self.state["phase"] == "requires_review"
|
||||
else self.state["phase"])
|
||||
if (phase not in {
|
||||
"resume_started", "custody_closeout_started"}
|
||||
or "expected_final" not in self.state):
|
||||
raise Refused("running_application_requires_owner_review")
|
||||
self.deployment(1)
|
||||
self.node.command(["kubectl", "rollout", "status", f"deployment/{NS}",
|
||||
"-n", NS, "--timeout=180s"])
|
||||
self.record("completed", application_resumed=True,
|
||||
resumed_reply_reconciled=True, post_resume_writes_preserved=True)
|
||||
return self.state
|
||||
return self.complete(resumed_reply_reconciled=True,
|
||||
post_resume_writes_preserved=True)
|
||||
self.deployment(0)
|
||||
writers = self.writers()
|
||||
if not writers:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue