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

+380 -296
View File
@@ -2,65 +2,67 @@
from __future__ import annotations
from dataclasses import dataclass
import os
import tempfile
from typing import List, Optional, Sequence, cast
from uuid import uuid4
from ._managed_files import (
FileSnapshot,
MAX_SOURCE_BYTES,
ensure_physical_directory,
physical_file_exists,
read_physical_file,
)
@dataclass(frozen=True)
class FileChange:
file_path: str
label: str
original: Optional[str]
next_source: Optional[str]
mode: int
original_mode: Optional[int]
original_device: Optional[int]
original_inode: Optional[int]
maximum_bytes: int
@dataclass(frozen=True)
class FilePrecondition:
file_path: str
label: str
original: Optional[FileSnapshot]
maximum_bytes: int = MAX_SOURCE_BYTES
@dataclass
class _StagedChange:
change: FileChange
temporary_path: Optional[str]
rollback_path: Optional[str]
committed_snapshot: Optional[FileSnapshot] = None
committed: bool = False
from ._opaque_cleanup import cleanup_opaque_removal
from ._opaque_removal import (
OpaqueRemovalChange,
StagedOpaqueRemoval,
assert_staged_opaque,
commit_opaque_removal,
prepare_opaque_rollback,
rollback_opaque_removal,
stage_opaque_removal,
)
from ._pinned_directory import PinnedDirectory
from ._transaction_commit import commit_change, prepare_change, rollback_change
from ._transaction_journal import (
FileFingerprint,
JournalRecord,
TransactionJournal,
active_journal_path,
clear_journal,
write_journal,
)
from ._transaction_lock import serialized_transactions
from ._transaction_restart import (
recover_pending_locked,
recover_pending_transactions as _recover_pending_transactions,
)
from ._transaction_staging import (
assert_transaction_preconditions,
cleanup_created_directories,
cleanup_staged,
close_directories,
directory_for,
pin_directories,
reserve_change,
same_snapshot,
stage_change,
)
from ._transaction_types import (
DirectoryMap,
DirectoryPrecondition,
FileChange,
FilePrecondition,
StagedChange,
)
_EXPECTED_SNAPSHOT_UNSET = object()
def _same_snapshot(
current: Optional[FileSnapshot],
expected: Optional[FileSnapshot],
) -> bool:
if current is None or expected is None:
return current is expected
return (
current.contents == expected.contents
and current.device == expected.device
and current.inode == expected.inode
)
def recover_pending_transactions() -> None:
"""Recover a journal left by a terminated transaction process."""
_recover_pending_transactions()
def read_optional(
@@ -80,24 +82,22 @@ def source_change(
mode: int,
maximum_bytes: int = MAX_SOURCE_BYTES,
expected_snapshot: object = _EXPECTED_SNAPSHOT_UNSET,
enforce_mode: bool = False,
) -> Optional[FileChange]:
if original == next_source:
return None
if (
next_source is not None
and len(next_source.encode("utf-8")) > maximum_bytes
):
raise ValueError(
f"{label} exceeds {maximum_bytes} bytes: {file_path}"
)
if next_source is not None and len(next_source.encode("utf-8")) > maximum_bytes:
raise ValueError(f"{label} exceeds {maximum_bytes} bytes: {file_path}")
snapshot = read_physical_file(file_path, label, maximum_bytes)
current = None if snapshot is None else snapshot.contents
if current != original:
raise OSError(f"{label} changed before staging: {file_path}")
if expected_snapshot is not _EXPECTED_SNAPSHOT_UNSET:
expected = cast(Optional[FileSnapshot], expected_snapshot)
if not _same_snapshot(snapshot, expected):
if not same_snapshot(snapshot, expected):
raise OSError(f"{label} changed before staging: {file_path}")
if original == next_source and (
not enforce_mode or snapshot is None or snapshot.mode == mode
):
return None
return FileChange(
file_path,
label,
@@ -117,6 +117,7 @@ def file_change(
next_source: Optional[str],
mode: int,
maximum_bytes: int = MAX_SOURCE_BYTES,
enforce_mode: bool = False,
) -> Optional[FileChange]:
snapshot = read_physical_file(file_path, label, maximum_bytes)
return source_change(
@@ -126,275 +127,358 @@ def file_change(
next_source,
mode,
maximum_bytes,
enforce_mode=enforce_mode,
)
def _remove_optional(file_path: Optional[str]) -> None:
if not file_path:
return
try:
os.remove(file_path)
except FileNotFoundError:
return
def _replace_physical(
source_path: str,
destination_path: str,
directory: PinnedDirectory,
) -> None:
del source_path, destination_path, directory
def _cleanup_paths(paths: Sequence[Optional[str]]) -> List[str]:
def _restore_physical(
source_path: str,
destination_path: str,
directory: PinnedDirectory,
) -> None:
del source_path, destination_path, directory
def _capture_physical(
source_path: str,
destination_path: str,
directory: PinnedDirectory,
) -> None:
del source_path, destination_path, directory
def _after_replace_physical(
source_path: str,
destination_path: str,
directory: PinnedDirectory,
) -> None:
del source_path, destination_path, directory
def _after_journal_commit() -> None:
"""Fault-injection boundary after the committed marker is durable."""
def _commit(staged: StagedChange) -> None:
commit_change(
staged,
_replace_physical,
_capture_physical,
_after_replace_physical,
)
def _rollback(staged: StagedChange) -> None:
rollback_change(staged, _restore_physical, _capture_physical)
def _recover(
staged: Sequence[StagedChange],
opaque_staged: Sequence[StagedOpaqueRemoval],
directories: DirectoryMap,
directory_preconditions: Sequence[DirectoryPrecondition],
) -> List[str]:
errors = []
for path in paths:
for staged_item in reversed(staged):
try:
_remove_optional(path)
_rollback(staged_item)
except Exception as error:
errors.append(str(error))
for opaque_item in reversed(opaque_staged):
try:
rollback_opaque_removal(opaque_item)
except Exception as error:
errors.append(str(error))
for staged_item in staged:
try:
cleanup_staged(staged_item)
except OSError as error:
errors.append(str(error))
for opaque_item in opaque_staged:
try:
cleanup_opaque_removal(opaque_item)
except OSError as error:
errors.append(str(error))
errors.extend(
cleanup_created_directories(directory_preconditions, directories)
)
errors.extend(close_directories(directories))
return errors
def _write_staged(
directory: str,
basename: str,
suffix: str,
content: str,
mode: int,
def _finish_committed(
staged: Sequence[StagedChange],
opaque_staged: Sequence[StagedOpaqueRemoval],
directories: DirectoryMap,
) -> List[str]:
errors = []
for staged_item in staged:
try:
cleanup_staged(staged_item)
except OSError as error:
errors.append(str(error))
for opaque_item in opaque_staged:
try:
cleanup_opaque_removal(opaque_item)
except OSError as error:
errors.append(str(error))
errors.extend(close_directories(directories))
if errors:
return errors
try:
clear_journal()
except OSError as error:
errors.append(str(error))
return errors
def _text_journal_record(staged: StagedChange) -> JournalRecord:
original = None
if staged.change.original is not None:
if (
staged.change.original_device is None
or staged.change.original_inode is None
or staged.change.original_mode is None
):
raise OSError(f"Missing original identity for {staged.change.label}")
original = FileFingerprint.from_text(
FileSnapshot(
staged.change.original,
staged.change.original_device,
staged.change.original_inode,
staged.change.original_mode,
)
)
next_value = (
None
if staged.temporary_snapshot is None
else FileFingerprint.from_text(staged.temporary_snapshot)
)
temporary_identity = (
None
if staged.temporary_reservation is None
else FileFingerprint.from_text(staged.temporary_reservation)
)
rollback = (
None
if staged.rollback_reservation is None
else FileFingerprint.from_text(staged.rollback_reservation)
)
return JournalRecord(
"text",
staged.change.label,
os.path.abspath(staged.change.file_path),
None if staged.temporary_path is None else os.path.abspath(staged.temporary_path),
None if staged.rollback_path is None else os.path.abspath(staged.rollback_path),
staged.directory.path,
staged.directory.snapshot,
original,
next_value,
temporary_identity,
rollback,
)
def _opaque_journal_record(staged: StagedOpaqueRemoval) -> JournalRecord:
original = (
None
if staged.change.original is None
else FileFingerprint.from_opaque(staged.change.original)
)
rollback = (
None
if staged.rollback_reservation is None
else FileFingerprint.from_text(staged.rollback_reservation)
)
return JournalRecord(
"opaque",
staged.change.label,
os.path.abspath(staged.change.file_path),
None,
None if staged.rollback_path is None else os.path.abspath(staged.rollback_path),
staged.directory.path,
staged.directory.snapshot,
original,
None,
None,
rollback,
)
def _write_changes_locked(
changes: Sequence[FileChange],
label: str,
) -> str:
descriptor = -1
temporary_path = ""
try:
descriptor, temporary_path = tempfile.mkstemp(
prefix=f".{basename}.",
suffix=suffix,
dir=directory,
text=True,
)
with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as file:
descriptor = -1
file.write(content)
file.flush()
os.fsync(file.fileno())
os.chmod(temporary_path, mode)
return temporary_path
except Exception as error:
failures = []
if descriptor >= 0:
try:
os.close(descriptor)
except OSError as close_error:
failures.append(f"close failed: {close_error}")
failures.extend(_cleanup_paths((temporary_path,)))
suffix = f"; cleanup failed: {'; '.join(failures)}" if failures else ""
raise OSError(f"Stage {label}: {error}{suffix}") from error
def _assert_unchanged(change: FileChange) -> None:
snapshot = read_physical_file(
change.file_path,
change.label,
change.maximum_bytes,
)
current = None if snapshot is None else snapshot.contents
identity_changed = snapshot is not None and (
snapshot.device != change.original_device
or snapshot.inode != change.original_inode
)
if current != change.original or identity_changed:
raise OSError(f"{change.label} changed during installation: {change.file_path}")
def _assert_preconditions(
preconditions: Sequence[FilePrecondition],
operation: str,
preconditions: Sequence[FilePrecondition] = (),
directory_preconditions: Sequence[DirectoryPrecondition] = (),
opaque_removals: Sequence[OpaqueRemovalChange] = (),
) -> None:
for condition in preconditions:
current = read_physical_file(
condition.file_path,
condition.label,
condition.maximum_bytes,
)
if not _same_snapshot(current, condition.original):
raise OSError(
f"{condition.label} changed during {operation}: "
f"{condition.file_path}"
)
def _stage(change: FileChange) -> _StagedChange:
_assert_unchanged(change)
directory = os.path.dirname(change.file_path)
ensure_physical_directory(directory, f"{change.label} directory")
basename = os.path.basename(change.file_path)
temporary_path = None
rollback_path = None
filtered = [
change
for change in changes
if change.original != change.next_source
or change.original_mode != change.mode
]
directories: DirectoryMap = {}
staged: List[StagedChange] = []
opaque_staged: List[StagedOpaqueRemoval] = []
journal: Optional[TransactionJournal] = None
committed_marker_durable = False
try:
if change.next_source is not None:
temporary_path = _write_staged(
directory,
basename,
".tmp",
change.next_source,
change.mode,
change.label,
)
if change.original is not None and change.next_source is not None:
rollback_path = _write_staged(
directory,
basename,
".rollback",
change.original,
change.original_mode or change.mode,
f"{change.label} rollback",
)
elif change.original is not None:
rollback_path = os.path.join(
directory,
f".{basename}.{uuid4().hex}.rollback",
)
if os.path.lexists(rollback_path):
raise FileExistsError(f"Rollback path already exists: {rollback_path}")
return _StagedChange(change, temporary_path, rollback_path)
except Exception as error:
failures = _cleanup_paths((temporary_path, rollback_path))
suffix = f"; cleanup failed: {'; '.join(failures)}" if failures else ""
raise OSError(f"Stage {change.label}: {error}{suffix}") from error
def _commit(staged: _StagedChange) -> None:
_assert_unchanged(staged.change)
if staged.change.next_source is None:
if staged.rollback_path is None:
raise OSError(f"Missing rollback path for {staged.change.label}")
os.replace(staged.change.file_path, staged.rollback_path)
else:
if staged.temporary_path is None:
raise OSError(f"Missing staged file for {staged.change.label}")
os.replace(staged.temporary_path, staged.change.file_path)
staged.committed = True
current = read_physical_file(
staged.change.file_path,
staged.change.label,
staged.change.maximum_bytes,
)
if staged.change.next_source is None:
if current is not None:
raise OSError(
f"{staged.change.label} remained after removal: "
f"{staged.change.file_path}"
)
return
if current is None or current.contents != staged.change.next_source:
raise OSError(
f"{staged.change.label} changed immediately after commit: "
f"{staged.change.file_path}"
assert_transaction_preconditions(preconditions, directory_preconditions, label)
if not filtered and not opaque_removals:
return
paths = [
os.path.normcase(os.path.abspath(change.file_path)) for change in filtered
]
paths.extend(
os.path.normcase(os.path.abspath(change.file_path))
for change in opaque_removals
)
staged.committed_snapshot = current
def _assert_committed_unchanged(staged: _StagedChange) -> None:
current = read_physical_file(
staged.change.file_path,
staged.change.label,
staged.change.maximum_bytes,
)
if staged.change.next_source is None:
if current is not None:
raise OSError(
f"{staged.change.label} changed during transaction recovery: "
f"{staged.change.file_path}"
)
return
committed = staged.committed_snapshot
if current is None or committed is None or (
current.contents != committed.contents
or current.device != committed.device
or current.inode != committed.inode
):
raise OSError(
f"{staged.change.label} changed during transaction recovery: "
f"{staged.change.file_path}"
)
def _preserve_rollback(staged: _StagedChange, error: Exception) -> OSError:
if staged.rollback_path is None:
return error if isinstance(error, OSError) else OSError(str(error))
rollback_path = staged.rollback_path
staged.rollback_path = None
return OSError(
f"{error}; original content preserved at {rollback_path}"
)
def _rollback(staged: _StagedChange) -> None:
if not staged.committed:
return
try:
_assert_committed_unchanged(staged)
except Exception as error:
raise _preserve_rollback(staged, error) from error
try:
if staged.change.next_source is None or staged.change.original is not None:
if staged.rollback_path is None:
raise OSError(
f"Missing rollback data for {staged.change.label}"
if len(paths) != len(set(paths)):
raise ValueError(f"{label} contains duplicate file targets")
directories = pin_directories(
filtered,
preconditions,
directory_preconditions,
[
(
os.path.dirname(change.file_path),
f"{change.label} directory",
True,
)
os.replace(staged.rollback_path, staged.change.file_path)
staged.rollback_path = None
else:
_remove_optional(staged.change.file_path)
for change in opaque_removals
],
)
for opaque_change in opaque_removals:
opaque_staged.append(
stage_opaque_removal(
opaque_change,
directory_for(opaque_change.file_path, directories),
label,
)
)
for file_change_item in filtered:
staged.append(
reserve_change(
file_change_item,
preconditions,
directory_preconditions,
label,
directories,
)
)
records = [
*(_opaque_journal_record(item) for item in opaque_staged),
*(_text_journal_record(item) for item in staged),
]
journal = TransactionJournal(uuid4().hex, label, "preparing", records)
write_journal(journal)
for staged_item in staged:
stage_change(staged_item)
records = [
*(_opaque_journal_record(item) for item in opaque_staged),
*(_text_journal_record(item) for item in staged),
]
journal = TransactionJournal(
journal.transaction_id,
journal.label,
journal.state,
records,
)
write_journal(journal)
assert_transaction_preconditions(
preconditions, directory_preconditions, label, directories
)
for opaque_item in opaque_staged:
assert_staged_opaque(opaque_item, label)
for opaque_item in opaque_staged:
prepare_opaque_rollback(opaque_item)
for staged_item in staged:
prepare_change(staged_item, _capture_physical)
journal = journal.with_state("committing")
write_journal(journal)
for opaque_item in opaque_staged:
commit_opaque_removal(opaque_item)
for staged_item in staged:
_commit(staged_item)
assert_transaction_preconditions(
preconditions, directory_preconditions, label, directories
)
for opaque_item in opaque_staged:
assert_staged_opaque(opaque_item, label)
committed_journal = journal.with_state("committed")
write_journal(committed_journal)
journal = committed_journal
committed_marker_durable = True
_after_journal_commit()
except Exception as error:
raise _preserve_rollback(staged, error) from error
if committed_marker_durable:
cleanup_errors = _finish_committed(
staged,
opaque_staged,
directories,
)
suffix = (
f"; cleanup pending in {active_journal_path()}: "
f"{'; '.join(cleanup_errors)}"
if cleanup_errors
else ""
)
raise OSError(
f"{label} committed before post-commit failure: {error}{suffix}"
) from error
recovery_errors = _recover(
staged,
opaque_staged,
directories,
directory_preconditions,
)
suffix = (
f"; recovery failed: {'; '.join(recovery_errors)}"
if recovery_errors
else ""
)
if journal is not None and not recovery_errors:
try:
clear_journal()
except OSError as journal_error:
suffix += f"; journal cleanup failed: {journal_error}"
elif journal is not None:
suffix += f"; recovery journal preserved at {active_journal_path()}"
raise OSError(f"{label}: {error}{suffix}") from error
def _cleanup(staged: _StagedChange) -> None:
_remove_optional(staged.temporary_path)
_remove_optional(staged.rollback_path)
cleanup_errors = _finish_committed(staged, opaque_staged, directories)
if cleanup_errors:
raise OSError(
f"{label} committed; cleanup pending in {active_journal_path()}: "
f"{'; '.join(cleanup_errors)}"
)
def write_changes(
changes: Sequence[FileChange],
label: str,
preconditions: Sequence[FilePrecondition] = (),
directory_preconditions: Sequence[DirectoryPrecondition] = (),
opaque_removals: Sequence[OpaqueRemovalChange] = (),
) -> None:
filtered = [change for change in changes if change.original != change.next_source]
if not filtered:
return
paths = [os.path.normcase(os.path.abspath(change.file_path)) for change in filtered]
if len(paths) != len(set(paths)):
raise ValueError(f"{label} contains duplicate file targets")
staged: List[_StagedChange] = []
try:
_assert_preconditions(preconditions, label)
for change in filtered:
staged.append(_stage(change))
_assert_preconditions(preconditions, label)
for item in staged:
_assert_preconditions(preconditions, label)
_commit(item)
_assert_preconditions(preconditions, label)
except Exception as error:
recovery_errors = []
for item in reversed(staged):
try:
_rollback(item)
except Exception as rollback_error:
recovery_errors.append(str(rollback_error))
for item in staged:
try:
_cleanup(item)
except OSError as cleanup_error:
recovery_errors.append(str(cleanup_error))
suffix = (
f"; recovery failed: {'; '.join(recovery_errors)}"
if recovery_errors
else ""
with serialized_transactions():
recover_pending_locked()
_write_changes_locked(
changes,
label,
preconditions,
directory_preconditions,
opaque_removals,
)
raise OSError(f"{label}: {error}{suffix}") from error
cleanup_errors = []
for item in staged:
try:
_cleanup(item)
except OSError as error:
cleanup_errors.append(str(error))
if cleanup_errors:
raise OSError(f"{label} cleanup failed: {'; '.join(cleanup_errors)}")
def regular_file_exists(file_path: str, label: str) -> bool: