chore(python): audit plugins shared modules
_managed_files owns the file snapshot reader and public metadata helpers used by pinned directories, durable copies, and opaque snapshots; transaction staging owns read_target and same_identity_and_contents; dead helpers removed; hook_template no longer re-exports the guard generator; mypy targets win32 so _windows_security type-checks. Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
This commit is contained in:
1 parent
0f72a5e2f1
commit
ed34831c20
21 files changed
+107
-363
No files matched your search
@@ -6,6 +6,7 @@ import os
|
||||
import stat
|
||||
from typing import Any, Optional, Tuple
|
||||
|
||||
from ._managed_files import same_file_metadata
|
||||
from ._pinned_directory import PinnedDirectory
|
||||
|
||||
|
||||
@@ -27,16 +28,6 @@ def _regular_identity(
|
||||
raise OSError(f"{label} changed while copying: {path}")
|
||||
|
||||
|
||||
def _stable_metadata(metadata: os.stat_result) -> tuple[int, int, int]:
|
||||
# Timestamps are excluded: Windows exposes file times with delayed
|
||||
# cross-handle visibility, so they are unsound identity evidence.
|
||||
return (
|
||||
metadata.st_dev,
|
||||
metadata.st_ino,
|
||||
metadata.st_size,
|
||||
)
|
||||
|
||||
|
||||
def copy_into_reserved(
|
||||
directory: PinnedDirectory,
|
||||
source_path: str,
|
||||
@@ -88,7 +79,7 @@ def copy_into_reserved(
|
||||
directory.chmod_descriptor(destination_descriptor, mode)
|
||||
os.fsync(destination_descriptor)
|
||||
after = os.fstat(source_descriptor)
|
||||
if _stable_metadata(after) != _stable_metadata(before):
|
||||
if not same_file_metadata(after, before):
|
||||
raise OSError(f"{label} changed while copying: {source_path}")
|
||||
finally:
|
||||
for descriptor in (destination_descriptor, source_descriptor):
|
||||
|
||||
@@ -4,9 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import tempfile
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
JsonObject = Dict[str, Any]
|
||||
@@ -64,46 +63,3 @@ def write_text_atomic(path: str, content: str, mode: int, label: str) -> None:
|
||||
|
||||
def write_json_atomic(path: str, value: JsonObject, mode: int, label: str) -> None:
|
||||
write_text_atomic(path, json.dumps(value, indent=2) + "\n", mode, label)
|
||||
|
||||
|
||||
def remove_file(path: str, label: str) -> None:
|
||||
try:
|
||||
os.remove(path)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
except OSError as error:
|
||||
raise OSError(f"Remove {label} at {path}: {error}") from error
|
||||
|
||||
|
||||
def regular_file_exists(path: str, label: str) -> bool:
|
||||
try:
|
||||
metadata = os.stat(path)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
except OSError as error:
|
||||
raise OSError(f"Read {label} at {path}: {error}") from error
|
||||
return stat.S_ISREG(metadata.st_mode)
|
||||
|
||||
|
||||
def require_runtime(path: str, label: str) -> None:
|
||||
if not path:
|
||||
raise ValueError(f"{label} path is required")
|
||||
if not regular_file_exists(path, label):
|
||||
raise FileNotFoundError(f"{label} is missing: {path}")
|
||||
|
||||
|
||||
def read_json(path: str, label: str) -> Optional[JsonObject]:
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as file:
|
||||
raw = file.read()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except OSError as error:
|
||||
raise OSError(f"Read {label} at {path}: {error}") from error
|
||||
try:
|
||||
value = json.loads(raw)
|
||||
except json.JSONDecodeError as error:
|
||||
raise ValueError(f"Failed to parse {label} at {path}: {error}") from error
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{label} at {path} must contain a JSON object")
|
||||
return value
|
||||
@@ -28,14 +28,7 @@ class DirectorySnapshot:
|
||||
|
||||
|
||||
def identity_mode_bits(mode: int) -> int:
|
||||
"""Collapse permission bits that are not identity evidence on Windows.
|
||||
|
||||
Windows synthesizes POSIX modes and derives execute bits from the file
|
||||
extension for path stats but not for handle stats, so the synthetic value
|
||||
cannot serve as identity there. Windows permissions are enforced through
|
||||
DACLs in _windows_security instead. Captured snapshots still store the
|
||||
raw bits because restoration re-applies them.
|
||||
"""
|
||||
"""Windows synthesizes mode bits, so they are not identity there."""
|
||||
return mode if os.name != "nt" else 0
|
||||
|
||||
|
||||
@@ -44,12 +37,8 @@ def portable_file_mode(metadata: os.stat_result) -> int:
|
||||
return identity_mode_bits(stat.S_IMODE(metadata.st_mode))
|
||||
|
||||
|
||||
def _stable_file_metadata(
|
||||
metadata: os.stat_result,
|
||||
) -> tuple[int, int, int, int]:
|
||||
# Timestamps are excluded deliberately: Windows exposes file times with
|
||||
# delayed cross-handle visibility, so they are unsound identity evidence.
|
||||
# Content changes are caught by the verification re-read instead.
|
||||
def stable_file_metadata(metadata: os.stat_result) -> tuple[int, int, int, int]:
|
||||
"""Identity without timestamps; Windows file times lag across handles."""
|
||||
return (
|
||||
metadata.st_dev,
|
||||
metadata.st_ino,
|
||||
@@ -58,11 +47,8 @@ def _stable_file_metadata(
|
||||
)
|
||||
|
||||
|
||||
def _same_file_metadata(
|
||||
first: os.stat_result,
|
||||
second: os.stat_result,
|
||||
) -> bool:
|
||||
return _stable_file_metadata(first) == _stable_file_metadata(second)
|
||||
def same_file_metadata(first: os.stat_result, second: os.stat_result) -> bool:
|
||||
return stable_file_metadata(first) == stable_file_metadata(second)
|
||||
|
||||
|
||||
def assert_content_stable(
|
||||
@@ -73,12 +59,7 @@ def assert_content_stable(
|
||||
raw: bytes,
|
||||
maximum_bytes: int,
|
||||
) -> None:
|
||||
"""Re-read a just-read file and require identical identity and bytes.
|
||||
|
||||
This is the deterministic replacement for timestamp comparisons: a
|
||||
concurrent in-place rewrite is proven by content divergence between the
|
||||
two reads, independent of filesystem timer visibility.
|
||||
"""
|
||||
"""Re-read the file and require identical identity and bytes."""
|
||||
descriptor = opener()
|
||||
try:
|
||||
current = os.fstat(descriptor)
|
||||
@@ -110,26 +91,25 @@ def _inspect_regular_file(file_path: str, label: str) -> Optional[os.stat_result
|
||||
return metadata
|
||||
|
||||
|
||||
def read_physical_file(
|
||||
def read_file_snapshot(
|
||||
before: os.stat_result,
|
||||
opener: Callable[[int], int],
|
||||
file_path: str,
|
||||
label: str,
|
||||
maximum_bytes: int = MAX_SOURCE_BYTES,
|
||||
) -> Optional[FileSnapshot]:
|
||||
before = _inspect_regular_file(file_path, label)
|
||||
if before is None:
|
||||
return None
|
||||
maximum_bytes: int,
|
||||
) -> FileSnapshot:
|
||||
"""Read one regular file through opener and prove it stayed stable."""
|
||||
if before.st_size > maximum_bytes:
|
||||
raise ValueError(f"{label} exceeds {maximum_bytes} bytes: {file_path}")
|
||||
|
||||
flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_CLOEXEC", 0)
|
||||
flags |= getattr(os, "O_NOFOLLOW", 0)
|
||||
descriptor = -1
|
||||
try:
|
||||
descriptor = os.open(file_path, flags)
|
||||
descriptor = opener(flags)
|
||||
after = os.fstat(descriptor)
|
||||
if not stat.S_ISREG(after.st_mode):
|
||||
raise OSError(f"{label} path is not a physical file: {file_path}")
|
||||
if not _same_file_metadata(before, after):
|
||||
if not same_file_metadata(before, after):
|
||||
raise OSError(f"{label} changed while opening: {file_path}")
|
||||
if after.st_size > maximum_bytes:
|
||||
raise ValueError(f"{label} exceeds {maximum_bytes} bytes: {file_path}")
|
||||
@@ -137,15 +117,10 @@ def read_physical_file(
|
||||
descriptor = -1
|
||||
raw = file.read(maximum_bytes + 1)
|
||||
finished = os.fstat(file.fileno())
|
||||
if not _same_file_metadata(finished, after):
|
||||
if not same_file_metadata(finished, after):
|
||||
raise OSError(f"{label} changed while reading: {file_path}")
|
||||
assert_content_stable(
|
||||
lambda: os.open(file_path, flags),
|
||||
file_path,
|
||||
label,
|
||||
after,
|
||||
raw,
|
||||
maximum_bytes,
|
||||
lambda: opener(flags), file_path, label, after, raw, maximum_bytes
|
||||
)
|
||||
except OSError as error:
|
||||
raise OSError(f"Read {label} at {file_path}: {error}") from error
|
||||
@@ -159,11 +134,19 @@ def read_physical_file(
|
||||
contents = raw.decode("utf-8")
|
||||
except UnicodeDecodeError as error:
|
||||
raise ValueError(f"{label} at {file_path} must contain UTF-8 text") from error
|
||||
return FileSnapshot(
|
||||
contents=contents,
|
||||
device=after.st_dev,
|
||||
inode=after.st_ino,
|
||||
mode=stat.S_IMODE(after.st_mode),
|
||||
return FileSnapshot(contents, after.st_dev, after.st_ino, stat.S_IMODE(after.st_mode))
|
||||
|
||||
|
||||
def read_physical_file(
|
||||
file_path: str,
|
||||
label: str,
|
||||
maximum_bytes: int = MAX_SOURCE_BYTES,
|
||||
) -> Optional[FileSnapshot]:
|
||||
before = _inspect_regular_file(file_path, label)
|
||||
if before is None:
|
||||
return None
|
||||
return read_file_snapshot(
|
||||
before, lambda flags: os.open(file_path, flags), file_path, label, maximum_bytes
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import os
|
||||
import stat
|
||||
from typing import Optional
|
||||
|
||||
from ._managed_files import portable_file_mode
|
||||
from ._managed_files import same_file_metadata
|
||||
from ._pinned_directory import PinnedDirectory
|
||||
|
||||
|
||||
@@ -17,12 +17,7 @@ _HASH_CHUNK_BYTES = 1024 * 1024
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OpaqueFileSnapshot:
|
||||
"""Content and physical identity without retaining file bytes in memory.
|
||||
|
||||
Identity is content-based (device, inode, mode, size, SHA-256). File
|
||||
timestamps are excluded because Windows exposes them with delayed
|
||||
visibility across handles, which makes them unsound identity evidence.
|
||||
"""
|
||||
"""Device, inode, mode, size, and SHA-256 without retaining the bytes."""
|
||||
|
||||
device: int
|
||||
inode: int
|
||||
@@ -31,19 +26,6 @@ class OpaqueFileSnapshot:
|
||||
sha256: str
|
||||
|
||||
|
||||
def _metadata(metadata: os.stat_result) -> tuple[int, int, int, int]:
|
||||
return (
|
||||
metadata.st_dev,
|
||||
metadata.st_ino,
|
||||
portable_file_mode(metadata),
|
||||
metadata.st_size,
|
||||
)
|
||||
|
||||
|
||||
def _same_metadata(first: os.stat_result, second: os.stat_result) -> bool:
|
||||
return _metadata(first) == _metadata(second)
|
||||
|
||||
|
||||
def _require_regular(
|
||||
metadata: os.stat_result,
|
||||
file_path: str,
|
||||
@@ -82,7 +64,7 @@ def stream_opaque_snapshot(
|
||||
descriptor = directory.open_file(name, flags)
|
||||
opened = os.fstat(descriptor)
|
||||
_require_regular(opened, file_path, label, require_owner_only)
|
||||
if not _same_metadata(opened, before):
|
||||
if not same_file_metadata(opened, before):
|
||||
raise OSError(f"{label} changed while opening: {file_path}")
|
||||
digest = hashlib.sha256()
|
||||
while True:
|
||||
@@ -92,7 +74,7 @@ def stream_opaque_snapshot(
|
||||
digest.update(chunk)
|
||||
finished = os.fstat(descriptor)
|
||||
_require_regular(finished, file_path, label, require_owner_only)
|
||||
if not _same_metadata(finished, opened):
|
||||
if not same_file_metadata(finished, opened):
|
||||
raise OSError(f"{label} changed while hashing: {file_path}")
|
||||
except OSError as error:
|
||||
raise OSError(f"Read {label} at {file_path}: {error}") from error
|
||||
@@ -105,7 +87,7 @@ def stream_opaque_snapshot(
|
||||
except FileNotFoundError as error:
|
||||
raise OSError(f"{label} changed while hashing: {file_path}") from error
|
||||
_require_regular(visible, file_path, label, require_owner_only)
|
||||
if not _same_metadata(visible, finished):
|
||||
if not same_file_metadata(visible, finished):
|
||||
raise OSError(f"{label} changed while hashing: {file_path}")
|
||||
return OpaqueFileSnapshot(
|
||||
finished.st_dev,
|
||||
|
||||
@@ -10,8 +10,7 @@ from typing import Any, Optional, Tuple
|
||||
from ._managed_files import (
|
||||
DirectorySnapshot,
|
||||
FileSnapshot,
|
||||
_same_file_metadata,
|
||||
assert_content_stable,
|
||||
read_file_snapshot,
|
||||
read_physical_directory,
|
||||
)
|
||||
|
||||
@@ -381,8 +380,7 @@ class PinnedDirectory:
|
||||
"""Persist directory entries where the host exposes directory fsync."""
|
||||
self._require_open()
|
||||
if os.name == "nt":
|
||||
# NTFS journals atomic rename metadata. Windows directory handles do
|
||||
# not support FlushFileBuffers, while every staged file is flushed.
|
||||
# Windows directory handles do not support FlushFileBuffers.
|
||||
return
|
||||
if os.name != "posix":
|
||||
raise OSError(f"Unsupported transaction platform: {os.name}")
|
||||
@@ -410,71 +408,21 @@ class PinnedDirectory:
|
||||
label: str,
|
||||
maximum_bytes: int,
|
||||
) -> Optional[FileSnapshot]:
|
||||
file_path = self.path_for(name)
|
||||
try:
|
||||
before = self.stat_file(name)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except OSError as error:
|
||||
raise OSError(f"Inspect {label} at {self.path_for(name)}: {error}") from error
|
||||
raise OSError(f"Inspect {label} at {file_path}: {error}") from error
|
||||
if not stat.S_ISREG(before.st_mode) or stat.S_ISLNK(before.st_mode):
|
||||
raise OSError(f"{label} path is not a physical file: {self.path_for(name)}")
|
||||
if before.st_size > maximum_bytes:
|
||||
raise ValueError(
|
||||
f"{label} exceeds {maximum_bytes} bytes: {self.path_for(name)}"
|
||||
)
|
||||
|
||||
flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_CLOEXEC", 0)
|
||||
flags |= getattr(os, "O_NOFOLLOW", 0)
|
||||
descriptor = -1
|
||||
try:
|
||||
descriptor = self.open_file(name, flags)
|
||||
opened = os.fstat(descriptor)
|
||||
if not stat.S_ISREG(opened.st_mode) or not _same_file_metadata(
|
||||
before,
|
||||
opened,
|
||||
):
|
||||
raise OSError(f"{label} changed while opening: {self.path_for(name)}")
|
||||
if opened.st_size > maximum_bytes:
|
||||
raise ValueError(
|
||||
f"{label} exceeds {maximum_bytes} bytes: {self.path_for(name)}"
|
||||
)
|
||||
with os.fdopen(descriptor, "rb") as file:
|
||||
descriptor = -1
|
||||
raw = file.read(maximum_bytes + 1)
|
||||
finished = os.fstat(file.fileno())
|
||||
if not _same_file_metadata(finished, opened):
|
||||
raise OSError(
|
||||
f"{label} changed while reading: {self.path_for(name)}"
|
||||
)
|
||||
assert_content_stable(
|
||||
lambda: self.open_file(name, flags),
|
||||
self.path_for(name),
|
||||
label,
|
||||
opened,
|
||||
raw,
|
||||
maximum_bytes,
|
||||
)
|
||||
except OSError as error:
|
||||
raise OSError(f"Read {label} at {self.path_for(name)}: {error}") from error
|
||||
finally:
|
||||
if descriptor >= 0:
|
||||
os.close(descriptor)
|
||||
|
||||
if len(raw) > maximum_bytes:
|
||||
raise ValueError(
|
||||
f"{label} exceeds {maximum_bytes} bytes: {self.path_for(name)}"
|
||||
)
|
||||
try:
|
||||
contents = raw.decode("utf-8")
|
||||
except UnicodeDecodeError as error:
|
||||
raise ValueError(
|
||||
f"{label} at {self.path_for(name)} must contain UTF-8 text"
|
||||
) from error
|
||||
return FileSnapshot(
|
||||
contents,
|
||||
opened.st_dev,
|
||||
opened.st_ino,
|
||||
stat.S_IMODE(opened.st_mode),
|
||||
raise OSError(f"{label} path is not a physical file: {file_path}")
|
||||
return read_file_snapshot(
|
||||
before,
|
||||
lambda flags: self.open_file(name, flags),
|
||||
file_path,
|
||||
label,
|
||||
maximum_bytes,
|
||||
)
|
||||
|
||||
def chmod_file(
|
||||
|
||||
@@ -6,12 +6,7 @@ import os
|
||||
from typing import List, Optional, Sequence, cast
|
||||
from uuid import uuid4
|
||||
|
||||
from ._managed_files import (
|
||||
FileSnapshot,
|
||||
MAX_SOURCE_BYTES,
|
||||
physical_file_exists,
|
||||
read_physical_file,
|
||||
)
|
||||
from ._managed_files import FileSnapshot, MAX_SOURCE_BYTES, read_physical_file
|
||||
from ._opaque_cleanup import cleanup_opaque_removal
|
||||
from ._opaque_removal import (
|
||||
OpaqueRemovalChange,
|
||||
@@ -65,15 +60,6 @@ def recover_pending_transactions() -> None:
|
||||
_recover_pending_transactions()
|
||||
|
||||
|
||||
def read_optional(
|
||||
file_path: str,
|
||||
label: str,
|
||||
maximum_bytes: int = MAX_SOURCE_BYTES,
|
||||
) -> Optional[str]:
|
||||
snapshot = read_physical_file(file_path, label, maximum_bytes)
|
||||
return None if snapshot is None else snapshot.contents
|
||||
|
||||
|
||||
def source_change(
|
||||
file_path: str,
|
||||
label: str,
|
||||
@@ -479,14 +465,3 @@ def write_changes(
|
||||
directory_preconditions,
|
||||
opaque_removals,
|
||||
)
|
||||
|
||||
|
||||
def regular_file_exists(file_path: str, label: str) -> bool:
|
||||
return physical_file_exists(file_path, label)
|
||||
|
||||
|
||||
def require_runtime(file_path: str, label: str) -> None:
|
||||
if not file_path:
|
||||
raise ValueError(f"{label} path is required")
|
||||
if not regular_file_exists(file_path, label):
|
||||
raise FileNotFoundError(f"{label} is missing: {file_path}")
|
||||
@@ -13,6 +13,8 @@ from ._transaction_recovery import remove_committed_creation
|
||||
from ._transaction_staging import (
|
||||
assert_transaction_preconditions,
|
||||
assert_unchanged,
|
||||
read_target,
|
||||
same_identity_and_contents,
|
||||
same_snapshot,
|
||||
)
|
||||
from ._transaction_types import FileChange, StagedChange
|
||||
@@ -38,14 +40,6 @@ def _original_snapshot(change: FileChange) -> Optional[FileSnapshot]:
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
@@ -66,18 +60,6 @@ def _read_rollback(staged: StagedChange) -> Optional[FileSnapshot]:
|
||||
)
|
||||
|
||||
|
||||
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))
|
||||
@@ -97,7 +79,7 @@ def prepare_change(staged: StagedChange, capture_file: ReplaceFile) -> 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)
|
||||
current = read_target(staged)
|
||||
if not same_snapshot(current, original):
|
||||
raise OSError(
|
||||
f"{staged.change.label} changed at commit boundary: "
|
||||
@@ -119,7 +101,7 @@ def prepare_change(staged: StagedChange, capture_file: ReplaceFile) -> None:
|
||||
staged.change.label,
|
||||
staged.change.maximum_bytes,
|
||||
)
|
||||
current = _read_target(staged)
|
||||
current = read_target(staged)
|
||||
if not same_snapshot(current, original):
|
||||
raise OSError(
|
||||
f"{staged.change.label} changed while preparing rollback: "
|
||||
@@ -151,7 +133,7 @@ def _replace_next(
|
||||
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)
|
||||
current = read_target(staged)
|
||||
expected = _original_snapshot(staged.change)
|
||||
if not same_snapshot(current, expected):
|
||||
error = OSError(
|
||||
@@ -227,7 +209,7 @@ def commit_change(
|
||||
else:
|
||||
_replace_next(staged, replace_file, after_replace)
|
||||
|
||||
current = _read_target(staged)
|
||||
current = read_target(staged)
|
||||
if not same_snapshot(current, staged.committed_snapshot):
|
||||
raise OSError(
|
||||
f"{staged.change.label} changed immediately after commit: "
|
||||
@@ -251,7 +233,7 @@ def _restore_rollback_file(
|
||||
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):
|
||||
if not same_snapshot(read_target(staged), expected_current):
|
||||
raise _preserve_rollback(
|
||||
staged,
|
||||
OSError(
|
||||
@@ -263,7 +245,7 @@ def _restore_rollback_file(
|
||||
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):
|
||||
if not same_snapshot(read_target(staged), expected_current):
|
||||
raise _preserve_rollback(
|
||||
staged,
|
||||
OSError(
|
||||
@@ -272,7 +254,7 @@ def _restore_rollback_file(
|
||||
),
|
||||
)
|
||||
current_rollback = _read_rollback(staged)
|
||||
if not _same_identity_and_contents(current_rollback, rollback):
|
||||
if not same_identity_and_contents(current_rollback, rollback):
|
||||
raise _preserve_rollback(
|
||||
staged,
|
||||
OSError(
|
||||
@@ -288,7 +270,7 @@ def _restore_rollback_file(
|
||||
)
|
||||
atomic_replace(staged.directory, rollback_path, staged.change.file_path)
|
||||
staged.rollback_path = None
|
||||
restored = _read_target(staged)
|
||||
restored = read_target(staged)
|
||||
if (
|
||||
restored is None
|
||||
or restored.contents != expected.contents
|
||||
@@ -307,7 +289,7 @@ def rollback_change(
|
||||
) -> None:
|
||||
if not staged.committed:
|
||||
return
|
||||
current = _read_target(staged)
|
||||
current = read_target(staged)
|
||||
if not same_snapshot(current, staged.committed_snapshot):
|
||||
raise _preserve_rollback(
|
||||
staged,
|
||||
|
||||
@@ -13,7 +13,7 @@ from uuid import uuid4
|
||||
from ._managed_files import (
|
||||
DirectorySnapshot,
|
||||
FileSnapshot,
|
||||
_same_file_metadata,
|
||||
same_file_metadata,
|
||||
identity_mode_bits,
|
||||
)
|
||||
from ._opaque_snapshot import OpaqueFileSnapshot
|
||||
@@ -300,7 +300,7 @@ def inspect_fingerprint(
|
||||
descriptor = directory.open_file(name, flags)
|
||||
try:
|
||||
opened = os.fstat(descriptor)
|
||||
if not stat.S_ISREG(opened.st_mode) or not _same_file_metadata(before, opened):
|
||||
if not stat.S_ISREG(opened.st_mode) or not same_file_metadata(before, opened):
|
||||
raise OSError(f"{label} changed while opening: {file_path}")
|
||||
digest = hashlib.sha256()
|
||||
size = 0
|
||||
@@ -311,12 +311,12 @@ def inspect_fingerprint(
|
||||
digest.update(chunk)
|
||||
size += len(chunk)
|
||||
finished = os.fstat(descriptor)
|
||||
if not _same_file_metadata(finished, opened):
|
||||
if not same_file_metadata(finished, opened):
|
||||
raise OSError(f"{label} changed while hashing: {file_path}")
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
visible = directory.stat_file(name)
|
||||
if not _same_file_metadata(visible, finished):
|
||||
if not same_file_metadata(visible, finished):
|
||||
raise OSError(f"{label} changed while hashing: {file_path}")
|
||||
return FileFingerprint(
|
||||
digest.hexdigest(),
|
||||
@@ -419,7 +419,7 @@ def load_journal() -> Optional[TransactionJournal]:
|
||||
descriptor = os.open(path, flags)
|
||||
try:
|
||||
opened = os.fstat(descriptor)
|
||||
if not _same_file_metadata(opened, metadata):
|
||||
if not same_file_metadata(opened, metadata):
|
||||
raise OSError(f"Transaction journal changed while opening: {path}")
|
||||
chunks = []
|
||||
remaining = MAX_JOURNAL_BYTES + 1
|
||||
@@ -431,7 +431,7 @@ def load_journal() -> Optional[TransactionJournal]:
|
||||
remaining -= len(chunk)
|
||||
raw = b"".join(chunks)
|
||||
finished = os.fstat(descriptor)
|
||||
if not _same_file_metadata(finished, opened):
|
||||
if not same_file_metadata(finished, opened):
|
||||
raise OSError(f"Transaction journal changed while reading: {path}")
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
@@ -439,7 +439,7 @@ def load_journal() -> Optional[TransactionJournal]:
|
||||
visible = os.lstat(path)
|
||||
except FileNotFoundError as error:
|
||||
raise OSError(f"Transaction journal changed while reading: {path}") from error
|
||||
if not _same_file_metadata(visible, finished):
|
||||
if not same_file_metadata(visible, finished):
|
||||
raise OSError(f"Transaction journal changed while reading: {path}")
|
||||
if len(raw) > MAX_JOURNAL_BYTES:
|
||||
raise ValueError("Transaction journal exceeds its size limit")
|
||||
|
||||
@@ -9,21 +9,13 @@ from uuid import uuid4
|
||||
from ._managed_files import FileSnapshot
|
||||
from ._pinned_directory import PinnedDirectory
|
||||
from ._private_artifact import QuarantinedFile, reserve_private_path
|
||||
from ._transaction_staging import same_snapshot
|
||||
from ._transaction_staging import read_target, same_snapshot
|
||||
from ._transaction_types import StagedChange
|
||||
|
||||
|
||||
ReplaceFile = Callable[[str, str, PinnedDirectory], None]
|
||||
|
||||
|
||||
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_private_path(
|
||||
staged: StagedChange,
|
||||
file_path: str,
|
||||
@@ -36,18 +28,6 @@ def _read_private_path(
|
||||
)
|
||||
|
||||
|
||||
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 _recovery_path(staged: StagedChange) -> tuple[str, FileSnapshot]:
|
||||
basename = os.path.basename(staged.change.file_path)
|
||||
path = staged.directory.path_for(f".{basename}.{uuid4().hex}.recovery")
|
||||
@@ -63,76 +43,6 @@ def _preserve_private_path(error: Exception, file_path: str) -> OSError:
|
||||
return OSError(f"{error}; transaction data preserved at {file_path}")
|
||||
|
||||
|
||||
def _release_rollback_source(staged: StagedChange, source_path: str) -> bool:
|
||||
if staged.rollback_path is not None and os.path.normcase(
|
||||
os.path.abspath(staged.rollback_path)
|
||||
) == os.path.normcase(os.path.abspath(source_path)):
|
||||
staged.rollback_path = None
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _preserve_source_path(
|
||||
error: Exception,
|
||||
file_path: str,
|
||||
original_source: bool,
|
||||
) -> OSError:
|
||||
kind = "original content" if original_source else "transaction data"
|
||||
return OSError(f"{error}; {kind} preserved at {file_path}")
|
||||
|
||||
|
||||
def install_private_file(
|
||||
staged: StagedChange,
|
||||
source_path: str,
|
||||
expected: FileSnapshot,
|
||||
replace_file: ReplaceFile,
|
||||
) -> None:
|
||||
try:
|
||||
replace_file(source_path, staged.change.file_path, staged.directory)
|
||||
except Exception:
|
||||
raise
|
||||
captured = QuarantinedFile.capture(
|
||||
staged.directory,
|
||||
source_path,
|
||||
f"{staged.change.label} recovery source",
|
||||
)
|
||||
original_source = _release_rollback_source(staged, source_path)
|
||||
captured_snapshot = captured.text_snapshot(staged.change.maximum_bytes)
|
||||
if not same_snapshot(captured_snapshot, expected):
|
||||
try:
|
||||
captured.restore(source_path)
|
||||
except Exception as error:
|
||||
preserved = captured.preserve() if captured.active else source_path
|
||||
raise OSError(
|
||||
f"{error}; concurrent data preserved at {preserved}"
|
||||
) from error
|
||||
raise OSError(
|
||||
f"{staged.change.label} recovery source changed: {source_path}; "
|
||||
f"concurrent data preserved at {source_path}"
|
||||
)
|
||||
try:
|
||||
captured.link(staged.change.file_path)
|
||||
except Exception as error:
|
||||
preserved = captured.preserve() if captured.active else source_path
|
||||
raise _preserve_source_path(
|
||||
error,
|
||||
preserved,
|
||||
original_source,
|
||||
) from error
|
||||
restored = _read_target(staged)
|
||||
if not same_snapshot(restored, expected):
|
||||
preserved = captured.preserve()
|
||||
raise _preserve_source_path(
|
||||
OSError(
|
||||
f"{staged.change.label} changed while installing recovery data: "
|
||||
f"{staged.change.file_path}"
|
||||
),
|
||||
preserved,
|
||||
original_source,
|
||||
)
|
||||
captured.discard()
|
||||
|
||||
|
||||
def capture_recovery_target(
|
||||
staged: StagedChange,
|
||||
expected: FileSnapshot,
|
||||
@@ -263,7 +173,7 @@ def remove_committed_creation(
|
||||
replace_file,
|
||||
capture_file,
|
||||
)
|
||||
if _read_target(staged) is not None:
|
||||
if read_target(staged) is not None:
|
||||
discard_private_file(staged, recovery_path, recovery_snapshot)
|
||||
raise OSError(
|
||||
f"{staged.change.label} changed after recovery capture: "
|
||||
@@ -275,6 +185,5 @@ def remove_committed_creation(
|
||||
__all__ = [
|
||||
"capture_recovery_target",
|
||||
"discard_private_file",
|
||||
"install_private_file",
|
||||
"remove_committed_creation",
|
||||
]
|
||||
@@ -46,6 +46,26 @@ def same_snapshot(
|
||||
return current == expected
|
||||
|
||||
|
||||
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 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 remove_optional(directory: PinnedDirectory, file_path: Optional[str]) -> None:
|
||||
if not file_path:
|
||||
return
|
||||
@@ -338,8 +358,7 @@ def _remove_exact_text(
|
||||
!= (expected.device, expected.inode)
|
||||
)
|
||||
if not changed:
|
||||
# An in-place rewrite keeps the inode, so the removal is only exact
|
||||
# once the bytes also match the owned snapshot.
|
||||
# An in-place rewrite keeps the inode; bytes must match too.
|
||||
try:
|
||||
snapshot = captured.text_snapshot(
|
||||
len(expected.contents.encode("utf-8"))
|
||||
|
||||
@@ -210,12 +210,6 @@ def _current_user_sid() -> Tuple[Any, str]:
|
||||
close_handle(int(token_value))
|
||||
|
||||
|
||||
def current_user_sid_string() -> str:
|
||||
"""Return the stable current-user SID used by the state namespace."""
|
||||
_sid, sid_text = _current_user_sid()
|
||||
return sid_text
|
||||
|
||||
|
||||
def _security_descriptor(directory: bool) -> Tuple[int, int]:
|
||||
_sid, sid_text = _current_user_sid()
|
||||
inheritance = "OICI" if directory else ""
|
||||
@@ -491,7 +485,6 @@ def verify_private_path(path: str, *, directory: bool) -> None:
|
||||
|
||||
__all__ = [
|
||||
"close_handle",
|
||||
"current_user_sid_string",
|
||||
"ensure_private_directory",
|
||||
"open_private_file",
|
||||
"verify_private_path",
|
||||
|
||||
@@ -27,7 +27,8 @@ from .claudecode_io import (
|
||||
validate_private_key,
|
||||
validate_runtime_tree,
|
||||
)
|
||||
from .hook_template import generate_guard_script, generate_hook_script
|
||||
from .guard_template import generate_guard_script
|
||||
from .hook_template import generate_hook_script
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@@ -36,7 +36,8 @@ from .codex_io import (
|
||||
validate_runtime_tree,
|
||||
write_codex_changes,
|
||||
)
|
||||
from .hook_template import generate_guard_script, generate_hook_script
|
||||
from .guard_template import generate_guard_script
|
||||
from .hook_template import generate_hook_script
|
||||
|
||||
|
||||
def _runtime_config(config: InstallConfig, agent_id: str) -> dict:
|
||||
|
||||
@@ -36,7 +36,8 @@ from .cursor_io import (
|
||||
validate_runtime_identity,
|
||||
write_cursor_changes,
|
||||
)
|
||||
from .hook_template import generate_guard_script, generate_hook_script
|
||||
from .guard_template import generate_guard_script
|
||||
from .hook_template import generate_hook_script
|
||||
|
||||
|
||||
def _runtime_config(config: InstallConfig, agent_id: str) -> dict:
|
||||
|
||||
@@ -26,7 +26,8 @@ from .gemini_io import (
|
||||
validate_private_key,
|
||||
validate_runtime_tree,
|
||||
)
|
||||
from .hook_template import generate_guard_script, generate_hook_script
|
||||
from .guard_template import generate_guard_script
|
||||
from .hook_template import generate_hook_script
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@@ -25,7 +25,8 @@ from .grok_io import (
|
||||
validate_private_key,
|
||||
validate_runtime_tree,
|
||||
)
|
||||
from .hook_template import generate_guard_script, generate_hook_script
|
||||
from .guard_template import generate_guard_script
|
||||
from .hook_template import generate_hook_script
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@@ -5,11 +5,8 @@ from __future__ import annotations
|
||||
import sys
|
||||
|
||||
from .error_log_template import ERROR_LOG_RUNTIME
|
||||
from .guard_template import generate_guard_script
|
||||
from .runtime_reader_template import PROTECTED_RUNTIME_READER
|
||||
|
||||
__all__ = ["generate_guard_script", "generate_hook_script"]
|
||||
|
||||
|
||||
def generate_hook_script(
|
||||
*,
|
||||
|
||||
@@ -12,7 +12,8 @@ from elydora._runtime_paths import resolve_agent_directory
|
||||
from ._managed_files import MAX_CONFIG_BYTES, MAX_SECRET_BYTES, MAX_SOURCE_BYTES
|
||||
from ._transaction import FileChange, file_change, source_change, write_changes
|
||||
from .base import InstallConfig
|
||||
from .hook_template import generate_guard_script, generate_hook_script
|
||||
from .guard_template import generate_guard_script
|
||||
from .hook_template import generate_hook_script
|
||||
from .kimi_command import runtime_root, same_kimi_path
|
||||
from .kimi_contract import (
|
||||
AGENT_KEY,
|
||||
|
||||
Reference in new issue
Block a user