feat(python): durable transaction subsystem and Kiro IDE 1.0 adapter

Land the crash-recoverable managed-file transaction engine (journal,
staging, commit, restart recovery, opaque snapshots, private artifacts,
Windows DACL hardening) and the Kiro IDE 1.0 workspace hook plugin with
legacy migration.

Repair five defects that kept the suite red (426 failures to zero):

- Anchor the transaction state directory at ~/.elydora/transactions
  instead of the shared system temp directory. The recovery journal now
  shares fate with the runtimes it repairs, survives temp cleaners, and
  isolates naturally under per-test fake homes.
- Skip pending-transaction recovery when no state directory exists so
  rejected installs never create runtime directories.
- Create the runtime root's parent before hardening it, matching the Go
  SDK's expand behavior for fresh homes.
- Stop using file timestamps as identity evidence: Windows exposes
  mtime/ctime with delayed cross-handle visibility. Reads now prove
  stability through a verification re-read; snapshots compare device,
  inode, size, and SHA-256.
- Stop comparing synthetic Windows permission bits across stat
  boundaries: path stats derive execute bits from file extensions while
  handle stats do not, which falsely flagged every managed .cmd wrapper.
  Snapshots store raw modes for restoration and compare portable bits.

Reservation cleanup now verifies content before discarding so a
concurrent writer's file at a rollback path is preserved, and the
install-failure tests assert the precise contract: no agent runtime
residue, with the durable state directory allowed to remain.

Gates: pytest 807 passed, mypy clean, pip check clean, wheel builds.
This commit is contained in:
ZacharyZhang-NY committed 2026-07-21 18:53:06 -04:00
1 parent dce7d1288a
commit e37418c489
53 files changed
+11855 -531

No files matched your search

+322
View File
@@ -0,0 +1,322 @@
"""Deterministic recovery of a transaction interrupted by process exit."""
from __future__ import annotations
import os
from typing import Dict, Optional
from ._durable_file import atomic_replace
from ._pinned_directory import PinnedDirectory
from ._private_artifact import QuarantinedFile
from ._transaction_journal import (
FileFingerprint,
JournalRecord,
TransactionJournal,
clear_journal,
inspect_fingerprint,
load_journal,
write_journal,
)
from ._transaction_lock import serialized_transactions, transaction_state_path
def _directory_key(path: str) -> str:
return os.path.normcase(os.path.abspath(path))
def _open_directories(journal: TransactionJournal) -> Dict[str, PinnedDirectory]:
directories: Dict[str, PinnedDirectory] = {}
try:
for record in journal.records:
key = _directory_key(record.directory)
current = directories.get(key)
if current is not None:
if current.snapshot != record.directory_snapshot:
raise OSError(
f"Conflicting directory identities in transaction journal: "
f"{record.directory}"
)
continue
directories[key] = PinnedDirectory.open(
record.directory,
f"{record.label} recovery directory",
record.directory_snapshot,
)
return directories
except Exception:
_close_directories(directories)
raise
def _close_directories(directories: Dict[str, PinnedDirectory]) -> None:
failures = []
for directory in reversed(list(directories.values())):
try:
directory.close()
except OSError as error:
failures.append(str(error))
if failures:
raise OSError("; ".join(failures))
def _directory_for(
record: JournalRecord,
directories: Dict[str, PinnedDirectory],
) -> PinnedDirectory:
directory = directories.get(_directory_key(record.directory))
if directory is None:
raise OSError(f"Missing recovery directory for {record.label}")
return directory
def _remove_owned_file(
directory: PinnedDirectory,
path: Optional[str],
expected_identity: Optional[FileFingerprint],
label: str,
) -> None:
if path is None or expected_identity is None:
return
current = inspect_fingerprint(directory, path, label)
if current is None:
return
if (
current.device != expected_identity.device
or current.inode != expected_identity.inode
):
raise OSError(f"{label} changed before recovery cleanup: {path}")
captured = QuarantinedFile.capture(
directory,
path,
label,
require_parent_path=False,
)
captured_current = inspect_fingerprint(
captured.container,
captured.container.path_for("entry"),
label,
)
if (
captured_current is None
or captured_current.device != expected_identity.device
or captured_current.inode != expected_identity.inode
):
preserved = captured.preserve()
raise OSError(
f"{label} changed at recovery cleanup boundary; preserved at {preserved}"
)
captured.discard()
directory.sync()
def _cleanup_record(
record: JournalRecord,
directories: Dict[str, PinnedDirectory],
) -> None:
directory = _directory_for(record, directories)
_remove_owned_file(
directory,
record.temporary,
record.temporary_identity,
f"staged {record.label}",
)
_remove_owned_file(
directory,
record.rollback,
record.rollback_identity,
f"{record.label} rollback",
)
def _restore_original(
record: JournalRecord,
directory: PinnedDirectory,
) -> None:
original = record.original
rollback_path = record.rollback
rollback_identity = record.rollback_identity
if original is None or rollback_path is None or rollback_identity is None:
raise OSError(f"Missing rollback data for {record.label}")
rollback = inspect_fingerprint(
directory,
rollback_path,
f"{record.label} rollback",
)
if (
rollback is None
or rollback.device != rollback_identity.device
or rollback.inode != rollback_identity.inode
or not original.same_contents(rollback)
):
raise OSError(
f"{record.label} rollback changed during restart recovery: "
f"{rollback_path}"
)
if rollback.mode != original.mode:
directory.chmod_file(
directory.name_for(rollback_path),
original.mode,
(rollback.device, rollback.inode),
)
atomic_replace(directory, rollback_path, record.target)
restored = inspect_fingerprint(directory, record.target, record.label)
if not original.same_visible(restored):
raise OSError(
f"{record.label} restoration failed during restart recovery: "
f"{record.target}"
)
def _matches_original_state(
record: JournalRecord,
current: Optional[FileFingerprint],
) -> bool:
original = record.original
if original is None or current is None or not original.same_visible(current):
return False
if original.same_exact(current):
return True
rollback = record.rollback_identity
return (
rollback is not None
and current.device == rollback.device
and current.inode == rollback.inode
)
def _remove_created_target(
record: JournalRecord,
directory: PinnedDirectory,
current: FileFingerprint,
) -> None:
expected = record.next
if expected is None or not expected.same_exact(current):
raise OSError(
f"{record.label} changed before creation recovery: {record.target}"
)
captured = QuarantinedFile.capture(directory, record.target, record.label)
captured_current = inspect_fingerprint(
captured.container,
captured.container.path_for("entry"),
record.label,
)
if not expected.same_exact(captured_current):
try:
captured.restore(record.target)
except Exception as error:
preserved = captured.preserve() if captured.active else record.target
raise OSError(
f"{record.label} changed at creation recovery boundary; "
f"preserved at {preserved}: {error}"
) from error
raise OSError(
f"{record.label} changed at creation recovery boundary: {record.target}"
)
captured.discard()
directory.sync()
def _recover_record(
record: JournalRecord,
directories: Dict[str, PinnedDirectory],
) -> None:
directory = _directory_for(record, directories)
current = inspect_fingerprint(directory, record.target, record.label)
original = record.original
next_value = record.next
if original is None:
if current is None:
return
_remove_created_target(record, directory, current)
return
if _matches_original_state(record, current):
return
if next_value is None:
if current is not None:
raise OSError(
f"{record.label} changed after transactional removal: {record.target}"
)
_restore_original(record, directory)
return
if current is None or not next_value.same_exact(current):
raise OSError(
f"{record.label} changed after atomic replacement; original content "
f"preserved at {record.rollback}: {record.target}"
)
_restore_original(record, directory)
def _verify_committed_record(
record: JournalRecord,
directories: Dict[str, PinnedDirectory],
) -> None:
directory = _directory_for(record, directories)
current = inspect_fingerprint(directory, record.target, record.label)
if record.next is None:
if current is not None:
raise OSError(
f"{record.label} changed after committed removal: {record.target}"
)
return
if not record.next.same_exact(current):
raise OSError(
f"{record.label} changed after committed transaction: {record.target}"
)
def recover_pending_locked() -> None:
"""Recover the active journal while the caller owns the transaction lock."""
journal = load_journal()
if journal is None:
return
directories = _open_directories(journal)
try:
if journal.state == "preparing":
for record in journal.records:
current = inspect_fingerprint(
_directory_for(record, directories),
record.target,
record.label,
)
if record.original is None:
if current is not None:
raise OSError(
f"{record.label} appeared during transaction preparation: "
f"{record.target}"
)
elif not record.original.same_exact(current):
raise OSError(
f"{record.label} changed during transaction preparation: "
f"{record.target}"
)
elif journal.state in {"committing", "recovering"}:
if journal.state != "recovering":
journal = journal.with_state("recovering")
write_journal(journal)
for record in reversed(journal.records):
_recover_record(record, directories)
elif journal.state == "committed":
for record in journal.records:
_verify_committed_record(record, directories)
else:
raise ValueError(f"Unsupported transaction state: {journal.state}")
for record in journal.records:
_cleanup_record(record, directories)
clear_journal()
finally:
_close_directories(directories)
def recover_pending_transactions() -> None:
"""Serialize and recover any transaction left by a terminated process.
A missing state directory proves no journal exists, so this returns without
creating runtime directories for commands that may still reject their input.
"""
if not os.path.isdir(transaction_state_path()):
return
with serialized_transactions():
recover_pending_locked()
__all__ = ["recover_pending_locked", "recover_pending_transactions"]