1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages

Add deterministic client integration diagnostics

This commit is contained in:
Andraxion 2026-07-29 10:15:27 -04:00
parent eb9355b003
commit fb0df5e4a1
15 changed files with 6193 additions and 18 deletions

View file

@ -11,6 +11,7 @@ import tempfile
import tomllib
from collections import Counter
from collections.abc import Mapping
from contextlib import suppress
from dataclasses import dataclass, replace
from pathlib import Path, PurePosixPath
from typing import Any, cast
@ -39,6 +40,7 @@ from .telemetry import increment, stage
SOURCE_GENERATION_SCHEMA_VERSION = 1
GENERIC_SOURCE_CONTRACT = "docforge-core:0.7.1:index:1"
MAX_PROJECT_DESCRIPTOR_BYTES = 1_000_000
_CORE_METADATA = frozenset(
{
@ -210,12 +212,167 @@ def _receipt_signature(path: Path) -> tuple[int, int, int, int, int] | None:
)
def _read_descriptor(descriptor_path: Path) -> bytes:
try:
parent = descriptor_path.parent
parent_status = parent.lstat()
if (
stat.S_ISLNK(parent_status.st_mode)
or not stat.S_ISDIR(parent_status.st_mode)
or parent.resolve(strict=True) != parent
):
raise DocForgeError(
"project_descriptor_unsafe",
"Project descriptor parent must be one real confined directory",
)
directory_fd = os.open(
parent,
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
)
except FileNotFoundError as error:
raise DocForgeError("missing_config", "Missing .docforge/project.toml") from error
except DocForgeError:
raise
except OSError as error:
raise DocForgeError(
"project_descriptor_unsafe",
"Project descriptor cannot be inspected safely",
) from error
opened_parent = os.fstat(directory_fd)
if opened_parent.st_dev != parent_status.st_dev or opened_parent.st_ino != parent_status.st_ino:
with suppress(OSError):
os.close(directory_fd)
raise DocForgeError(
"project_descriptor_changed",
"Project descriptor parent changed while it was opened",
)
try:
def parent_current() -> bool:
try:
before = parent.lstat()
resolved = parent.resolve(strict=True)
after = parent.lstat()
opened = os.fstat(directory_fd)
return (
not stat.S_ISLNK(before.st_mode)
and stat.S_ISDIR(before.st_mode)
and resolved == parent
and (before.st_dev, before.st_ino, before.st_mode)
== (after.st_dev, after.st_ino, after.st_mode)
== (opened.st_dev, opened.st_ino, opened.st_mode)
)
except OSError:
return False
if not parent_current():
raise DocForgeError(
"project_descriptor_changed",
"Project descriptor parent changed before it was read",
)
try:
before = os.stat(
descriptor_path.name,
dir_fd=directory_fd,
follow_symlinks=False,
)
except FileNotFoundError as error:
raise DocForgeError("missing_config", "Missing .docforge/project.toml") from error
except OSError as error:
raise DocForgeError(
"project_descriptor_unsafe",
"Project descriptor cannot be inspected safely",
) from error
if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode):
raise DocForgeError(
"project_descriptor_unsafe",
"Project descriptor must be a regular file and not a symbolic link",
)
if before.st_size > MAX_PROJECT_DESCRIPTOR_BYTES:
raise DocForgeError(
"project_descriptor_oversized",
"Project descriptor exceeds the bounded configuration limit",
maximum_bytes=MAX_PROJECT_DESCRIPTOR_BYTES,
)
try:
descriptor = os.open(
descriptor_path.name,
os.O_RDONLY | os.O_NOFOLLOW,
dir_fd=directory_fd,
)
except OSError as error:
raise DocForgeError(
"project_descriptor_unsafe",
"Project descriptor cannot be opened safely",
) from error
try:
opened = os.fstat(descriptor)
if opened.st_dev != before.st_dev or opened.st_ino != before.st_ino:
raise DocForgeError(
"project_descriptor_changed",
"Project descriptor changed while it was opened",
)
chunks: list[bytes] = []
remaining = MAX_PROJECT_DESCRIPTOR_BYTES + 1
while remaining:
chunk = os.read(descriptor, min(65_536, remaining))
if not chunk:
break
chunks.append(chunk)
remaining -= len(chunk)
raw = b"".join(chunks)
finally:
with suppress(OSError):
os.close(descriptor)
if len(raw) > MAX_PROJECT_DESCRIPTOR_BYTES:
raise DocForgeError(
"project_descriptor_oversized",
"Project descriptor exceeds the bounded configuration limit",
maximum_bytes=MAX_PROJECT_DESCRIPTOR_BYTES,
)
try:
after = os.stat(
descriptor_path.name,
dir_fd=directory_fd,
follow_symlinks=False,
)
except OSError as error:
raise DocForgeError(
"project_descriptor_changed",
"Project descriptor changed while it was read",
) from error
if (
before.st_dev,
before.st_ino,
before.st_size,
before.st_mtime_ns,
before.st_ctime_ns,
) != (
after.st_dev,
after.st_ino,
after.st_size,
after.st_mtime_ns,
after.st_ctime_ns,
):
raise DocForgeError(
"project_descriptor_changed",
"Project descriptor changed while it was read",
)
if not parent_current():
raise DocForgeError(
"project_descriptor_changed",
"Project descriptor parent changed while it was read",
)
return raw
finally:
with suppress(OSError):
os.close(directory_fd)
def _load_descriptor(root: Path) -> ProjectDescriptor:
descriptor_path = root / ".docforge" / "project.toml"
if not descriptor_path.is_file():
raise DocForgeError("missing_config", "Missing .docforge/project.toml")
descriptor_bytes = _read_descriptor(descriptor_path)
try:
descriptor_bytes = descriptor_path.read_bytes()
document = cast(dict[str, object], tomllib.loads(descriptor_bytes.decode("utf-8")))
except UnicodeDecodeError as error:
raise DocForgeError("invalid_config", "Project descriptor is not UTF-8") from error
@ -472,6 +629,17 @@ def _load_descriptor(root: Path) -> ProjectDescriptor:
)
def validate_descriptor_binding(descriptor: ProjectDescriptor) -> None:
"""Require the bounded descriptor bytes to match one opened project binding."""
descriptor_bytes = _read_descriptor(descriptor.descriptor_path)
if hashlib.sha256(descriptor_bytes).hexdigest() != descriptor.descriptor_hash:
raise DocForgeError(
"source_changed",
"Project descriptor changed after the project was opened",
)
def _markdown_record(path: Path, text: str) -> tuple[dict[str, Any], str]:
lines = text.splitlines()
if not lines or lines[0] != "+++":
@ -739,11 +907,7 @@ class Project:
def load(self) -> ProjectSnapshot:
increment("project_loads")
descriptor_bytes = self.descriptor.descriptor_path.read_bytes()
if hashlib.sha256(descriptor_bytes).hexdigest() != self.descriptor.descriptor_hash:
raise DocForgeError(
"source_changed", "Project descriptor changed after the project was opened"
)
validate_descriptor_binding(self.descriptor)
ordered_sources, ordered_directories = self._canonical_inventory()
generation_paths = (
self.descriptor.descriptor_path,