Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
afbc737
Prepare fresh functional wheel targets through the runtime owner
trissim Nov 1, 2026
85965da
Use the runtime homes declared UV instead of caller PATH
trissim Nov 1, 2026
f9b7206
Record interpreter artifacts and verify retained runtime wheel inputs
trissim Nov 1, 2026
590f535
Bind repository proofs to their runtime interpreter observation
trissim Nov 1, 2026
2a4698b
Reobserve a built donor's retained wheel inputs
trissim Nov 1, 2026
3bd1611
Prepare install observations and publish real proof fixture refs
trissim Nov 1, 2026
5341b4a
Separate recorded runtime completion from production lock reuse
trissim Nov 1, 2026
1f31b55
Import runtime preparation storage policy from its declaration
trissim Nov 1, 2026
46fc678
Bind interpreter images to mapped files and expose model-owned schemas
trissim Nov 2, 2026
b7e0137
Reject active shell substitutions before quote removal
trissim Nov 1, 2026
cf5b080
Preserve unquoted words across shell continuations
trissim Nov 1, 2026
eede686
Read expanding heredocs as logical lines
trissim Nov 1, 2026
e5136c7
Decode shell continuations before syntax classification
trissim Nov 2, 2026
ba2cc72
Bound shell source reads without copying the command
trissim Nov 2, 2026
9aa3ae5
Match the native text read signature at the shell boundary
trissim Nov 2, 2026
33c2b1c
Match StringIO readline size annotation
trissim Nov 2, 2026
dc5ddbc
Merge remote-tracking branch 'origin/pi-commander-3825-46fc' into bat…
trissim Nov 2, 2026
45be1d1
Merge remote-tracking branch 'origin/pi-commander-3847-33c2' into bat…
trissim Nov 2, 2026
ae6468c
Keep absent interpreter evidence out of runtime cache matches
trissim Nov 2, 2026
e2ebacf
Ship merged export and shared native border contract
trissim Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/agent_comms/build_artifact.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Stable file-content observations shared by interpreter and wheel build inputs."""

from __future__ import annotations

import hashlib
from pathlib import Path
from typing import Annotated, BinaryIO

from pydantic import BaseModel, ConfigDict, field_validator

from agent_comms.nominal.field_codec import Sha256Text
from agent_comms.private_path import FileRevision


class BuildArtifact(BaseModel):
"""A native file identity and its bytes, captured through the same open handle."""

model_config = ConfigDict(frozen=True, extra="forbid")
path: Path
revision: FileRevision
sha256: Annotated[str, Sha256Text]

@field_validator("sha256")
@classmethod
def valid_digest(cls, value: str) -> str:
return Sha256Text.decode(value)

@classmethod
def capture(cls, path: Path) -> BuildArtifact:
path = path.resolve(strict=True)
with path.open("rb") as stream:
return cls.from_stream(stream)

@classmethod
def from_stream(cls, stream: BinaryIO) -> BuildArtifact:
"""Capture the already-open file, including a platform-qualified native image."""
path = Path(stream.name).resolve(strict=True)
stream.seek(0)
revision = FileRevision.from_file(stream)
digest = hashlib.file_digest(stream, "sha256").hexdigest()
if revision != FileRevision.from_file(stream):
raise ValueError(f"Build artifact changed while reading: {path}")
if FileRevision.capture(path) != revision:
raise ValueError(f"Build artifact was replaced while reading: {path}")
return cls(path=path, revision=revision, sha256=digest)
41 changes: 29 additions & 12 deletions src/agent_comms/build_identity.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
"""Installed repository commits, shared by delivery proofs and close checks."""
"""Installed repository and interpreter identity, shared by proofs and close checks."""

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Annotated, ClassVar

from agent_comms.nominal.declared_family import DeclaredFamily
from agent_comms.nominal.field_codec import GitCommitText
from agent_comms.nominal.field_codec import GitCommitText, WireOmitDefault
from agent_comms.installer.runtime_resolution import RuntimePackage, RuntimeResolution
from agent_comms.installer.probes.interpreter import InterpreterBuild, RecordedInterpreterBuild
from agent_comms.launch_runtime import LaunchRuntime
from agent_comms.repo import Repo
from agent_comms.repository_name import RepositoryName
Expand All @@ -20,7 +21,7 @@


class ProofBuildMismatchError(ValueError):
"""A retained proof does not identify the currently installed repositories."""
"""A retained proof does not identify the installed repositories and interpreter."""


class DeliveredCommitNotInstalledError(ValueError):
Expand Down Expand Up @@ -132,7 +133,7 @@ def installation(self, commits: tuple[str, ...], directory: Path) -> CommitInsta
@dataclass(frozen=True)
class UnrecordedBuildIdentity(BuildIdentity):
def recorded(self) -> RecordedBuildIdentity:
return RecordedBuildIdentity.installed()
return RecordedBuildIdentity.installed().recorded()

def installation(self, commits: tuple[str, ...], directory: Path) -> CommitInstallation:
installed = RecordedBuildIdentity.installed()
Expand All @@ -143,28 +144,43 @@ def installation(self, commits: tuple[str, ...], directory: Path) -> CommitInsta

@dataclass(frozen=True)
class RecordedBuildIdentity(BuildIdentity, InstalledBuild):
"""Declared runtime source revisions, each identified by its repository."""
"""Repository revisions and the interpreter observation owned by their runtime."""

commits: tuple[RepositoryCommit, ...]
interpreter: Annotated[
RecordedInterpreterBuild | InterpreterBuild, WireOmitDefault()
] = field(default_factory=InterpreterBuild)

def __str__(self) -> str:
return ", ".join(
f"{record.repository.to_wire()} {record.commit}" for record in self.commits
)
return ", ".join((
*(f"{record.repository.to_wire()} {record.commit}" for record in self.commits),
str(self.interpreter),
))

def recorded(self) -> RecordedBuildIdentity:
try:
self.interpreter.require_recorded()
except ValueError as error:
raise ProofBuildMismatchError(
f"Proof build {self} has no interpreter observation; run and record a new proof."
) from error
return self

@classmethod
def installed(cls) -> RecordedBuildIdentity:
runtime = LaunchRuntime.installed()
return cls.at(LaunchRuntime.installed())

@classmethod
def at(cls, runtime: LaunchRuntime) -> RecordedBuildIdentity:
"""Read the selected build, never borrow the current pointer for a candidate."""
runtime.require_available()
resolution = RuntimeResolution.at(runtime.prefix)
return cls(
tuple(
RepositoryCommit(member.repository, GitCommitText.decode(member.commit(resolution)))
for member in RuntimePackage.members_with(RuntimePackage)
)
),
interpreter=resolution.interpreter,
)

def for_repository(self, repository: Repo) -> RepositoryCommit:
Expand Down Expand Up @@ -192,7 +208,8 @@ def contains(self, commit: str, directory: Path) -> bool:
return Repo.descends(directory, commit, recorded.commit)

def installation(self, commits: tuple[str, ...], directory: Path) -> CommitInstallation:
installed = self.installed()
self.recorded()
installed = self.installed().recorded()
if self != installed:
raise ProofBuildMismatchError(
f"Proof build {self} differs from installed {installed}; run the proof again."
Expand Down
47 changes: 46 additions & 1 deletion src/agent_comms/child_process.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Identity-bound child ownership, process-group retirement and bounded runs.

Linux pidfd operations are lifted from compaction_child_watchdog. Process
Expand Down Expand Up @@ -39,7 +39,7 @@
from functools import cache, cached_property, partial
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import IO, TYPE_CHECKING, Annotated, Any, ClassVar, NoReturn, Self
from typing import IO, TYPE_CHECKING, Annotated, Any, BinaryIO, ClassVar, NoReturn, Self

from .nominal.declared_family import DeclaredFamily
from .errors import RelationViolationError
Expand Down Expand Up @@ -529,6 +529,28 @@
finally:
os.close(descriptor)

class NativeImage(ctypes.Structure):
"""The POSIX loader's native symbol-image ABI."""

_fields_ = (
("filename", ctypes.c_char_p),
("base", ctypes.c_void_p),
("symbol", ctypes.c_char_p),
("address", ctypes.c_void_p),
)

@contextmanager
def native_image(self, address: ctypes.c_void_p) -> Generator[BinaryIO]:
"""Open the native file providing a symbol; the caller consumes this handle."""
lookup = ctypes.CDLL(None).dladdr
lookup.argtypes = (ctypes.c_void_p, ctypes.POINTER(self.NativeImage))
lookup.restype = ctypes.c_int
image = self.NativeImage()
if not lookup(address, ctypes.byref(image)) or not image.filename:
raise ValueError("The loader did not identify the native symbol image")
with Path(os.fsdecode(image.filename)).resolve(strict=True).open("rb") as stream:
yield stream

@abstractmethod
def require_watched_procfs(self, refusal: Exception) -> None: ...

Expand Down Expand Up @@ -657,6 +679,29 @@


class LinuxPlatform(PidfdHandles, Platform):
@contextmanager
def native_image(self, address: ctypes.c_void_p) -> Generator[BinaryIO]:
# glibc's main-image filename aliases argv[0]; setproctitle changes it.
# Resolve the actual symbol mapping instead, including shared libpython.
pointer = address.value
if pointer is None:
raise ValueError("A null address has no native image")
with Path("/proc/self/maps").open() as mappings:
for line in mappings:
extent, _permissions, _offset, device, inode, *_name = line.split(maxsplit=5)
first, stop = (int(value, 16) for value in extent.split("-"))
if first <= pointer < stop:
link = Path("/proc/self/map_files") / f"{first:x}-{stop:x}"
with link.resolve(strict=True).open("rb") as stream:
observed = os.fstat(stream.fileno())
major, minor = (int(value, 16) for value in device.split(":"))
# Decode kernel metadata once at this native boundary.
if (observed.st_dev, observed.st_ino) != (os.makedev(major, minor), int(inode)):
raise ValueError("The native image file no longer matches its mapping")
yield stream
return
raise ValueError("The kernel did not identify the native symbol image")

def require_watched_procfs(self, refusal: Exception) -> None:
return None

Expand Down
54 changes: 54 additions & 0 deletions src/agent_comms/cli_verbs/prepare_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Prepare a functional candidate through the existing runtime/preparation owners."""
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Annotated

from agent_comms.cli_command import CliCommand
from agent_comms.cli_result import CliResult
from agent_comms.comms import Comms
from agent_comms.installer.commands import InstallCommands
from agent_comms.installer.runtime_stack import WheelRuntime
from agent_comms.launch_runtime import LaunchRuntime
from agent_comms.nominal.field_codec import Argument, FieldCodec, Normalize, PathText
from agent_comms.shell_effects import BuildStorageEffect, WriteStorageEffect
from agent_comms.storage_projection import StorageProjection
from agent_comms.tool_caller import ThreadCaller


@dataclass(frozen=True)
class PreparedRuntimeResult(CliResult):
prefix: Annotated[Path, PathText]


@dataclass(frozen=True, kw_only=True)
class PrepareRuntimeCliCommand(CliCommand, declared_name="prepare-runtime"):
help = ("Prepare a NEW owned functional target from an explicit interpreter, "
"recorded dependency runtime and exact local wheels; never publish it")
caller: Annotated[ThreadCaller, Normalize(FieldCodec.encode),
Argument(flags=("--from",), options={"type": ThreadCaller})]
into: Annotated[Path, PathText, Argument(flags=("--into",))]
interpreter: Annotated[Path, PathText, Argument(flags=("--python",))]
source_runtime: Annotated[Path, PathText, Argument(flags=("--source-runtime",))]
checkout: Annotated[Path, PathText, Argument(flags=("--checkout",))]
wheels: Annotated[tuple[Annotated[Path, PathText], ...],
Argument(flags=("--wheels",), options={"nargs": "+"})]

def apply(self, ctx: Comms) -> CliResult:
thread = ctx.registry.require(self.caller.own_thread())
prefix = self.into.expanduser().absolute()
checkout = self.checkout.expanduser().resolve()
projection = StorageProjection(ctx.registry.storage_rule, thread, ctx)
projection.check((BuildStorageEffect(), WriteStorageEffect(prefix),
WriteStorageEffect(checkout)))
with InstallCommands() as commands:
target = WheelRuntime(
checkout=checkout, commands=commands,
runtime=LaunchRuntime.at_prefix(prefix),
interpreter=LaunchRuntime.from_interpreter(self.interpreter.expanduser().absolute()),
source_runtime=LaunchRuntime.at_prefix(self.source_runtime.expanduser().resolve()),
wheels=tuple(path.expanduser().resolve(strict=True) for path in self.wheels),
)
target.prepare()
return PreparedRuntimeResult(prefix)
5 changes: 4 additions & 1 deletion src/agent_comms/commander/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -1509,7 +1509,10 @@ class InstallVerb(OrderVerb):
clears = (InstallDue,)

def run(self, ctx: Commander) -> None:
for due in InstallDue.due(ctx.comms, ProgressRead()):
pulls = ProgressRead()
InstallDue.prepare(ctx.comms, ctx.commander,
HeldOrders.read(ctx.comms, ctx.commander), pulls)
for due in InstallDue.due(ctx.comms, pulls):
print(f"{due.repo.to_wire()} main {due.main[:12]}: {due.launch(ctx.comms)}")


Expand Down
70 changes: 70 additions & 0 deletions src/agent_comms/installer/probes/interpreter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""The selected interpreter's observed native build, not its repository ancestry."""

from __future__ import annotations

import ctypes
from pathlib import Path
import sys
import sysconfig

from pydantic import BaseModel, ConfigDict, Field

from agent_comms.installer.interpreter_operation import RuntimeProbe, TargetProbe
from agent_comms.build_artifact import BuildArtifact
from agent_comms.child_process import Platform


class InterpreterBuild(BaseModel):
"""An existing build record without an interpreter observation is unqualified."""

model_config = ConfigDict(frozen=True, extra="forbid")

def __str__(self) -> str:
return "unrecorded interpreter"

def matches(self, actual: RecordedInterpreterBuild) -> bool:
"""Absent evidence cannot make a cached runtime eligible for reuse."""
return False

def require_recorded(self) -> RecordedInterpreterBuild:
raise ValueError("The runtime has no recorded interpreter build")


class RecordedInterpreterBuild(InterpreterBuild):
"""Native artifact identity, with the invoked environment kept separately."""

invoked: Path
version: str = Field(min_length=1)
abi: str = Field(min_length=1)
stdlib: Path
artifacts: tuple[BuildArtifact, ...] = Field(min_length=1)

def __str__(self) -> str:
images = ", ".join(f"{artifact.path} sha256={artifact.sha256}"
for artifact in self.artifacts)
return f"interpreter {self.invoked} [{self.abi}]: {images}"

def matches(self, actual: RecordedInterpreterBuild) -> bool:
return self == actual

def require_recorded(self) -> RecordedInterpreterBuild:
return self


class InterpreterBuildProbe(TargetProbe, RuntimeProbe[RecordedInterpreterBuild]):
def run(self) -> RecordedInterpreterBuild:
abi = sysconfig.get_config_var("SOABI")
if not isinstance(abi, str) or not abi:
raise ValueError("The selected interpreter does not declare its native ABI")
address = ctypes.cast(ctypes.pythonapi.Py_GetVersion, ctypes.c_void_p)
with Platform.current().native_image(address) as stream:
native = BuildArtifact.from_stream(stream)
executable = Path(sys.executable).resolve(strict=True)
images = {native.path: native}
if executable not in images:
images[executable] = BuildArtifact.capture(executable)
return RecordedInterpreterBuild(
invoked=Path(sys.executable), version=sys.version, abi=abi,
stdlib=Path(sysconfig.get_path("stdlib")).resolve(strict=True),
artifacts=tuple(images[path] for path in dict.fromkeys((executable, native.path))),
)
Loading
Loading