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

+336
View File
@@ -0,0 +1,336 @@
"""Durable preparation, atomic commit, and rollback for text-file changes."""
from __future__ import annotations
import os
from typing import Callable, Optional
from ._durable_file import atomic_replace, copy_into_reserved
from ._managed_files import FileSnapshot
from ._pinned_directory import PinnedDirectory
from ._private_artifact import QuarantinedFile
from ._transaction_recovery import remove_committed_creation
from ._transaction_staging import (
assert_transaction_preconditions,
assert_unchanged,
same_snapshot,
)
from ._transaction_types import FileChange, StagedChange
ReplaceFile = Callable[[str, str, PinnedDirectory], None]
def _original_snapshot(change: FileChange) -> Optional[FileSnapshot]:
if change.original is None:
return None
if (
change.original_device is None
or change.original_inode is None
or change.original_mode is None
):
raise OSError(f"Missing original identity for {change.label}")
return FileSnapshot(
change.original,
change.original_device,
change.original_inode,
change.original_mode,
)
def _read_target(staged: StagedChange) -> Optional[FileSnapshot]:
return staged.directory.read_file(
staged.directory.name_for(staged.change.file_path),
staged.change.label,
staged.change.maximum_bytes,
)
def _read_temporary(staged: StagedChange) -> Optional[FileSnapshot]:
if staged.temporary_path is None:
return None
return staged.directory.read_file(
staged.directory.name_for(staged.temporary_path),
f"staged {staged.change.label}",
staged.change.maximum_bytes,
)
def _read_rollback(staged: StagedChange) -> Optional[FileSnapshot]:
if staged.rollback_path is None:
return None
return staged.directory.read_file(
staged.directory.name_for(staged.rollback_path),
f"{staged.change.label} rollback",
staged.change.maximum_bytes,
)
def _same_identity_and_contents(
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 _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 prepare_change(staged: StagedChange, capture_file: ReplaceFile) -> None:
"""Create and flush rollback material while the public target stays visible."""
original = _original_snapshot(staged.change)
if original is None:
return
reservation = staged.rollback_reservation
rollback_path = staged.rollback_path
if reservation is None or rollback_path is None:
raise OSError(f"Missing rollback reservation for {staged.change.label}")
capture_file(staged.change.file_path, rollback_path, staged.directory)
assert_transaction_preconditions(*staged.preconditions, staged.directories)
current = _read_target(staged)
if not same_snapshot(current, original):
raise OSError(
f"{staged.change.label} changed at commit boundary: "
f"{staged.change.file_path}"
)
rollback = _read_rollback(staged)
if not same_snapshot(rollback, reservation):
raise OSError(
f"{staged.change.label} existing rollback reservation changed: "
f"{rollback_path}"
)
copy_into_reserved(
staged.directory,
staged.change.file_path,
rollback_path,
(original.device, original.inode),
(reservation.device, reservation.inode),
0o600,
staged.change.label,
staged.change.maximum_bytes,
)
current = _read_target(staged)
if not same_snapshot(current, original):
raise OSError(
f"{staged.change.label} changed while preparing rollback: "
f"{staged.change.file_path}"
)
prepared = _read_rollback(staged)
if (
prepared is None
or prepared.contents != original.contents
or (prepared.device, prepared.inode)
!= (reservation.device, reservation.inode)
or (os.name != "nt" and prepared.mode != 0o600)
):
raise OSError(
f"{staged.change.label} rollback changed while preparing: {rollback_path}"
)
staged.rollback_snapshot = prepared
staged.rollback_reservation = None
def _replace_next(
staged: StagedChange,
replace_file: ReplaceFile,
after_replace: ReplaceFile,
) -> None:
temporary = staged.temporary_snapshot
temporary_path = staged.temporary_path
if temporary_path is None or temporary is None:
raise OSError(f"Missing staged file for {staged.change.label}")
replace_file(temporary_path, staged.change.file_path, staged.directory)
assert_transaction_preconditions(*staged.preconditions, staged.directories)
current = _read_target(staged)
expected = _original_snapshot(staged.change)
if not same_snapshot(current, expected):
error = OSError(
f"{staged.change.label} changed before atomic replacement: "
f"{staged.change.file_path}"
)
if expected is not None:
raise _preserve_rollback(staged, error) from error
raise error
if not same_snapshot(_read_temporary(staged), temporary):
raise OSError(
f"Staged {staged.change.label} changed at install boundary: "
f"{temporary_path}"
)
atomic_replace(staged.directory, temporary_path, staged.change.file_path)
staged.temporary_path = None
staged.committed_snapshot = temporary
staged.committed = True
after_replace(temporary_path, staged.change.file_path, staged.directory)
def _remove_original(staged: StagedChange) -> None:
original = _original_snapshot(staged.change)
if original is None:
raise OSError(f"Missing original data for {staged.change.label}")
captured = QuarantinedFile.capture(
staged.directory,
staged.change.file_path,
staged.change.label,
)
snapshot = captured.text_snapshot(staged.change.maximum_bytes)
if not same_snapshot(snapshot, original):
try:
captured.restore()
except Exception as error:
preserved = (
captured.preserve()
if captured.active
else staged.change.file_path
)
raise OSError(
f"{staged.change.label} changed at removal boundary; "
f"concurrent data preserved at {preserved}: {error}"
) from error
raise OSError(
f"{staged.change.label} changed at removal boundary: "
f"{staged.change.file_path}"
)
captured.discard()
staged.directory.sync()
staged.committed_snapshot = None
staged.committed = True
def commit_change(
staged: StagedChange,
replace_file: ReplaceFile,
capture_file: ReplaceFile,
after_replace: ReplaceFile,
) -> None:
del capture_file
assert_transaction_preconditions(*staged.preconditions, staged.directories)
assert_unchanged(staged.change, staged.directories)
if staged.change.original is not None and staged.rollback_snapshot is None:
raise OSError(f"Missing rollback data for {staged.change.label}")
if staged.change.next_source is None:
_remove_original(staged)
after_replace(
staged.change.file_path,
staged.change.file_path,
staged.directory,
)
else:
_replace_next(staged, replace_file, after_replace)
current = _read_target(staged)
if not same_snapshot(current, staged.committed_snapshot):
raise OSError(
f"{staged.change.label} changed immediately after commit: "
f"{staged.change.file_path}"
)
if os.name != "nt" and current is not None and current.mode != staged.change.mode:
raise OSError(
f"{staged.change.label} has an unexpected mode after commit: "
f"{staged.change.file_path}"
)
assert_transaction_preconditions(*staged.preconditions, staged.directories)
def _restore_rollback_file(
staged: StagedChange,
expected: FileSnapshot,
expected_current: Optional[FileSnapshot],
restore_file: ReplaceFile,
) -> None:
rollback_path = staged.rollback_path
rollback = staged.rollback_snapshot
if rollback_path is None or rollback is None:
raise OSError(f"Missing rollback path for {staged.change.label}")
if not same_snapshot(_read_target(staged), expected_current):
raise _preserve_rollback(
staged,
OSError(
f"{staged.change.label} changed before restoration: "
f"{staged.change.file_path}"
),
)
try:
restore_file(rollback_path, staged.change.file_path, staged.directory)
except Exception as error:
raise _preserve_rollback(staged, error) from error
if not same_snapshot(_read_target(staged), expected_current):
raise _preserve_rollback(
staged,
OSError(
f"{staged.change.label} changed before atomic restoration: "
f"{staged.change.file_path}"
),
)
current_rollback = _read_rollback(staged)
if not _same_identity_and_contents(current_rollback, rollback):
raise _preserve_rollback(
staged,
OSError(
f"{staged.change.label} recovery source changed; concurrent data "
f"preserved at {rollback_path}"
),
)
if expected.mode != rollback.mode:
staged.directory.chmod_file(
staged.directory.name_for(rollback_path),
expected.mode,
(rollback.device, rollback.inode),
)
atomic_replace(staged.directory, rollback_path, staged.change.file_path)
staged.rollback_path = None
restored = _read_target(staged)
if (
restored is None
or restored.contents != expected.contents
or restored.mode != expected.mode
):
raise OSError(
f"{staged.change.label} changed while restoring: "
f"{staged.change.file_path}"
)
def rollback_change(
staged: StagedChange,
restore_file: ReplaceFile,
capture_file: ReplaceFile,
) -> None:
if not staged.committed:
return
current = _read_target(staged)
if not same_snapshot(current, staged.committed_snapshot):
raise _preserve_rollback(
staged,
OSError(
f"{staged.change.label} changed during transaction recovery: "
f"{staged.change.file_path}"
),
)
if staged.change.original is None:
remove_committed_creation(staged, restore_file, capture_file)
return
original = _original_snapshot(staged.change)
if original is None:
raise OSError(f"Missing original data for {staged.change.label}")
_restore_rollback_file(
staged,
original,
staged.committed_snapshot,
restore_file,
)
staged.rollback_snapshot = None
staged.committed_snapshot = None
staged.committed = False
__all__ = ["commit_change", "prepare_change", "rollback_change"]