Files
Elydora-Python-SDK/elydora/plugins/_runtime_removal.py
T
ZacharyZhang-NY e37418c489 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.
2026-07-21 18:53:06 -04:00

275 lines
8.6 KiB
Python

"""Identity-bound removal of one Elydora agent runtime directory."""
from __future__ import annotations
from dataclasses import dataclass
import os
from typing import Callable, Dict, List, Optional, Tuple
from elydora._runtime_paths import resolve_agent_directory, runtime_root
from ._managed_files import (
DirectorySnapshot,
FileSnapshot,
MAX_CONFIG_BYTES,
MAX_SECRET_BYTES,
MAX_SOURCE_BYTES,
read_physical_directory,
)
from ._directory_removal import remove_pinned_empty_directory
from ._opaque_removal import OpaqueRemovalChange, prepare_opaque_removal
from ._pinned_directory import PinnedDirectory
from ._strict_json import JsonObject, parse_json_object
from ._transaction import write_changes
from ._transaction_types import (
DirectoryPrecondition,
FileChange,
FilePrecondition,
)
RuntimeConfigValidator = Callable[[JsonObject, str], None]
RuntimeFileContract = Dict[str, Tuple[str, int]]
STANDARD_RUNTIME_FILES: RuntimeFileContract = {
"chain-state.json": ("Elydora chain state", MAX_SOURCE_BYTES),
"status-cache.json": ("Elydora status cache", MAX_SOURCE_BYTES),
"error.log": ("Elydora error log", MAX_SOURCE_BYTES),
"guard.py": ("Elydora guard runtime", MAX_SOURCE_BYTES),
"hook.py": ("Elydora audit runtime", MAX_SOURCE_BYTES),
"private.key": ("Elydora private key", MAX_SECRET_BYTES),
"config.json": ("Elydora runtime config", MAX_CONFIG_BYTES),
}
KIRO_IDE_RUNTIME_FILES: RuntimeFileContract = dict(STANDARD_RUNTIME_FILES)
_OPAQUE_RUNTIME_FILE = "error.log"
def _managed_runtime_files(agent_name: str) -> RuntimeFileContract:
files = dict(STANDARD_RUNTIME_FILES)
if agent_name == "augment":
extension = ".cmd" if os.name == "nt" else ".sh"
files.update(
{
f"augment-guard{extension}": (
"Elydora Augment guard wrapper",
MAX_SOURCE_BYTES,
),
f"augment-hook{extension}": (
"Elydora Augment audit wrapper",
MAX_SOURCE_BYTES,
),
}
)
return files
@dataclass(frozen=True)
class PreparedRuntimeRemoval:
agent_id: str
agent_name: str
runtime_root: str
agent_directory: str
root_snapshot: DirectorySnapshot
agent_snapshot: DirectorySnapshot
changes: List[FileChange]
opaque_removals: List[OpaqueRemovalChange]
file_preconditions: List[FilePrecondition]
directory_preconditions: List[DirectoryPrecondition]
def _close_pinned(*directories: Optional[PinnedDirectory]) -> None:
failures = []
for directory in reversed(directories):
if directory is None:
continue
try:
directory.close()
except OSError as error:
failures.append(str(error))
if failures:
raise OSError("; ".join(failures))
def _validate_identity(
config: JsonObject,
config_path: str,
agent_id: str,
agent_name: str,
) -> None:
if config.get("agent_id") != agent_id or config.get("agent_name") != agent_name:
raise ValueError(
f"Elydora runtime identity does not match {agent_name}: {config_path}"
)
def _removal_change(
file_path: str,
label: str,
snapshot: FileSnapshot,
maximum_bytes: int,
) -> FileChange:
return FileChange(
file_path,
label,
snapshot.contents,
None,
snapshot.mode,
snapshot.mode,
snapshot.device,
snapshot.inode,
maximum_bytes,
)
def prepare_runtime_removal(
agent_id: str,
agent_name: str,
*,
config_validator: Optional[RuntimeConfigValidator] = None,
managed_files: Optional[RuntimeFileContract] = None,
) -> Optional[PreparedRuntimeRemoval]:
"""Inspect an exact managed runtime without mutating it."""
root_path = runtime_root()
agent_path = resolve_agent_directory(root_path, agent_id)
runtime_files = (
_managed_runtime_files(agent_name)
if managed_files is None
else managed_files
)
root_snapshot = read_physical_directory(root_path, "Elydora runtime directory")
if root_snapshot is None:
return None
agent_snapshot = read_physical_directory(
agent_path, "Elydora agent runtime directory"
)
if agent_snapshot is None:
return None
root = None
agent = None
try:
root = PinnedDirectory.open(
root_path, "Elydora runtime directory", root_snapshot
)
agent = PinnedDirectory.open(
agent_path, "Elydora agent runtime directory", agent_snapshot
)
names = agent.list_names()
unknown = [name for name in names if name not in runtime_files]
if unknown:
raise OSError(
"Elydora agent runtime contains unmanaged entries: "
f"{agent_path}: {', '.join(unknown)}"
)
opaque_path = os.path.join(agent_path, _OPAQUE_RUNTIME_FILE)
opaque_removals = [
prepare_opaque_removal(
agent,
opaque_path,
runtime_files[_OPAQUE_RUNTIME_FILE][0],
)
]
snapshots: Dict[str, Optional[FileSnapshot]] = {}
for name, (label, maximum_bytes) in runtime_files.items():
if name == _OPAQUE_RUNTIME_FILE:
continue
snapshots[name] = agent.read_file(name, label, maximum_bytes)
config_path = os.path.join(agent_path, "config.json")
config_snapshot = snapshots.get("config.json")
present = [name for name, snapshot in snapshots.items() if snapshot is not None]
if opaque_removals[0].original is not None:
present.append(_OPAQUE_RUNTIME_FILE)
if config_snapshot is None:
if present:
raise ValueError(
"Elydora runtime identity cannot be verified without "
f"config.json: {agent_path}"
)
else:
config = parse_json_object(
config_snapshot.contents,
f"Elydora runtime config at {config_path}",
)
_validate_identity(config, config_path, agent_id, agent_name)
if config_validator is not None:
config_validator(config, config_path)
changes = []
preconditions = []
for name, (label, maximum_bytes) in runtime_files.items():
if name == _OPAQUE_RUNTIME_FILE:
continue
file_path = os.path.join(agent_path, name)
snapshot = snapshots[name]
if snapshot is None:
preconditions.append(
FilePrecondition(file_path, label, None, maximum_bytes)
)
else:
changes.append(
_removal_change(file_path, label, snapshot, maximum_bytes)
)
root.assert_path_stable("runtime removal preparation")
agent.assert_path_stable("runtime removal preparation")
return PreparedRuntimeRemoval(
agent_id,
agent_name,
root_path,
agent_path,
root_snapshot,
agent_snapshot,
changes,
opaque_removals,
preconditions,
[
DirectoryPrecondition(
root_path, "Elydora runtime directory", root_snapshot
),
DirectoryPrecondition(
agent_path,
"Elydora agent runtime directory",
agent_snapshot,
),
],
)
finally:
_close_pinned(root, agent)
def finalize_runtime_removal(plan: PreparedRuntimeRemoval) -> Optional[str]:
"""Remove the exact empty agent directory retained by a completed transaction."""
root = None
agent = None
try:
root = PinnedDirectory.open(
plan.runtime_root,
"Elydora runtime directory",
plan.root_snapshot,
)
agent = PinnedDirectory.open(
plan.agent_directory,
"Elydora agent runtime directory",
plan.agent_snapshot,
delete_access=True,
)
preserved = remove_pinned_empty_directory(root, agent)
finally:
_close_pinned(root, agent)
return preserved
def commit_runtime_removal(plan: PreparedRuntimeRemoval) -> None:
"""Delete all proven managed files, then the exact empty agent directory."""
write_changes(
plan.changes,
f"Remove {plan.agent_name} runtime",
plan.file_preconditions,
plan.directory_preconditions,
plan.opaque_removals,
)
finalize_runtime_removal(plan)