feat(observatory): add installed package dispatch and durable publication

Checkpoint existing backend lifecycle changes. Focused verification found nine legacy fixture failures in portable LAB V1 executor/runtime tests; repair follows separately without rewriting this snapshot. ADR date retains its intentional Markdown hard break.
This commit is contained in:
DCCONSTRUCTIONS
2026-09-02 00:58:37 +03:00
parent d655d6998d
commit a945d665dd
47 changed files with 6008 additions and 1034 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,6 +5,11 @@ Status: accepted; portable LAB V1 admission and Worker pull foundations implemen
production executor, claim lease, result publication, Worker deployment and
live-trigger wiring pending
Amended on 2026-09-01 by ADR 0048. Capability-aware claims, renewable leases,
verified result transport and the durable publication lifecycle are now
implemented; physical Worker installation and live-trigger wiring remain outside
that source increment.
## Context
Observatory must let an operator apply an admitted laboratory setup to a saved
@@ -4,6 +4,11 @@ Date: 2026-08-31
Status: accepted as a backend foundation; production validators, transport and
executor wiring remain blocked
Amended on 2026-09-01 by ADR 0048. Exact built-in validators, package transport,
application publication wiring, durable publication status and a typed portable
result viewer are now implemented. Installed generic package executors and a
physical Worker 006 cutover remain separate acceptance work.
## Context
The durable Observatory queue deliberately treats a Worker `succeed` call as a
@@ -0,0 +1,138 @@
# ADR 0048: Independent installed LAB packages and portable result lifecycle
Date: 2026-09-01
Status: accepted and implemented as source contracts; no Worker deployment or image migration
## Context
The first portable Observatory path still composed M4.9 and LAB V1 through
profile-specific builders and a combined Worker release. A Worker could claim a
job whose exact executor was not installed, execution success could become
terminal before catalog publication, and the UI could open only one hard-coded
legacy replay family. Adding another LAB therefore still risked changes across
the Worker coordinator, backend publication and frontend viewer.
The target remains observation-only. This decision does not authorize a build,
deployment, Docker installation, Worker 006 mutation, K1 command or safety use.
## Decision
### Installed package boundary
A Worker-local `InstalledLabPackageRegistry` binds one independently installed
package to the exact RunDefinition, RuntimeCandidate and four-digest executor
identity. Its manifest contains only reviewed container images, argv, dependency
topology, fixed in-container mount targets and immutable asset IDs. It cannot
contain a host path, secret, environment, Docker socket or job-provided command.
Every package uses one stable container I/O contract:
- read-only source at `/missioncore/input/source`;
- read-only canonical plan at `/missioncore/input/run-plan.json`;
- result package at `/missioncore/output`;
- ephemeral work at `/missioncore/work`.
`single-container` and `fixed-stack` are package properties. Exactly one
container owns the portable result. All images must already be admitted by the
bound RuntimeCandidate, and the package asset inventory must exactly equal that
candidate's reusable asset inventory.
Worker composition now exposes one generic package executor factory. A Worker
may install any non-empty subset of server-ready definitions. It advertises only
the resulting four-digest identities; it is not required to implement every LAB
known by the backend. Existing profile-specific builders remain a compatibility
path until their images adopt the common package I/O contract.
The generic executor runs a deterministic dependency graph of hardened one-shot
containers through the local Docker Engine API. Every container is pinned by
image SHA-256, has no network, a read-only root filesystem, no Linux
capabilities or privilege escalation, and receives only the declared read-only
inputs plus the single shared result output. Memory, CPU, PID, shared-memory,
tmpfs, GPU-count and timeout limits are explicit package fields. Exactly one
`result-writer` step must transitively depend on every compute step, after which
the Worker verifies the declared result manifest, file lengths, digests and the
absence of links or undeclared files before publication can begin.
Worker-local asset bindings translate reviewed controller paths to Docker-host
paths. Those host paths never enter the package manifest or queued job. The
current package contract intentionally admits ordered offline steps only; a
future LAB that genuinely requires simultaneous services needs an explicit new
contract instead of silently weakening the isolation boundary.
### Capability-aware dispatch
Worker claim protocol v2 sends a bounded, canonical snapshot of installed
four-digest executor identities. The queue selects the oldest queued job that
matches one of those identities inside the same transaction that creates the
claim. An empty snapshot claims nothing. The snapshot is included in the claim
request digest, so an idempotency key cannot be replayed with different Worker
capabilities.
Claim v1 remains accepted only as a rolling compatibility path and cannot send a
capability field. New Worker code always uses v2.
### Execution and publication are separate durable lifecycles
Verified upload completion atomically seals execution as `succeeded` and creates
a publication outbox entry. Publication has its own state:
- `not-required` for the compatibility path;
- `pending` after verified execution completion;
- `failed` with bounded error evidence and attempt count;
- `published` with timestamp.
A publisher failure no longer rewrites or loses the successful execution. The
authenticated Worker API returns the durable job with HTTP 202 and exposes an
idempotent publication retry endpoint. Exact retries reuse content-addressed
artifacts and the immutable SessionStore projection; they do not rerun the model.
### Contract-driven result viewing
Portable publications receive an explicit v2 viewer capability
`portable-result-review / portable-result / result-defined`. The public viewer
service resolves that capability, immutable publication provenance and central
artifact manifest, then rechecks the JSON result document's length and SHA-256.
The Control Station selects either the legacy canonical replay adapter or the
portable result adapter from the typed capability. No portable setup ID, LAB ID,
source session ID or result-name family selects the viewer.
The UI polls while execution or publication is pending, refreshes the catalog
after publication, and keeps legacy and portable setup catalogs independently
usable if either endpoint is temporarily unavailable. Catalog merge identity is
`setup_id`, never a display name.
### Validator extension
Result validators are registered and selected by exact result-contract SHA-256.
The built-in LAB V1 and M4.9 functions remain compatibility registrations. The
composition core no longer requires both setup IDs or branches on them, so a
future contract can supply another server-owned registration without changing
the queue or Worker router.
## Compatibility and migration boundary
This source increment does not claim that the currently installed M4.9 or LAB V1
images implement the new common container I/O surface. No synthetic package
manifest is checked in for an image that has not been rebuilt and smoked against
that surface. The old builders continue to work, and M4.9 startup no longer
requires LAB V1 receipt/release environment values.
The next physical migration is deliberately per profile:
1. make one image emit the existing portable result package through the common I/O contract;
2. seal its independent installed package manifest and local asset bindings;
3. run contract composition and a short offline smoke;
4. advertise only that executor identity in claim v2;
5. perform a reversible canary without changing another profile.
## Consequences
- A Worker cannot take an unsupported job merely because it is earlier in the queue.
- Adding a conforming LAB changes its definition, runtime candidate, validator
registration and installed package, not the queue protocol or frontend routing.
- Successful compute and successful catalog publication are both visible and
recoverable facts.
- Portable result review is generic JSON/artifact evidence; richer visual viewers
can be added as new typed capabilities without adding setup-name conditionals.
- The generic package boundary is implemented, while image conformance and
physical Worker 006 acceptance remain explicit, unclaimed work.
@@ -0,0 +1,912 @@
"""Generic Worker-local launcher for independently installed LAB packages.
The backend supplies only a sealed job identity. Reviewed local installation
bindings translate controller paths to Docker-host paths, while the package
owns the exact image, argv, dependency order, resource limits, and mount
targets. Every container is a one-shot step and is removed by its exact
Docker container id after completion.
"""
from __future__ import annotations
import hashlib
import os
import re
import secrets
import shutil
import stat
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
from typing import TYPE_CHECKING, Final, Protocol
import httpx
from k1link.observatory.installed_lab_packages import (
INSTALLED_LAB_PLAN_PATH,
INSTALLED_LAB_RESULT_ROOT,
INSTALLED_LAB_SOURCE_ROOT,
INSTALLED_LAB_STEP_INPUT_ROOT,
INSTALLED_LAB_WORK_ROOT,
InstalledLabContainer,
InstalledLabPackage,
)
from k1link.observatory.portable_result_contract import (
RESULT_PACKAGE_MANIFEST_NAME,
PortableResultPackageIntegrityError,
PortableResultPackageManifest,
canonical_json,
relative_artifact_path,
)
from k1link.observatory.portable_run_definitions import PortableRunDefinition
from k1link.observatory.portable_worker_runtime import (
PortableWorkerExecutorAdapter,
PortableWorkerLocalAssetBinding,
PortableWorkerResultDraft,
PortableWorkerRuntimePlan,
PortableWorkerSourceStage,
inspect_runtime_candidate,
)
from k1link.observatory.worker_agent import ObservatoryWorkerExecutorRegistration
if TYPE_CHECKING:
from k1link.observatory.worker_service import (
ObservatoryWorkerPackageExecutorBuildContext,
)
INSTALLED_LAB_RUN_PLAN_SCHEMA: Final = "missioncore.observatory-installed-lab-run-plan/v1"
_DOCKER_SOCKET: Final = Path("/var/run/docker.sock")
_DOCKER_API_VERSION: Final = "v1.47"
_MAX_ENGINE_RESPONSE_BYTES: Final = 1024 * 1024
_MAX_RESULT_MANIFEST_BYTES: Final = 1024 * 1024
_CONTAINER_ID: Final = re.compile(r"^[a-f0-9]{12,128}$")
_ASSET_ID: Final = re.compile(r"^[a-z][a-z0-9.-]{2,127}$")
_SHA256: Final = re.compile(r"^[a-f0-9]{64}$")
_WINDOWS_ABSOLUTE_PATH: Final = re.compile(r"^[A-Za-z]:\\")
_ENGINE_PATH_PLACEHOLDER: Final = re.compile(
r"(?:\$[A-Za-z_][A-Za-z0-9_]*|%[A-Za-z_][A-Za-z0-9_]*%|"
r"\{[A-Za-z_][A-Za-z0-9_]*\})"
)
class InstalledLabPackageRunnerError(RuntimeError):
"""The generic installed-package runtime changed or failed closed."""
@dataclass(frozen=True, slots=True)
class InstalledLabLocalAssetBinding:
"""Reviewed local locator; neither path is serialized into a queued job."""
asset_id: str
controller_path: Path | None = None
engine_path: str | None = None
image_sha256: str | None = None
def __post_init__(self) -> None:
if _ASSET_ID.fullmatch(self.asset_id) is None:
raise ValueError("installed LAB local asset id is invalid")
file_binding = self.controller_path is not None or self.engine_path is not None
image_binding = self.image_sha256 is not None
if file_binding == image_binding:
raise ValueError("installed LAB local asset locator is ambiguous")
if file_binding:
if self.controller_path is None or self.engine_path is None:
raise ValueError("installed LAB file asset binding is incomplete")
_absolute_controller_path(self.controller_path, "installed LAB asset")
_engine_host_path(self.engine_path, "installed LAB asset")
elif self.image_sha256 is None or _SHA256.fullmatch(self.image_sha256) is None:
raise ValueError("installed LAB image asset digest is invalid")
def portable_binding(self) -> PortableWorkerLocalAssetBinding:
if self.controller_path is not None:
return PortableWorkerLocalAssetBinding(
asset_id=self.asset_id,
file_path=self.controller_path,
)
return PortableWorkerLocalAssetBinding(
asset_id=self.asset_id,
image_sha256=self.image_sha256,
)
@dataclass(frozen=True, slots=True)
class InstalledLabDockerMount:
engine_path: str
container_path: str
read_only: bool
def __post_init__(self) -> None:
_engine_host_path(self.engine_path, "installed LAB Docker mount")
target = PurePosixPath(self.container_path)
if not target.is_absolute() or ".." in target.parts:
raise InstalledLabPackageRunnerError("Docker mount target is unsafe")
if not isinstance(self.read_only, bool):
raise InstalledLabPackageRunnerError("Docker mount mode is invalid")
def engine_document(self) -> dict[str, object]:
return {
"Type": "bind",
"Source": self.engine_path,
"Target": self.container_path,
"ReadOnly": self.read_only,
"BindOptions": {"Propagation": "rprivate"},
}
@dataclass(frozen=True, slots=True)
class InstalledLabDockerLaunch:
package_id: str
container: InstalledLabContainer
mounts: tuple[InstalledLabDockerMount, ...]
labels: Mapping[str, str]
name_token: str
def __post_init__(self) -> None:
targets = tuple(mount.container_path for mount in self.mounts)
if targets != tuple(sorted(targets)) or len(targets) != len(set(targets)):
raise InstalledLabPackageRunnerError("Docker mounts are not canonical")
writable = tuple(mount for mount in self.mounts if not mount.read_only)
if len(writable) != 1 or writable[0].container_path != INSTALLED_LAB_RESULT_ROOT:
raise InstalledLabPackageRunnerError(
"Docker package must expose exactly one writable result mount"
)
if not re.fullmatch(r"[a-f0-9]{16}", self.name_token):
raise InstalledLabPackageRunnerError("Docker launch token is invalid")
required_labels = {
"com.nodedc.authority",
"com.nodedc.component",
"com.nodedc.definition-sha256",
"com.nodedc.job-id",
"com.nodedc.managed-by",
"com.nodedc.package-sha256",
"com.nodedc.product",
"com.nodedc.stack",
}
if set(self.labels) != required_labels:
raise InstalledLabPackageRunnerError("Docker launch label set changed")
if (
self.labels["com.nodedc.authority"] != "observation-only"
or self.labels["com.nodedc.component"] != self.container.container_id
or self.labels["com.nodedc.managed-by"] != "mission-core-worker"
or self.labels["com.nodedc.product"] != "mission-core"
or self.labels["com.nodedc.stack"] != "observatory"
):
raise InstalledLabPackageRunnerError("Docker launch labels changed")
for key in ("com.nodedc.definition-sha256", "com.nodedc.package-sha256"):
if _SHA256.fullmatch(self.labels[key]) is None:
raise InstalledLabPackageRunnerError("Docker launch digest label is invalid")
class InstalledLabContainerLauncher(Protocol):
def __call__(self, launch: InstalledLabDockerLaunch) -> None: ...
@dataclass(frozen=True, slots=True)
class DockerEngineInstalledLabLauncher:
"""Execute one hardened one-shot package step through the local Engine."""
socket_path: Path = _DOCKER_SOCKET
api_version: str = _DOCKER_API_VERSION
transport_factory: Callable[[], httpx.BaseTransport] | None = None
def __post_init__(self) -> None:
if not self.socket_path.is_absolute():
raise InstalledLabPackageRunnerError("Docker socket path is not absolute")
if re.fullmatch(r"v[0-9]+\.[0-9]+", self.api_version) is None:
raise InstalledLabPackageRunnerError("Docker API version is invalid")
def __call__(self, launch: InstalledLabDockerLaunch) -> None:
if self.transport_factory is None:
_require_local_socket(self.socket_path)
transport: httpx.BaseTransport = httpx.HTTPTransport(uds=str(self.socket_path))
else:
transport = self.transport_factory()
container_id: str | None = None
primary_error: BaseException | None = None
try:
with httpx.Client(
base_url="http://docker",
transport=transport,
timeout=httpx.Timeout(launch.container.timeout_seconds, connect=5.0),
) as client:
self._verify_image(client, launch.container.image_sha256)
container_id = self._create(client, launch)
self._empty(
client,
"POST",
self._api_path(f"/containers/{container_id}/start"),
{204},
)
status_code = self._wait(client, container_id)
if status_code != 0:
log_sha256, log_bytes = self._log_identity(client, container_id)
raise InstalledLabPackageRunnerError(
f"package step {launch.container.container_id} exited with status "
f"{status_code}; logs={log_sha256}:{log_bytes}"
)
except (httpx.HTTPError, OSError, ValueError) as exc:
primary_error = exc
raise InstalledLabPackageRunnerError(
f"local Docker Engine step {launch.container.container_id} failed"
) from exc
except BaseException as exc:
primary_error = exc
raise
finally:
if container_id is not None:
cleanup_error = self._cleanup(container_id)
if cleanup_error is not None:
raise InstalledLabPackageRunnerError(
"Docker package container cleanup failed after retry"
) from (primary_error or cleanup_error)
def verify_images(self, image_sha256s: tuple[str, ...]) -> None:
"""Prove a canonical installed image inventory without creating a container."""
if (
not image_sha256s
or image_sha256s != tuple(sorted(image_sha256s))
or len(image_sha256s) != len(set(image_sha256s))
or any(_SHA256.fullmatch(value) is None for value in image_sha256s)
):
raise InstalledLabPackageRunnerError(
"installed LAB image inventory is invalid"
)
if self.transport_factory is None:
_require_local_socket(self.socket_path)
transport: httpx.BaseTransport = httpx.HTTPTransport(
uds=str(self.socket_path)
)
else:
transport = self.transport_factory()
with httpx.Client(
base_url="http://docker",
transport=transport,
timeout=httpx.Timeout(10.0, connect=5.0),
) as client:
for image_sha256 in image_sha256s:
self._verify_image(client, image_sha256)
def _verify_image(self, client: httpx.Client, image_sha256: str) -> None:
response = self._response(
client,
"GET",
self._api_path(f"/images/sha256:{image_sha256}/json"),
{200},
)
document = _response_object(response, "Docker image inspection")
if document.get("Id") != f"sha256:{image_sha256}":
raise InstalledLabPackageRunnerError("Docker image identity changed")
def _create(self, client: httpx.Client, launch: InstalledLabDockerLaunch) -> str:
component = launch.container.container_id[:32]
name = f"ndc-observatory-{component}-{launch.name_token}"
response = self._response(
client,
"POST",
self._api_path(f"/containers/create?name={name}"),
{201},
json_body=_container_create_document(launch),
)
document = _response_object(response, "Docker container creation")
container_id = document.get("Id")
if not isinstance(container_id, str) or _CONTAINER_ID.fullmatch(container_id) is None:
raise InstalledLabPackageRunnerError("Docker container id is invalid")
if document.get("Warnings") not in (None, []):
raise InstalledLabPackageRunnerError("Docker container creation returned warnings")
return container_id
def _cleanup(self, container_id: str) -> BaseException | None:
last_error: BaseException | None = None
for _attempt in range(2):
try:
transport: httpx.BaseTransport = (
httpx.HTTPTransport(uds=str(self.socket_path))
if self.transport_factory is None
else self.transport_factory()
)
with httpx.Client(
base_url="http://docker",
transport=transport,
timeout=httpx.Timeout(10.0, connect=5.0),
) as client:
self._empty(
client,
"DELETE",
self._api_path(f"/containers/{container_id}?force=1&v=1"),
{204, 404},
)
return None
except (httpx.HTTPError, OSError, ValueError, InstalledLabPackageRunnerError) as exc:
last_error = exc
return last_error
def _wait(self, client: httpx.Client, container_id: str) -> int:
response = self._response(
client,
"POST",
self._api_path(f"/containers/{container_id}/wait?condition=not-running"),
{200},
)
document = _response_object(response, "Docker container wait")
status_code = document.get("StatusCode")
if isinstance(status_code, bool) or not isinstance(status_code, int):
raise InstalledLabPackageRunnerError("Docker exit status is invalid")
if document.get("Error") not in (None, {"Message": ""}):
raise InstalledLabPackageRunnerError("Docker wait returned an Engine error")
return status_code
def _log_identity(self, client: httpx.Client, container_id: str) -> tuple[str, int]:
response = self._response(
client,
"GET",
self._api_path(f"/containers/{container_id}/logs?stdout=1&stderr=1&tail=200"),
{200},
)
return hashlib.sha256(response.content).hexdigest(), len(response.content)
def _api_path(self, path: str) -> str:
return f"/{self.api_version}{path}"
@staticmethod
def _empty(
client: httpx.Client,
method: str,
path: str,
statuses: set[int],
) -> None:
DockerEngineInstalledLabLauncher._response(client, method, path, statuses)
@staticmethod
def _response(
client: httpx.Client,
method: str,
path: str,
statuses: set[int],
*,
json_body: Mapping[str, object] | None = None,
) -> httpx.Response:
with client.stream(method, path, json=json_body) as streamed:
if streamed.status_code not in statuses:
raise InstalledLabPackageRunnerError(
f"Docker Engine rejected {method} {path.split('?')[0]} "
f"with status {streamed.status_code}"
)
declared_length = streamed.headers.get("content-length")
if declared_length is not None:
try:
length = int(declared_length)
except ValueError as exc:
raise InstalledLabPackageRunnerError(
"Docker Engine response length is invalid"
) from exc
if length < 0 or length > _MAX_ENGINE_RESPONSE_BYTES:
raise InstalledLabPackageRunnerError("Docker Engine response is too large")
payload = bytearray()
blocks = (
(streamed.content,)
if streamed.is_stream_consumed
else streamed.iter_raw(chunk_size=16 * 1024)
)
for block in blocks:
if len(payload) + len(block) > _MAX_ENGINE_RESPONSE_BYTES:
raise InstalledLabPackageRunnerError("Docker Engine response is too large")
payload.extend(block)
return httpx.Response(
status_code=streamed.status_code,
headers=streamed.headers,
content=bytes(payload),
request=streamed.request,
)
@dataclass(frozen=True, slots=True)
class InstalledLabPackageProfileRunner:
package: InstalledLabPackage
definition: PortableRunDefinition
controller_work_root: Path
engine_work_root: str
local_assets: tuple[InstalledLabLocalAssetBinding, ...]
launcher: InstalledLabContainerLauncher
token_factory: Callable[[], str] = lambda: secrets.token_hex(8)
def __post_init__(self) -> None:
_absolute_controller_path(self.controller_work_root, "Worker package root")
_engine_host_path(self.engine_work_root, "Worker package root")
asset_ids = tuple(binding.asset_id for binding in self.local_assets)
if asset_ids != tuple(sorted(asset_ids)) or len(asset_ids) != len(set(asset_ids)):
raise InstalledLabPackageRunnerError(
"installed LAB local assets must be canonical and unique"
)
def run(
self,
plan: PortableWorkerRuntimePlan,
source: PortableWorkerSourceStage,
) -> PortableWorkerResultDraft:
if (
plan.setup_id != self.package.setup_id
or plan.definition_sha256 != self.package.definition_sha256
or plan.candidate_sha256 != self.package.runtime_candidate_sha256
or plan.result_contract_sha256 != self.package.result_contract_sha256
or plan.source_bundle_sha256 != source.source_bundle_sha256
or plan.source_capability_manifest_sha256 != source.source_capability_manifest_sha256
):
raise InstalledLabPackageRunnerError(
"installed LAB run plan differs from its package or source"
)
controller_root = _real_directory(
self.controller_work_root,
"Worker package root",
)
source_root = _real_directory(source.root, "installed LAB source root")
_require_descendant(source_root, controller_root, "installed LAB source root")
jobs_root = controller_root / "jobs"
jobs_root.mkdir(mode=0o700, parents=True, exist_ok=True)
attempt_token = self.token_factory()
if re.fullmatch(r"[a-f0-9]{16}", attempt_token) is None:
raise InstalledLabPackageRunnerError("installed LAB attempt token is invalid")
job_root = jobs_root / plan.job_id / attempt_token
try:
job_root.mkdir(mode=0o700, parents=True, exist_ok=False)
output_root = job_root / "result-staging"
output_root.mkdir(mode=0o700)
steps_root = job_root / "steps"
steps_root.mkdir(mode=0o700)
plan_path = job_root / "run-plan.json"
_write_exclusive(
plan_path,
canonical_json(
{
"schema_version": INSTALLED_LAB_RUN_PLAN_SCHEMA,
"runtime_plan": plan.as_dict(),
"package_id": self.package.package_id,
"package_sha256": self.package.package_sha256,
"authority": self.definition.authority.as_dict(),
}
),
)
for container in _topological_containers(self.package):
container_output_root = output_root
if container.role == "step":
container_output_root = steps_root / container.container_id
container_output_root.mkdir(mode=0o700)
self.launcher(
self._launch(
container=container,
plan=plan,
source_root=source_root,
plan_path=plan_path,
output_root=container_output_root,
steps_root=steps_root,
name_token=attempt_token,
)
)
return _read_result_draft(
output_root,
plan=plan,
definition=self.definition,
)
except FileExistsError as exc:
raise InstalledLabPackageRunnerError(
"installed LAB attempt directory already exists"
) from exc
except OSError as exc:
shutil.rmtree(job_root, ignore_errors=True)
raise InstalledLabPackageRunnerError(
"installed LAB job workspace is unavailable"
) from exc
except BaseException:
shutil.rmtree(job_root, ignore_errors=True)
raise
def _launch(
self,
*,
container: InstalledLabContainer,
plan: PortableWorkerRuntimePlan,
source_root: Path,
plan_path: Path,
output_root: Path,
steps_root: Path,
name_token: str,
) -> InstalledLabDockerLaunch:
by_asset = {binding.asset_id: binding for binding in self.local_assets}
mounts = [
InstalledLabDockerMount(
_translate_work_path(
source_root,
controller_root=self.controller_work_root,
engine_root=self.engine_work_root,
),
INSTALLED_LAB_SOURCE_ROOT,
True,
),
InstalledLabDockerMount(
_translate_work_path(
plan_path,
controller_root=self.controller_work_root,
engine_root=self.engine_work_root,
),
INSTALLED_LAB_PLAN_PATH,
True,
),
InstalledLabDockerMount(
_translate_work_path(
output_root,
controller_root=self.controller_work_root,
engine_root=self.engine_work_root,
),
INSTALLED_LAB_RESULT_ROOT,
False,
),
]
for dependency in _container_ancestors(self.package, container.container_id):
dependency_root = _real_directory(
steps_root / dependency,
"installed LAB dependency output",
)
mounts.append(
InstalledLabDockerMount(
_translate_work_path(
dependency_root,
controller_root=self.controller_work_root,
engine_root=self.engine_work_root,
),
f"{INSTALLED_LAB_STEP_INPUT_ROOT}/{dependency}",
True,
)
)
for package_mount in container.mounts:
binding = by_asset.get(package_mount.asset_id)
if binding is None or binding.engine_path is None:
raise InstalledLabPackageRunnerError(
"package file mount has no reviewed local binding"
)
mounts.append(
InstalledLabDockerMount(
binding.engine_path,
package_mount.target,
True,
)
)
return InstalledLabDockerLaunch(
package_id=self.package.package_id,
container=container,
mounts=tuple(sorted(mounts, key=lambda item: item.container_path)),
labels={
"com.nodedc.authority": "observation-only",
"com.nodedc.component": container.container_id,
"com.nodedc.definition-sha256": plan.definition_sha256,
"com.nodedc.job-id": plan.job_id,
"com.nodedc.managed-by": "mission-core-worker",
"com.nodedc.package-sha256": self.package.package_sha256,
"com.nodedc.product": "mission-core",
"com.nodedc.stack": "observatory",
},
name_token=name_token,
)
@dataclass(frozen=True, slots=True)
class InstalledLabPackageExecutorFactory:
"""One generic factory for every independently installed package."""
local_assets: tuple[InstalledLabLocalAssetBinding, ...]
engine_work_root: str
launcher: InstalledLabContainerLauncher = DockerEngineInstalledLabLauncher()
def __post_init__(self) -> None:
asset_ids = tuple(binding.asset_id for binding in self.local_assets)
if asset_ids != tuple(sorted(asset_ids)) or len(asset_ids) != len(set(asset_ids)):
raise InstalledLabPackageRunnerError(
"installed LAB factory assets must be canonical and unique"
)
_engine_host_path(self.engine_work_root, "Worker Engine work root")
def __call__(
self,
context: ObservatoryWorkerPackageExecutorBuildContext,
) -> ObservatoryWorkerExecutorRegistration:
context.package.bind(context.definition, context.candidate)
by_asset = {binding.asset_id: binding for binding in self.local_assets}
package_assets = tuple(
by_asset[asset_id] for asset_id in context.package.asset_ids if asset_id in by_asset
)
if tuple(binding.asset_id for binding in package_assets) != context.package.asset_ids:
raise InstalledLabPackageRunnerError(
"installed LAB package has an incomplete local asset map"
)
admission = inspect_runtime_candidate(
context.candidate,
{binding.asset_id: binding.portable_binding() for binding in package_assets},
)
if not admission.ready:
blockers = ",".join(admission.blockers)
raise InstalledLabPackageRunnerError(
f"installed LAB package local assets are not admitted: {blockers}"
)
runner = InstalledLabPackageProfileRunner(
package=context.package,
definition=context.definition,
controller_work_root=context.work_root,
engine_work_root=self.engine_work_root,
local_assets=package_assets,
launcher=self.launcher,
)
adapter = PortableWorkerExecutorAdapter(
candidate=context.candidate,
definition=context.definition,
admission=admission,
source_materializer=context.source_transport,
runner=runner,
publisher=context.result_transport,
)
return ObservatoryWorkerExecutorRegistration(
identity=context.package.executor_identity,
adapter=adapter,
)
def _topological_containers(
package: InstalledLabPackage,
) -> tuple[InstalledLabContainer, ...]:
remaining = {container.container_id: container for container in package.containers}
completed: set[str] = set()
ordered: list[InstalledLabContainer] = []
while remaining:
ready = tuple(
container
for container in remaining.values()
if set(container.depends_on).issubset(completed)
)
if not ready:
raise InstalledLabPackageRunnerError("package container topology is blocked")
for container in sorted(ready, key=lambda item: item.container_id):
ordered.append(container)
completed.add(container.container_id)
del remaining[container.container_id]
return tuple(ordered)
def _container_ancestors(
package: InstalledLabPackage,
container_id: str,
) -> tuple[str, ...]:
by_id = {container.container_id: container for container in package.containers}
ancestors: set[str] = set()
def collect(current: str) -> None:
for dependency in by_id[current].depends_on:
if dependency not in ancestors:
ancestors.add(dependency)
collect(dependency)
collect(container_id)
return tuple(sorted(ancestors))
def _container_create_document(launch: InstalledLabDockerLaunch) -> dict[str, object]:
container = launch.container
device_requests: list[dict[str, object]] = []
if container.gpu_count:
device_requests.append(
{
"Driver": "nvidia",
"Count": container.gpu_count,
"Capabilities": [["gpu"]],
}
)
return {
"Image": f"sha256:{container.image_sha256}",
"Cmd": list(container.argv),
"WorkingDir": INSTALLED_LAB_WORK_ROOT,
"Env": [
"HF_HUB_OFFLINE=1",
"TRANSFORMERS_OFFLINE=1",
"PYTHONDONTWRITEBYTECODE=1",
],
"Labels": dict(sorted(launch.labels.items())),
"NetworkDisabled": True,
"OpenStdin": False,
"StdinOnce": False,
"Tty": False,
"AttachStdout": True,
"AttachStderr": True,
"HostConfig": {
"AutoRemove": False,
"CapDrop": ["ALL"],
"DeviceRequests": device_requests,
"Init": True,
"IpcMode": "private",
"Memory": container.memory_bytes,
"MemorySwap": container.memory_bytes,
"Mounts": [mount.engine_document() for mount in launch.mounts],
"NanoCpus": container.nano_cpus,
"NetworkMode": "none",
"PidsLimit": container.pids_limit,
"Privileged": False,
"ReadonlyRootfs": True,
"SecurityOpt": ["no-new-privileges:true"],
"ShmSize": container.shm_bytes,
"Tmpfs": {
INSTALLED_LAB_WORK_ROOT: (
f"rw,noexec,nosuid,nodev,size={container.tmpfs_bytes},mode=1777"
),
"/tmp": "rw,noexec,nosuid,nodev,size=67108864,mode=1777",
},
},
}
def _read_result_draft(
root: Path,
*,
plan: PortableWorkerRuntimePlan,
definition: PortableRunDefinition,
) -> PortableWorkerResultDraft:
result_root = _real_directory(root, "installed LAB result root")
manifest_path = result_root / RESULT_PACKAGE_MANIFEST_NAME
try:
metadata = manifest_path.lstat()
if not stat.S_ISREG(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode):
raise InstalledLabPackageRunnerError(
"installed LAB result manifest is not a regular file"
)
if not 0 < metadata.st_size <= _MAX_RESULT_MANIFEST_BYTES:
raise InstalledLabPackageRunnerError("installed LAB result manifest size is invalid")
payload = manifest_path.read_bytes()
manifest = PortableResultPackageManifest.from_bytes(payload)
except InstalledLabPackageRunnerError:
raise
except (OSError, PortableResultPackageIntegrityError) as exc:
raise InstalledLabPackageRunnerError("installed LAB result manifest is invalid") from exc
source = manifest.source
run_definition = manifest.run_definition
result = manifest.result
if (
manifest.job.get("job_id") != plan.job_id
or source.get("bundle_sha256") != plan.source_bundle_sha256
or source.get("capability_manifest_sha256") != plan.source_capability_manifest_sha256
or run_definition.get("definition_sha256") != plan.definition_sha256
or result.get("result_contract_sha256") != plan.result_contract_sha256
or result.get("result_schema") != definition.result_contract.result_schema
or result.get("result_kind") != definition.result_contract.result_kind
or manifest.authority != definition.authority.as_dict()
):
raise InstalledLabPackageRunnerError(
"installed LAB result package lost its sealed identity"
)
result_id = result.get("result_id")
if not isinstance(result_id, str):
raise InstalledLabPackageRunnerError("installed LAB result id is invalid")
expected_files = {RESULT_PACKAGE_MANIFEST_NAME}
for artifact in manifest.artifacts:
relative = relative_artifact_path(artifact.relative_path)
artifact_path = result_root.joinpath(*relative.parts)
try:
metadata = artifact_path.lstat()
if not stat.S_ISREG(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode):
raise InstalledLabPackageRunnerError(
"installed LAB result artifact is not a regular file"
)
if metadata.st_size != artifact.byte_length:
raise InstalledLabPackageRunnerError("installed LAB result artifact size changed")
if _sha256_file(artifact_path) != artifact.sha256:
raise InstalledLabPackageRunnerError("installed LAB result artifact digest changed")
except OSError as exc:
raise InstalledLabPackageRunnerError(
"installed LAB result artifact is unavailable"
) from exc
expected_files.add(relative.as_posix())
actual_files: set[str] = set()
for candidate in result_root.rglob("*"):
relative_name = candidate.relative_to(result_root).as_posix()
metadata = candidate.lstat()
if stat.S_ISLNK(metadata.st_mode):
raise InstalledLabPackageRunnerError("installed LAB result contains a link")
if stat.S_ISREG(metadata.st_mode):
actual_files.add(relative_name)
elif not stat.S_ISDIR(metadata.st_mode):
raise InstalledLabPackageRunnerError("installed LAB result contains a special file")
if actual_files != expected_files:
raise InstalledLabPackageRunnerError("installed LAB result contains undeclared files")
return PortableWorkerResultDraft(
root=result_root,
result_id=result_id,
result_sha256=manifest.manifest_sha256,
result_contract_sha256=plan.result_contract_sha256,
)
def _translate_work_path(
path: Path,
*,
controller_root: Path,
engine_root: str,
) -> str:
candidate = path.expanduser().absolute()
root = controller_root.expanduser().absolute()
try:
relative = candidate.relative_to(root)
except ValueError as exc:
raise InstalledLabPackageRunnerError(
"Worker path is outside the Docker-host work binding"
) from exc
if _WINDOWS_ABSOLUTE_PATH.match(engine_root):
suffix = "\\".join(relative.parts)
return engine_root.rstrip("\\") + (f"\\{suffix}" if suffix else "")
suffix = "/".join(relative.parts)
return engine_root.rstrip("/") + (f"/{suffix}" if suffix else "")
def _require_descendant(path: Path, root: Path, label: str) -> None:
if path == root or not path.is_relative_to(root):
raise InstalledLabPackageRunnerError(f"{label} is outside the Worker package root")
def _absolute_controller_path(path: Path, label: str) -> None:
if not path.is_absolute():
raise ValueError(f"{label} controller path must be absolute")
def _engine_host_path(value: str, label: str) -> None:
if (
not isinstance(value, str)
or not value
or value != value.strip()
or "\x00" in value
or "\n" in value
or "\r" in value
or _ENGINE_PATH_PLACEHOLDER.search(value) is not None
or ".." in PurePosixPath(value.replace("\\", "/")).parts
or not (value.startswith("/") or _WINDOWS_ABSOLUTE_PATH.match(value))
):
raise ValueError(f"{label} Engine path is invalid")
def _real_directory(path: Path, label: str) -> Path:
candidate = path.expanduser().absolute()
try:
metadata = candidate.lstat()
resolved = candidate.resolve(strict=True)
except OSError as exc:
raise InstalledLabPackageRunnerError(f"{label} is unavailable") from exc
if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):
raise InstalledLabPackageRunnerError(f"{label} is not a real directory")
return resolved
def _write_exclusive(path: Path, payload: bytes) -> None:
with path.open("xb") as stream:
stream.write(payload)
stream.flush()
os.fsync(stream.fileno())
def _sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _response_object(response: httpx.Response, label: str) -> dict[str, object]:
try:
document: object = response.json()
except ValueError as exc:
raise InstalledLabPackageRunnerError(f"{label} is not JSON") from exc
if not isinstance(document, dict) or any(not isinstance(key, str) for key in document):
raise InstalledLabPackageRunnerError(f"{label} is not an object")
return document
def _require_local_socket(path: Path) -> None:
try:
metadata = path.stat()
except OSError as exc:
raise InstalledLabPackageRunnerError("local Docker socket is unavailable") from exc
if not stat.S_ISSOCK(metadata.st_mode):
raise InstalledLabPackageRunnerError("local Docker socket is not a socket")
@@ -0,0 +1,721 @@
"""Worker-local, content-addressed execution packages for portable LABs.
Mission Core seals data identities; it never sends executable instructions.
This module defines the separately installed Worker package that is allowed to
contain reviewed container argv and in-container mount targets. A package is
bound to one exact RunDefinition and runtime candidate and uses one stable I/O
surface, so adding a conforming LAB does not require another Worker micro-app.
"""
from __future__ import annotations
import json
import re
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
from typing import Final, Literal
from k1link.observatory.portable_run_definitions import (
PortableRunDefinition,
PortableRunDefinitionRegistry,
canonical_sha256,
)
from k1link.observatory.portable_worker_runtime import (
PortableWorkerRuntimeCandidate,
PortableWorkerRuntimeRegistry,
)
from k1link.observatory.recorded_jobs import RecordedExecutorIdentity
INSTALLED_LAB_PACKAGE_SCHEMA: Final = "missioncore.observatory-installed-lab-package/v1"
INSTALLED_LAB_PACKAGE_REGISTRY_SCHEMA: Final = (
"missioncore.observatory-installed-lab-package-registry/v1"
)
INSTALLED_LAB_CONTAINER_IO_SCHEMA: Final = "missioncore.observatory-installed-lab-container-io/v2"
INSTALLED_LAB_SOURCE_ROOT: Final = "/missioncore/input/source"
INSTALLED_LAB_PLAN_PATH: Final = "/missioncore/input/run-plan.json"
INSTALLED_LAB_STEP_INPUT_ROOT: Final = "/missioncore/input/steps"
INSTALLED_LAB_RESULT_ROOT: Final = "/missioncore/output"
INSTALLED_LAB_WORK_ROOT: Final = "/missioncore/work"
_MAX_REGISTRY_BYTES: Final = 1024 * 1024
_IDENTIFIER = re.compile(r"^[a-z][a-z0-9-]{2,95}$")
_ASSET_ID = re.compile(r"^[a-z][a-z0-9.-]{2,127}$")
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
_FORBIDDEN_KEYS = frozenset(
{
"host_path",
"host-path",
"hostpath",
"secret",
"secrets",
"token",
"password",
"privileged",
"docker_socket",
}
)
_AUTHORITY: Final = {
"commands_enabled": False,
"actuation_allowed": False,
"navigation_or_safety_accepted": False,
"production_accepted": False,
}
type InstalledLabExecutionMode = Literal["single-container", "fixed-stack"]
type InstalledLabContainerRole = Literal["step", "result-writer"]
type InstalledLabNetworkMode = Literal["none"]
class InstalledLabPackageError(RuntimeError):
"""An installed LAB package is malformed or not exactly bound."""
@dataclass(frozen=True, slots=True)
class InstalledLabPackageMount:
"""Read-only asset mount; its Worker-local source path is not serialized."""
asset_id: str
target: str
read_only: Literal[True] = True
def __post_init__(self) -> None:
_pattern(self.asset_id, _ASSET_ID, "package mount asset id")
target = PurePosixPath(self.target)
reserved_roots = tuple(
PurePosixPath(value)
for value in (
INSTALLED_LAB_SOURCE_ROOT,
INSTALLED_LAB_STEP_INPUT_ROOT,
INSTALLED_LAB_RESULT_ROOT,
INSTALLED_LAB_WORK_ROOT,
)
)
if (
not target.is_absolute()
or ".." in target.parts
or target == PurePosixPath(INSTALLED_LAB_PLAN_PATH)
or any(target == root or target.is_relative_to(root) for root in reserved_roots)
):
raise InstalledLabPackageError("package asset mount target is unsafe")
if self.read_only is not True:
raise InstalledLabPackageError("package assets must remain read-only")
def as_dict(self) -> dict[str, object]:
return {
"asset_id": self.asset_id,
"target": self.target,
"read_only": True,
}
@dataclass(frozen=True, slots=True)
class InstalledLabContainer:
container_id: str
role: InstalledLabContainerRole
image_sha256: str
argv: tuple[str, ...]
depends_on: tuple[str, ...]
mounts: tuple[InstalledLabPackageMount, ...]
network: InstalledLabNetworkMode
gpu_count: int
memory_bytes: int
nano_cpus: int
pids_limit: int
shm_bytes: int
tmpfs_bytes: int
timeout_seconds: int
def __post_init__(self) -> None:
_pattern(self.container_id, _IDENTIFIER, "package container id")
_digest(self.image_sha256, "package container image sha256")
if self.role not in ("step", "result-writer"):
raise InstalledLabPackageError("package container role is invalid")
if not self.argv or len(self.argv) > 64:
raise InstalledLabPackageError("package container argv is invalid")
for argument in self.argv:
if (
not isinstance(argument, str)
or not argument
or len(argument) > 1_024
or "\x00" in argument
or "\n" in argument
or "\r" in argument
or "${" in argument
):
raise InstalledLabPackageError("package container argument is unsafe")
if self.depends_on != tuple(sorted(self.depends_on)) or len(self.depends_on) != len(
set(self.depends_on)
):
raise InstalledLabPackageError("package container dependencies must be canonical")
for dependency in self.depends_on:
_pattern(dependency, _IDENTIFIER, "package container dependency")
mount_keys = [(mount.target, mount.asset_id) for mount in self.mounts]
if mount_keys != sorted(mount_keys) or len(mount_keys) != len(set(mount_keys)):
raise InstalledLabPackageError("package mounts must be canonical and unique")
if self.network != "none":
raise InstalledLabPackageError("package container network mode is invalid")
if isinstance(self.gpu_count, bool) or not 0 <= self.gpu_count <= 8:
raise InstalledLabPackageError("package container GPU count is invalid")
for value, minimum, maximum, label in (
(self.memory_bytes, 64 * 1024**2, 512 * 1024**3, "memory"),
(self.nano_cpus, 100_000_000, 128_000_000_000, "CPU"),
(self.pids_limit, 16, 4_096, "PID"),
(self.shm_bytes, 64 * 1024**2, 128 * 1024**3, "shared-memory"),
(self.tmpfs_bytes, 64 * 1024**2, 64 * 1024**3, "tmpfs"),
):
if isinstance(value, bool) or not minimum <= value <= maximum:
raise InstalledLabPackageError(f"package container {label} limit is invalid")
if isinstance(self.timeout_seconds, bool) or not 1 <= self.timeout_seconds <= 24 * 60 * 60:
raise InstalledLabPackageError("package container timeout is invalid")
def as_dict(self) -> dict[str, object]:
return {
"container_id": self.container_id,
"role": self.role,
"image_sha256": self.image_sha256,
"argv": list(self.argv),
"depends_on": list(self.depends_on),
"mounts": [mount.as_dict() for mount in self.mounts],
"network": self.network,
"gpu_count": self.gpu_count,
"memory_bytes": self.memory_bytes,
"nano_cpus": self.nano_cpus,
"pids_limit": self.pids_limit,
"shm_bytes": self.shm_bytes,
"tmpfs_bytes": self.tmpfs_bytes,
"timeout_seconds": self.timeout_seconds,
}
@dataclass(frozen=True, slots=True)
class InstalledLabPackage:
package_id: str
package_version: int
package_sha256: str
setup_id: str
definition_id: str
definition_version: int
definition_sha256: str
runtime_candidate_sha256: str
source_adapter_sha256: str
result_contract_sha256: str
executor_identity: RecordedExecutorIdentity
execution_mode: InstalledLabExecutionMode
asset_ids: tuple[str, ...]
containers: tuple[InstalledLabContainer, ...]
def __post_init__(self) -> None:
for value, label in (
(self.package_id, "package id"),
(self.setup_id, "package setup id"),
(self.definition_id, "package definition id"),
):
_pattern(value, _IDENTIFIER, label)
for value, label in (
(self.package_sha256, "package sha256"),
(self.definition_sha256, "package definition sha256"),
(self.runtime_candidate_sha256, "runtime candidate sha256"),
(self.source_adapter_sha256, "source adapter sha256"),
(self.result_contract_sha256, "result contract sha256"),
):
_digest(value, label)
for numeric_value, label in (
(self.package_version, "package version"),
(self.definition_version, "package definition version"),
):
if (
isinstance(numeric_value, bool)
or not isinstance(numeric_value, int)
or numeric_value < 1
):
raise InstalledLabPackageError(f"{label} is invalid")
if self.execution_mode not in ("single-container", "fixed-stack"):
raise InstalledLabPackageError("package execution mode is invalid")
if self.asset_ids != tuple(sorted(self.asset_ids)) or len(self.asset_ids) != len(
set(self.asset_ids)
):
raise InstalledLabPackageError("package asset ids must be canonical")
for asset_id in self.asset_ids:
_pattern(asset_id, _ASSET_ID, "package asset id")
container_ids = tuple(container.container_id for container in self.containers)
if (
not container_ids
or container_ids != tuple(sorted(container_ids))
or len(container_ids) != len(set(container_ids))
):
raise InstalledLabPackageError("package containers must be canonical and unique")
writers = tuple(
container for container in self.containers if container.role == "result-writer"
)
if len(writers) != 1:
raise InstalledLabPackageError("package requires exactly one result writer")
if self.execution_mode == "single-container" and (
len(self.containers) != 1 or self.containers[0].role != "result-writer"
):
raise InstalledLabPackageError(
"single-container package must contain one result writer"
)
if self.execution_mode == "fixed-stack" and len(self.containers) < 2:
raise InstalledLabPackageError("fixed-stack package requires multiple containers")
self._verify_topology()
mounted_assets = {
mount.asset_id for container in self.containers for mount in container.mounts
}
if not mounted_assets.issubset(set(self.asset_ids)):
raise InstalledLabPackageError("package mounts reference undeclared assets")
if self.package_sha256 != canonical_sha256(self.identity_document()):
raise InstalledLabPackageError("installed LAB package digest changed")
def _verify_topology(self) -> None:
by_id = {container.container_id: container for container in self.containers}
for container in self.containers:
if container.container_id in container.depends_on or any(
dependency not in by_id for dependency in container.depends_on
):
raise InstalledLabPackageError("package dependency is invalid")
visiting: set[str] = set()
visited: set[str] = set()
def visit(container_id: str) -> None:
if container_id in visiting:
raise InstalledLabPackageError("package container graph contains a cycle")
if container_id in visited:
return
visiting.add(container_id)
for dependency in by_id[container_id].depends_on:
visit(dependency)
visiting.remove(container_id)
visited.add(container_id)
for container_id in by_id:
visit(container_id)
writer = next(
container for container in self.containers if container.role == "result-writer"
)
writer_ancestors: set[str] = set()
def collect(container_id: str) -> None:
for dependency in by_id[container_id].depends_on:
if dependency not in writer_ancestors:
writer_ancestors.add(dependency)
collect(dependency)
collect(writer.container_id)
steps = {
container.container_id for container in self.containers if container.role == "step"
}
if writer_ancestors != steps:
raise InstalledLabPackageError(
"package result writer must depend on every execution step"
)
def identity_document(self) -> dict[str, object]:
return _package_identity_document(
package_id=self.package_id,
package_version=self.package_version,
setup_id=self.setup_id,
definition_id=self.definition_id,
definition_version=self.definition_version,
definition_sha256=self.definition_sha256,
runtime_candidate_sha256=self.runtime_candidate_sha256,
source_adapter_sha256=self.source_adapter_sha256,
result_contract_sha256=self.result_contract_sha256,
executor_identity=self.executor_identity,
execution_mode=self.execution_mode,
asset_ids=self.asset_ids,
containers=self.containers,
)
def bind(
self,
definition: PortableRunDefinition,
candidate: PortableWorkerRuntimeCandidate,
) -> None:
candidate.bind_definition(definition)
if not candidate.ready:
raise InstalledLabPackageError("installed package binds a blocked runtime")
if (
self.setup_id != definition.setup_id
or self.definition_id != definition.definition_id
or self.definition_version != definition.version
or self.definition_sha256 != definition.definition_sha256
or self.runtime_candidate_sha256 != candidate.candidate_sha256
or self.source_adapter_sha256 != definition.source_adapter.contract_sha256
or self.result_contract_sha256 != definition.result_contract.contract_sha256
or self.executor_identity != candidate.executor_identity()
):
raise InstalledLabPackageError(
"installed package differs from its definition or runtime candidate"
)
candidate_assets = {asset.asset_id: asset for asset in candidate.reusable_assets}
if set(self.asset_ids) != set(candidate_assets):
raise InstalledLabPackageError(
"installed package asset inventory differs from its runtime candidate"
)
admitted_images = {
asset.sha256 for asset in candidate.reusable_assets if asset.kind == "container-image"
}
if candidate.executor is not None:
admitted_images.add(candidate.executor.image_sha256)
if any(container.image_sha256 not in admitted_images for container in self.containers):
raise InstalledLabPackageError("package container image is not runtime-admitted")
for container in self.containers:
for mount in container.mounts:
if candidate_assets[mount.asset_id].kind == "container-image":
raise InstalledLabPackageError("container images cannot be mounted as files")
@dataclass(frozen=True, slots=True)
class InstalledLabPackageRegistry:
packages: tuple[InstalledLabPackage, ...]
def __post_init__(self) -> None:
if not self.packages:
raise InstalledLabPackageError("installed LAB package registry is empty")
for values, label in (
([package.package_id for package in self.packages], "package ids"),
([package.package_sha256 for package in self.packages], "package digests"),
(
[(package.setup_id, package.definition_sha256) for package in self.packages],
"package definition bindings",
),
):
if len(values) != len(set(values)):
raise InstalledLabPackageError(f"installed LAB {label} must be unique")
@classmethod
def from_file(
cls,
path: Path,
*,
definitions: PortableRunDefinitionRegistry,
runtime_registry: PortableWorkerRuntimeRegistry,
) -> InstalledLabPackageRegistry:
candidate_path = path.expanduser().absolute()
try:
if candidate_path.is_symlink() or not candidate_path.is_file():
raise InstalledLabPackageError(
"installed LAB package registry must be a regular file"
)
payload = candidate_path.read_bytes()
if not 0 < len(payload) <= _MAX_REGISTRY_BYTES:
raise InstalledLabPackageError("installed LAB package registry size is invalid")
document: object = json.loads(payload.decode("utf-8"))
except InstalledLabPackageError:
raise
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise InstalledLabPackageError("installed LAB package registry is unreadable") from exc
_reject_forbidden_keys(document)
root = _object(document, "installed LAB package registry")
_exact_keys(root, {"schema_version", "packages"}, "installed LAB package registry")
if root["schema_version"] != INSTALLED_LAB_PACKAGE_REGISTRY_SCHEMA:
raise InstalledLabPackageError("installed LAB package registry schema is invalid")
registry = cls(tuple(_package(value) for value in _array(root["packages"], "packages")))
for package in registry.packages:
definition = definitions.resolve(package.setup_id, package.definition_sha256)
candidate = runtime_registry.resolve(
package.setup_id,
package.definition_sha256,
)
package.bind(definition, candidate)
return registry
def resolve(self, setup_id: str, definition_sha256: str) -> InstalledLabPackage:
for package in self.packages:
if package.setup_id == setup_id and package.definition_sha256 == definition_sha256:
return package
raise InstalledLabPackageError("installed LAB package is unavailable")
def seal_installed_lab_package(
*,
package_id: str,
package_version: int,
setup_id: str,
definition_id: str,
definition_version: int,
definition_sha256: str,
runtime_candidate_sha256: str,
source_adapter_sha256: str,
result_contract_sha256: str,
executor_identity: RecordedExecutorIdentity,
execution_mode: InstalledLabExecutionMode,
asset_ids: tuple[str, ...],
containers: tuple[InstalledLabContainer, ...],
) -> InstalledLabPackage:
canonical_asset_ids = tuple(sorted(asset_ids))
canonical_containers = tuple(sorted(containers, key=lambda value: value.container_id))
package_sha256 = canonical_sha256(
_package_identity_document(
package_id=package_id,
package_version=package_version,
setup_id=setup_id,
definition_id=definition_id,
definition_version=definition_version,
definition_sha256=definition_sha256,
runtime_candidate_sha256=runtime_candidate_sha256,
source_adapter_sha256=source_adapter_sha256,
result_contract_sha256=result_contract_sha256,
executor_identity=executor_identity,
execution_mode=execution_mode,
asset_ids=canonical_asset_ids,
containers=canonical_containers,
)
)
return InstalledLabPackage(
package_id=package_id,
package_version=package_version,
package_sha256=package_sha256,
setup_id=setup_id,
definition_id=definition_id,
definition_version=definition_version,
definition_sha256=definition_sha256,
runtime_candidate_sha256=runtime_candidate_sha256,
source_adapter_sha256=source_adapter_sha256,
result_contract_sha256=result_contract_sha256,
executor_identity=executor_identity,
execution_mode=execution_mode,
asset_ids=canonical_asset_ids,
containers=canonical_containers,
)
def _package_identity_document(
*,
package_id: str,
package_version: int,
setup_id: str,
definition_id: str,
definition_version: int,
definition_sha256: str,
runtime_candidate_sha256: str,
source_adapter_sha256: str,
result_contract_sha256: str,
executor_identity: RecordedExecutorIdentity,
execution_mode: InstalledLabExecutionMode,
asset_ids: tuple[str, ...],
containers: tuple[InstalledLabContainer, ...],
) -> dict[str, object]:
return {
"schema_version": INSTALLED_LAB_PACKAGE_SCHEMA,
"package_id": package_id,
"package_version": package_version,
"setup_id": setup_id,
"definition_id": definition_id,
"definition_version": definition_version,
"definition_sha256": definition_sha256,
"runtime_candidate_sha256": runtime_candidate_sha256,
"source_adapter_sha256": source_adapter_sha256,
"result_contract_sha256": result_contract_sha256,
"executor_identity": executor_identity.as_dict(),
"container_io": {
"schema_version": INSTALLED_LAB_CONTAINER_IO_SCHEMA,
"source_root": INSTALLED_LAB_SOURCE_ROOT,
"plan_path": INSTALLED_LAB_PLAN_PATH,
"step_input_root": INSTALLED_LAB_STEP_INPUT_ROOT,
"result_root": INSTALLED_LAB_RESULT_ROOT,
"work_root": INSTALLED_LAB_WORK_ROOT,
},
"execution_mode": execution_mode,
"asset_ids": list(asset_ids),
"containers": [container.as_dict() for container in containers],
"authority": dict(_AUTHORITY),
}
def _package(value: object) -> InstalledLabPackage:
row = _object(value, "installed LAB package")
_exact_keys(
row,
{
"schema_version",
"package_id",
"package_version",
"package_sha256",
"setup_id",
"definition_id",
"definition_version",
"definition_sha256",
"runtime_candidate_sha256",
"source_adapter_sha256",
"result_contract_sha256",
"executor_identity",
"container_io",
"execution_mode",
"asset_ids",
"containers",
"authority",
},
"installed LAB package",
)
if row["schema_version"] != INSTALLED_LAB_PACKAGE_SCHEMA:
raise InstalledLabPackageError("installed LAB package schema is invalid")
if row["authority"] != _AUTHORITY:
raise InstalledLabPackageError("installed LAB package authority changed")
if row["container_io"] != {
"schema_version": INSTALLED_LAB_CONTAINER_IO_SCHEMA,
"source_root": INSTALLED_LAB_SOURCE_ROOT,
"plan_path": INSTALLED_LAB_PLAN_PATH,
"step_input_root": INSTALLED_LAB_STEP_INPUT_ROOT,
"result_root": INSTALLED_LAB_RESULT_ROOT,
"work_root": INSTALLED_LAB_WORK_ROOT,
}:
raise InstalledLabPackageError("installed LAB container I/O contract changed")
executor = _object(row["executor_identity"], "executor identity")
_exact_keys(
executor,
{
"release_sha256",
"image_sha256",
"model_manifest_sha256",
"resource_profile_sha256",
},
"executor identity",
)
mode = row["execution_mode"]
if mode not in ("single-container", "fixed-stack"):
raise InstalledLabPackageError("installed LAB execution mode is invalid")
return InstalledLabPackage(
package_id=_string(row["package_id"], "package id"),
package_version=_integer(row["package_version"], "package version"),
package_sha256=_string(row["package_sha256"], "package sha256"),
setup_id=_string(row["setup_id"], "setup id"),
definition_id=_string(row["definition_id"], "definition id"),
definition_version=_integer(row["definition_version"], "definition version"),
definition_sha256=_string(row["definition_sha256"], "definition sha256"),
runtime_candidate_sha256=_string(
row["runtime_candidate_sha256"], "runtime candidate sha256"
),
source_adapter_sha256=_string(row["source_adapter_sha256"], "source adapter sha256"),
result_contract_sha256=_string(row["result_contract_sha256"], "result contract sha256"),
executor_identity=RecordedExecutorIdentity(
release_sha256=_string(executor["release_sha256"], "executor release sha256"),
image_sha256=_string(executor["image_sha256"], "executor image sha256"),
model_manifest_sha256=_string(
executor["model_manifest_sha256"], "model manifest sha256"
),
resource_profile_sha256=_string(
executor["resource_profile_sha256"], "resource profile sha256"
),
),
execution_mode=mode,
asset_ids=tuple(
_string(item, "package asset id")
for item in _array(row["asset_ids"], "package asset ids")
),
containers=tuple(
_container(item) for item in _array(row["containers"], "package containers")
),
)
def _container(value: object) -> InstalledLabContainer:
row = _object(value, "package container")
_exact_keys(
row,
{
"container_id",
"role",
"image_sha256",
"argv",
"depends_on",
"mounts",
"network",
"gpu_count",
"memory_bytes",
"nano_cpus",
"pids_limit",
"shm_bytes",
"tmpfs_bytes",
"timeout_seconds",
},
"package container",
)
role = row["role"]
network = row["network"]
if role not in ("step", "result-writer") or network != "none":
raise InstalledLabPackageError("package container enum is invalid")
return InstalledLabContainer(
container_id=_string(row["container_id"], "package container id"),
role=role,
image_sha256=_string(row["image_sha256"], "package image sha256"),
argv=tuple(
_string(item, "package argument") for item in _array(row["argv"], "package argv")
),
depends_on=tuple(
_string(item, "package dependency")
for item in _array(row["depends_on"], "package dependencies")
),
mounts=tuple(_mount(item) for item in _array(row["mounts"], "package mounts")),
network=network,
gpu_count=_integer(row["gpu_count"], "package GPU count"),
memory_bytes=_integer(row["memory_bytes"], "package memory limit"),
nano_cpus=_integer(row["nano_cpus"], "package CPU limit"),
pids_limit=_integer(row["pids_limit"], "package PID limit"),
shm_bytes=_integer(row["shm_bytes"], "package shared-memory limit"),
tmpfs_bytes=_integer(row["tmpfs_bytes"], "package tmpfs limit"),
timeout_seconds=_integer(row["timeout_seconds"], "package timeout"),
)
def _mount(value: object) -> InstalledLabPackageMount:
row = _object(value, "package mount")
_exact_keys(row, {"asset_id", "target", "read_only"}, "package mount")
if row["read_only"] is not True:
raise InstalledLabPackageError("package mount must be read-only")
return InstalledLabPackageMount(
asset_id=_string(row["asset_id"], "package mount asset id"),
target=_string(row["target"], "package mount target"),
)
def _reject_forbidden_keys(value: object) -> None:
if isinstance(value, dict):
for key, child in value.items():
if not isinstance(key, str) or key.lower() in _FORBIDDEN_KEYS:
raise InstalledLabPackageError(
"installed LAB package contains a forbidden host or secret input"
)
_reject_forbidden_keys(child)
elif isinstance(value, list):
for child in value:
_reject_forbidden_keys(child)
def _pattern(value: str, pattern: re.Pattern[str], label: str) -> None:
if not isinstance(value, str) or pattern.fullmatch(value) is None:
raise InstalledLabPackageError(f"{label} is invalid")
def _digest(value: str, label: str) -> None:
_pattern(value, _SHA256, label)
def _object(value: object, label: str) -> dict[str, object]:
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
raise InstalledLabPackageError(f"{label} must be an object")
return value
def _array(value: object, label: str) -> list[object]:
if not isinstance(value, list):
raise InstalledLabPackageError(f"{label} must be an array")
return value
def _string(value: object, label: str) -> str:
if not isinstance(value, str):
raise InstalledLabPackageError(f"{label} must be a string")
return value
def _integer(value: object, label: str) -> int:
if isinstance(value, bool) or not isinstance(value, int):
raise InstalledLabPackageError(f"{label} must be an integer")
return value
def _exact_keys(row: dict[str, object], expected: set[str], label: str) -> None:
if set(row) != expected:
raise InstalledLabPackageError(f"{label} keys are invalid")
@@ -0,0 +1,21 @@
"""Container entrypoint for the generic installed-LAB Worker agent."""
from __future__ import annotations
from collections.abc import Sequence
from k1link.observatory import installed_lab_worker_service
from k1link.observatory.worker_container_proxy import (
FixedObservatoryContainerLoopbackProxy,
)
def main(arguments: Sequence[str] | None = None) -> int:
"""Run the fixed loopback bridge around the package-driven service."""
with FixedObservatoryContainerLoopbackProxy():
return installed_lab_worker_service.main(arguments)
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,466 @@
"""Generic Worker entrypoint for independently installed LAB packages.
The environment selects only immutable registry files, one reviewed local
asset-binding file, the Worker work roots, and the existing transport settings.
Executable images, argv, topology, and limits live in the content-addressed
package registry and cannot arrive through a queued job.
"""
from __future__ import annotations
import argparse
import json
import os
import signal
import stat
import sys
from collections.abc import Iterator, Mapping, Sequence
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from threading import Event
from types import FrameType
from typing import Final, cast
from k1link.observatory.installed_lab_package_runner import (
DockerEngineInstalledLabLauncher,
InstalledLabLocalAssetBinding,
InstalledLabPackageExecutorFactory,
)
from k1link.observatory.installed_lab_packages import InstalledLabPackageRegistry
from k1link.observatory.portable_result_contract import (
OBSERVATION_ONLY_AUTHORITY,
canonical_json,
)
from k1link.observatory.portable_run_definitions import (
PortableRunDefinitionRegistry,
canonical_sha256,
)
from k1link.observatory.portable_worker_runtime import (
PortableWorkerResultDraft,
PortableWorkerRuntimeRegistry,
PortableWorkerSourceStage,
)
from k1link.observatory.worker_agent import (
ObservatoryWorkerExecutionResult,
SealedObservatoryRecordedJob,
)
from k1link.observatory.worker_service import (
OBSERVATORY_WORKER_WORK_ROOT_ENV,
InstalledObservatoryWorkerService,
ObservatoryWorkerServiceConfiguration,
build_ready_executor_registry_from_packages,
compose_installed_observatory_worker_service_from_packages,
)
INSTALLED_LAB_ASSET_BINDINGS_SCHEMA: Final = (
"missioncore.observatory-installed-lab-asset-bindings/v1"
)
INSTALLED_LAB_VALIDATION_RECEIPT_SCHEMA: Final = (
"missioncore.observatory-installed-lab-worker-validation/v1"
)
INSTALLED_LAB_DEFINITIONS_FILE_ENV: Final = (
"MISSIONCORE_OBSERVATORY_WORKER_DEFINITIONS_FILE"
)
INSTALLED_LAB_RUNTIME_REGISTRY_FILE_ENV: Final = (
"MISSIONCORE_OBSERVATORY_WORKER_RUNTIME_REGISTRY_FILE"
)
INSTALLED_LAB_PACKAGE_REGISTRY_FILE_ENV: Final = (
"MISSIONCORE_OBSERVATORY_WORKER_PACKAGE_REGISTRY_FILE"
)
INSTALLED_LAB_ASSET_BINDINGS_FILE_ENV: Final = (
"MISSIONCORE_OBSERVATORY_WORKER_PACKAGE_ASSET_BINDINGS_FILE"
)
_MAX_BINDINGS_BYTES: Final = 1024 * 1024
class InstalledLabWorkerCompositionError(RuntimeError):
"""The generic installed-LAB Worker configuration is not exact."""
@dataclass(frozen=True, slots=True)
class InstalledLabWorkerAssetBindings:
engine_work_root: str
assets: tuple[InstalledLabLocalAssetBinding, ...]
def __post_init__(self) -> None:
asset_ids = tuple(asset.asset_id for asset in self.assets)
if asset_ids != tuple(sorted(asset_ids)) or len(asset_ids) != len(set(asset_ids)):
raise InstalledLabWorkerCompositionError(
"installed LAB asset bindings must be canonical and unique"
)
@classmethod
def from_file(cls, path: Path) -> InstalledLabWorkerAssetBindings:
document = _read_object(path, "installed LAB asset bindings")
_exact_keys(
document,
{"schema_version", "engine_work_root", "assets"},
"installed LAB asset bindings",
)
if document["schema_version"] != INSTALLED_LAB_ASSET_BINDINGS_SCHEMA:
raise InstalledLabWorkerCompositionError(
"installed LAB asset binding schema changed"
)
values = document["assets"]
if not isinstance(values, list) or not values:
raise InstalledLabWorkerCompositionError(
"installed LAB asset binding inventory is empty"
)
assets = tuple(
sorted((_asset_binding(value) for value in values), key=lambda item: item.asset_id)
)
return cls(
engine_work_root=_text(document["engine_work_root"], "engine work root"),
assets=assets,
)
@dataclass(frozen=True, slots=True)
class InstalledLabWorkerEntrypointConfiguration:
worker: ObservatoryWorkerServiceConfiguration
definitions_file: Path
runtime_registry_file: Path
package_registry_file: Path
asset_bindings_file: Path
@classmethod
def from_environment(
cls,
environment: Mapping[str, str] | None = None,
) -> InstalledLabWorkerEntrypointConfiguration:
values = os.environ if environment is None else environment
return cls(
worker=ObservatoryWorkerServiceConfiguration.from_environment(values),
definitions_file=_required_path(values, INSTALLED_LAB_DEFINITIONS_FILE_ENV),
runtime_registry_file=_required_path(
values,
INSTALLED_LAB_RUNTIME_REGISTRY_FILE_ENV,
),
package_registry_file=_required_path(
values,
INSTALLED_LAB_PACKAGE_REGISTRY_FILE_ENV,
),
asset_bindings_file=_required_path(
values,
INSTALLED_LAB_ASSET_BINDINGS_FILE_ENV,
),
)
@dataclass(frozen=True, slots=True)
class InstalledLabWorkerValidationConfiguration:
work_root: Path
definitions_file: Path
runtime_registry_file: Path
package_registry_file: Path
asset_bindings_file: Path
@classmethod
def from_environment(
cls,
environment: Mapping[str, str] | None = None,
) -> InstalledLabWorkerValidationConfiguration:
values = os.environ if environment is None else environment
return cls(
work_root=_required_path(values, OBSERVATORY_WORKER_WORK_ROOT_ENV),
definitions_file=_required_path(
values,
INSTALLED_LAB_DEFINITIONS_FILE_ENV,
),
runtime_registry_file=_required_path(
values,
INSTALLED_LAB_RUNTIME_REGISTRY_FILE_ENV,
),
package_registry_file=_required_path(
values,
INSTALLED_LAB_PACKAGE_REGISTRY_FILE_ENV,
),
asset_bindings_file=_required_path(
values,
INSTALLED_LAB_ASSET_BINDINGS_FILE_ENV,
),
)
def compose_installed_lab_worker_service(
configuration: InstalledLabWorkerEntrypointConfiguration,
) -> InstalledObservatoryWorkerService:
definitions = PortableRunDefinitionRegistry.from_file(configuration.definitions_file)
runtime = PortableWorkerRuntimeRegistry.from_file(
configuration.runtime_registry_file,
definitions=definitions,
)
packages = InstalledLabPackageRegistry.from_file(
configuration.package_registry_file,
definitions=definitions,
runtime_registry=runtime,
)
bindings = InstalledLabWorkerAssetBindings.from_file(configuration.asset_bindings_file)
return compose_installed_observatory_worker_service_from_packages(
configuration=configuration.worker,
definitions=definitions,
runtime_registry=runtime,
packages=packages,
executor_factory=InstalledLabPackageExecutorFactory(
local_assets=bindings.assets,
engine_work_root=bindings.engine_work_root,
),
)
def validate_installed_lab_worker(
configuration: InstalledLabWorkerValidationConfiguration,
*,
launcher: DockerEngineInstalledLabLauncher | None = None,
) -> dict[str, object]:
"""Validate one installed package set without credentials or queue access."""
work_root = _real_directory(configuration.work_root, "installed LAB work root")
definitions = PortableRunDefinitionRegistry.from_file(configuration.definitions_file)
runtime = PortableWorkerRuntimeRegistry.from_file(
configuration.runtime_registry_file,
definitions=definitions,
)
packages = InstalledLabPackageRegistry.from_file(
configuration.package_registry_file,
definitions=definitions,
runtime_registry=runtime,
)
bindings = InstalledLabWorkerAssetBindings.from_file(configuration.asset_bindings_file)
image_sha256s = tuple(
sorted(
{
container.image_sha256
for package in packages.packages
for container in package.containers
}
)
)
image_launcher = DockerEngineInstalledLabLauncher() if launcher is None else launcher
image_launcher.verify_images(image_sha256s)
offline = _OfflineWorkerBoundary()
executors = build_ready_executor_registry_from_packages(
definitions=definitions,
runtime_registry=runtime,
packages=packages,
executor_factory=InstalledLabPackageExecutorFactory(
local_assets=bindings.assets,
engine_work_root=bindings.engine_work_root,
launcher=image_launcher,
),
source_transport=offline,
result_transport=offline,
work_root=work_root,
)
expected_identities = tuple(
sorted(package.executor_identity for package in packages.packages)
)
if executors.supported_identities != expected_identities:
raise InstalledLabWorkerCompositionError(
"installed LAB validation capability inventory changed"
)
package_receipts: list[dict[str, object]] = []
for package in packages.packages:
candidate = runtime.resolve(package.setup_id, package.definition_sha256)
package_receipts.append(
{
"package_id": package.package_id,
"package_version": package.package_version,
"package_sha256": package.package_sha256,
"setup_id": package.setup_id,
"definition_sha256": package.definition_sha256,
"runtime_candidate_sha256": package.runtime_candidate_sha256,
"executor": package.executor_identity.as_dict(),
"assets": [
{
"asset_id": asset.asset_id,
"kind": asset.kind,
"sha256": asset.sha256,
}
for asset in candidate.reusable_assets
],
"containers": [
{
"container_id": container.container_id,
"role": container.role,
"image_sha256": container.image_sha256,
}
for container in package.containers
],
}
)
identity = {
"schema_version": INSTALLED_LAB_VALIDATION_RECEIPT_SCHEMA,
"state": "ready",
"packages": package_receipts,
"verified_image_sha256s": list(image_sha256s),
"supported_executor_identities": [
identity.as_dict() for identity in executors.supported_identities
],
"checks": {
"registry_binding": "passed",
"local_asset_identity": "passed",
"docker_image_identity": "passed",
"backend_contacted": False,
"claim_attempted": False,
},
"authority": dict(OBSERVATION_ONLY_AUTHORITY),
}
return {**identity, "receipt_sha256": canonical_sha256(identity)}
def run_installed_lab_worker(
service: InstalledObservatoryWorkerService,
*,
stop: Event,
once: bool = False,
) -> None:
if once:
try:
service.agent.run_once()
finally:
service.close()
return
service.run(stop=stop)
def main(arguments: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
mode = parser.add_mutually_exclusive_group()
mode.add_argument("--once", action="store_true", help="Run one claim cycle and exit.")
mode.add_argument(
"--validate-only",
action="store_true",
help="Validate local packages and print a receipt without contacting Mission Core.",
)
options = parser.parse_args(arguments)
if cast(bool, options.validate_only):
receipt = validate_installed_lab_worker(
InstalledLabWorkerValidationConfiguration.from_environment()
)
sys.stdout.buffer.write(canonical_json(receipt) + b"\n")
return 0
service = compose_installed_lab_worker_service(
InstalledLabWorkerEntrypointConfiguration.from_environment()
)
stop = Event()
with _posix_shutdown_signals(stop):
run_installed_lab_worker(service, stop=stop, once=cast(bool, options.once))
return 0
def _asset_binding(value: object) -> InstalledLabLocalAssetBinding:
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
raise InstalledLabWorkerCompositionError("installed LAB asset binding is not an object")
_exact_keys(
value,
{"asset_id", "controller_path", "engine_path", "image_sha256"},
"installed LAB asset binding",
)
controller_value = value["controller_path"]
engine_value = value["engine_path"]
image_value = value["image_sha256"]
try:
if controller_value is None and engine_value is None and isinstance(image_value, str):
return InstalledLabLocalAssetBinding(
asset_id=_text(value["asset_id"], "installed LAB asset id"),
image_sha256=image_value,
)
if (
isinstance(controller_value, str)
and isinstance(engine_value, str)
and image_value is None
):
return InstalledLabLocalAssetBinding(
asset_id=_text(value["asset_id"], "installed LAB asset id"),
controller_path=Path(controller_value),
engine_path=engine_value,
)
except ValueError as exc:
raise InstalledLabWorkerCompositionError(
"installed LAB asset locator is invalid"
) from exc
raise InstalledLabWorkerCompositionError("installed LAB asset locator is ambiguous")
def _read_object(path: Path, label: str) -> dict[str, object]:
candidate = path.expanduser().absolute()
try:
if candidate.is_symlink() or not candidate.is_file():
raise InstalledLabWorkerCompositionError(f"{label} must be a regular file")
payload = candidate.read_bytes()
if not 0 < len(payload) <= _MAX_BINDINGS_BYTES:
raise InstalledLabWorkerCompositionError(f"{label} size is invalid")
value: object = json.loads(payload.decode("utf-8"))
except InstalledLabWorkerCompositionError:
raise
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise InstalledLabWorkerCompositionError(f"{label} is unreadable") from exc
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
raise InstalledLabWorkerCompositionError(f"{label} is not an object")
return cast(dict[str, object], value)
def _required_path(values: Mapping[str, str], name: str) -> Path:
value = values.get(name)
if value is None or not value or not Path(value).is_absolute():
raise InstalledLabWorkerCompositionError(f"{name} must name an absolute path")
return Path(value)
def _exact_keys(value: Mapping[str, object], expected: set[str], label: str) -> None:
if set(value) != expected:
raise InstalledLabWorkerCompositionError(f"{label} fields changed")
def _text(value: object, label: str) -> str:
if not isinstance(value, str) or not value or value != value.strip():
raise InstalledLabWorkerCompositionError(f"{label} is invalid")
return value
def _real_directory(path: Path, label: str) -> Path:
candidate = path.expanduser().absolute()
try:
metadata = candidate.lstat()
resolved = candidate.resolve(strict=True)
except OSError as exc:
raise InstalledLabWorkerCompositionError(f"{label} is unavailable") from exc
if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):
raise InstalledLabWorkerCompositionError(f"{label} is not a real directory")
return resolved
class _OfflineWorkerBoundary:
def materialize(self, job: SealedObservatoryRecordedJob) -> PortableWorkerSourceStage:
raise InstalledLabWorkerCompositionError(
f"offline validation cannot materialize job {job.job_id}"
)
def publish(
self,
job: SealedObservatoryRecordedJob,
draft: PortableWorkerResultDraft,
) -> ObservatoryWorkerExecutionResult:
raise InstalledLabWorkerCompositionError(
f"offline validation cannot publish job {job.job_id} from {draft.root}"
)
@contextmanager
def _posix_shutdown_signals(stop: Event) -> Iterator[None]:
def request_stop(_signal: int, _frame: FrameType | None) -> None:
stop.set()
previous_int = signal.signal(signal.SIGINT, request_stop)
previous_term = signal.signal(signal.SIGTERM, request_stop)
try:
yield
finally:
signal.signal(signal.SIGINT, previous_int)
signal.signal(signal.SIGTERM, previous_term)
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,499 @@
"""Package-owned prepare/assemble steps for the generic LAB V1 stack.
The generic Worker only supplies the sealed source, run plan, dependency
outputs, and reviewed assets. This module is the LAB package's own adapter: it
turns those standard inputs into the existing EoMT/DDRNet component contracts
and finally emits the common portable result package.
"""
from __future__ import annotations
import hashlib
import json
import os
import re
import shutil
import sys
from collections.abc import Mapping, Sequence
from datetime import UTC, datetime
from pathlib import Path
from typing import Final, cast
from k1link.observatory.installed_lab_packages import (
INSTALLED_LAB_PLAN_PATH,
INSTALLED_LAB_RESULT_ROOT,
INSTALLED_LAB_SOURCE_ROOT,
INSTALLED_LAB_STEP_INPUT_ROOT,
)
from k1link.observatory.portable_lab_v1_executor import (
PortableLabV1OrchestrationPlan,
assemble_lab_v1_result_v2,
package_lab_v1_result,
portable_lab_v1_orchestration_plan_from_document,
portable_lab_v1_source_input_from_document,
)
from k1link.observatory.portable_lab_v1_local_runners import (
PORTABLE_LAB_V1_COMPONENT_REQUEST_SCHEMA,
)
from k1link.observatory.portable_lab_v1_worker import (
_SealedPackageJobView,
materialize_lab_v1_source_from_worker_stage,
)
from k1link.observatory.portable_result_contract import (
OBSERVATION_ONLY_AUTHORITY,
canonical_json,
)
from k1link.observatory.portable_run_definitions import (
PortableRunDefinition,
PortableRunDefinitionRegistry,
)
from k1link.observatory.portable_worker_runtime import PortableWorkerSourceStage
from k1link.observatory.recorded_jobs import RecordedExecutorIdentity
from k1link.observatory.worker_agent import SealedObservatoryRecordedJob
LAB_V1_INSTALLED_PACKAGE_CONTRACT_SCHEMA: Final = (
"missioncore.observatory-lab-v1-installed-package-contract/v1"
)
_PACKAGE_CONTRACT = Path("/opt/nodedc/package/contract.json")
_DEFINITION_REGISTRY = Path("/opt/nodedc/package/portable-run-definitions.json")
_DDRNET_PROFILE = Path("/opt/nodedc/package/lab-v1-eomt-ddrnet-portable-v2.json")
_PREPARE_STEP = Path(INSTALLED_LAB_STEP_INPUT_ROOT) / "prepare"
_EOMT_STEP = Path(INSTALLED_LAB_STEP_INPUT_ROOT) / "eomt"
_DDRNET_STEP = Path(INSTALLED_LAB_STEP_INPUT_ROOT) / "ddrnet"
_MAX_DOCUMENT_BYTES: Final = 2 * 1024 * 1024
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
_IDENTIFIER = re.compile(r"^[a-z][a-z0-9-]{2,95}$")
class LabV1InstalledPackageStepError(RuntimeError):
"""The installed package or one of its sealed step inputs changed."""
def main(argv: Sequence[str] | None = None) -> int:
arguments = tuple(sys.argv[1:] if argv is None else argv)
if arguments == ("prepare",):
prepare()
return 0
if arguments == ("assemble",):
assemble()
return 0
raise LabV1InstalledPackageStepError("LAB V1 package step is not allowlisted")
def prepare() -> None:
output = _empty_directory(Path(INSTALLED_LAB_RESULT_ROOT), "LAB V1 prepare output")
contract = _load_contract()
runtime_plan = _runtime_plan()
definition = _definition(runtime_plan)
job = _sealed_job(runtime_plan, definition=definition, contract=contract)
source_stage = PortableWorkerSourceStage(
root=_real_directory(Path(INSTALLED_LAB_SOURCE_ROOT), "LAB V1 package source"),
source_bundle_sha256=_digest(runtime_plan["source_bundle_sha256"], "source bundle"),
source_capability_manifest_sha256=_digest(
runtime_plan["source_capability_manifest_sha256"],
"source capability",
),
source_adapter_sha256=_digest(
runtime_plan["source_adapter_sha256"],
"source adapter",
),
)
materialized = materialize_lab_v1_source_from_worker_stage(
worker_stage=source_stage,
job=job,
definition=definition,
output_parent=output,
)
camera_target = output / "camera-job"
camera_stage_parent = materialized.camera_job_root.parent
if camera_stage_parent.parent != materialized.root:
raise LabV1InstalledPackageStepError(
"materialized camera job is outside its digest-owned stage"
)
os.replace(materialized.camera_job_root, camera_target)
camera_stage_parent.rmdir()
materialized.root.rmdir()
ddrnet_profile = _load_object(_DDRNET_PROFILE, "portable DDRNet profile")
plan = PortableLabV1OrchestrationPlan.create_for_installed_package(
job=job,
definition=definition,
source=materialized.descriptor,
package_release_sha256=_digest(
contract["package_release_sha256"],
"package release",
),
ddrnet_runner_sha256=_digest(
contract["ddrnet_runner_sha256"],
"DDRNet runner",
),
result_assembler_sha256=_digest(
contract["result_assembler_sha256"],
"result assembler",
),
legacy_ddrnet_config=ddrnet_profile,
)
plan.require_executable()
_write(output / "source-input.json", materialized.descriptor.as_dict())
_write(output / "orchestration-plan.json", plan.as_dict())
_write(output / "effective-ddrnet-config.json", plan.effective_ddrnet_config)
component_images = _object(contract["component_images"], "component images")
component_assets = _object(contract["component_assets"], "component assets")
for component in ("eomt", "ddrnet"):
_write(
output / f"{component}-request.json",
_component_request(
component=component,
component_image_sha256=_digest(
component_images[component],
f"{component} image",
),
source=materialized.descriptor.as_dict(),
plan=plan,
assets=_asset_rows(component_assets[component], component),
),
)
def assemble() -> None:
output = _empty_directory(Path(INSTALLED_LAB_RESULT_ROOT), "LAB V1 result output")
contract = _load_contract()
runtime_plan = _runtime_plan()
definition = _definition(runtime_plan)
job = _sealed_job(runtime_plan, definition=definition, contract=contract)
source_input = portable_lab_v1_source_input_from_document(
_load_object(_PREPARE_STEP / "source-input.json", "LAB V1 source input")
)
plan = portable_lab_v1_orchestration_plan_from_document(
_load_object(
_PREPARE_STEP / "orchestration-plan.json",
"LAB V1 orchestration plan",
),
source_input=source_input,
)
if plan.release_candidate_sha256 != contract["package_release_sha256"]:
raise LabV1InstalledPackageStepError("LAB V1 package release identity changed")
assembly_parent = output / ".assembly"
assembly_parent.mkdir(mode=0o700)
package_parent = output / ".package"
package_parent.mkdir(mode=0o700)
assembly_result = assemble_lab_v1_result_v2(
plan=plan,
definition=definition,
eomt_result_root=_real_directory(_EOMT_STEP, "EoMT step result"),
ddrnet_result_root=_real_directory(_DDRNET_STEP, "DDRNet step result"),
output_parent=assembly_parent,
)
draft = package_lab_v1_result(
assembly=assembly_result,
plan=plan,
job=_SealedPackageJobView.from_job(job),
definition=definition,
created_at_utc=datetime.now(UTC).isoformat().replace("+00:00", "Z"),
output_parent=package_parent,
)
shutil.rmtree(assembly_parent)
for child in tuple(draft.root.iterdir()):
os.replace(child, output / child.name)
draft.root.rmdir()
package_parent.rmdir()
def _component_request(
*,
component: str,
component_image_sha256: str,
source: Mapping[str, object],
plan: PortableLabV1OrchestrationPlan,
assets: list[dict[str, object]],
) -> dict[str, object]:
prepared = f"{INSTALLED_LAB_STEP_INPUT_ROOT}/prepare"
return {
"schema_version": PORTABLE_LAB_V1_COMPONENT_REQUEST_SCHEMA,
"component": component,
"component_image_sha256": component_image_sha256,
"plan_sha256": plan.plan_sha256,
"definition_sha256": plan.definition_sha256,
"release_candidate_sha256": plan.release_candidate_sha256,
"source": dict(source),
"paths": {
"camera_job_root": f"{prepared}/camera-job",
"request": f"{prepared}/{component}-request.json",
"output_root": INSTALLED_LAB_RESULT_ROOT,
"effective_ddrnet_config": (
f"{prepared}/effective-ddrnet-config.json"
if component == "ddrnet"
else None
),
"eomt_result_root": (
f"{INSTALLED_LAB_STEP_INPUT_ROOT}/eomt"
if component == "ddrnet"
else None
),
"decoded_frames_root": (
f"{INSTALLED_LAB_RESULT_ROOT}/source-frames"
if component == "eomt"
else f"{INSTALLED_LAB_STEP_INPUT_ROOT}/eomt/source-frames"
),
},
"effective_ddrnet_config_sha256": (
plan.effective_ddrnet_config_sha256 if component == "ddrnet" else None
),
"assets": assets,
"authority": dict(OBSERVATION_ONLY_AUTHORITY),
}
def _sealed_job(
runtime_plan: Mapping[str, object],
*,
definition: PortableRunDefinition,
contract: Mapping[str, object],
) -> SealedObservatoryRecordedJob:
executor = _object(contract["executor"], "package executor")
identity = RecordedExecutorIdentity(
release_sha256=_digest(executor["release_sha256"], "executor release"),
image_sha256=_digest(executor["image_sha256"], "executor image"),
model_manifest_sha256=definition.model_manifest_sha256,
resource_profile_sha256=definition.resource_profile.profile_sha256,
)
return SealedObservatoryRecordedJob(
job_id=_text(runtime_plan["job_id"], "job id"),
request_sha256=_digest(runtime_plan["request_sha256"], "request"),
identity_sha256=_digest(runtime_plan["identity_sha256"], "job identity"),
submission_receipt_sha256=_digest(
runtime_plan["submission_receipt_sha256"],
"submission receipt",
),
source_session_id=_text(runtime_plan["source_session_id"], "source session"),
source_catalog_sha256=_digest(runtime_plan["source_catalog_sha256"], "catalog"),
source_bundle_sha256=_digest(runtime_plan["source_bundle_sha256"], "bundle"),
source_capability_manifest_sha256=_digest(
runtime_plan["source_capability_manifest_sha256"],
"source capability",
),
source_adapter_id=_identifier(
runtime_plan["source_adapter_id"],
"source adapter id",
),
source_adapter_version=_positive_int(
runtime_plan["source_adapter_version"],
"source adapter version",
),
source_adapter_sha256=_digest(
runtime_plan["source_adapter_sha256"],
"source adapter",
),
setup_id=definition.setup_id,
definition_id=definition.definition_id,
definition_version=definition.version,
definition_sha256=definition.definition_sha256,
executor_release_id=_identifier(executor["release_id"], "executor release id"),
executor_identity=identity,
model_release_ids=definition.learned_models,
resource_profile_id=definition.resource_profile.profile_id,
checkpoint_policy=definition.resource_profile.checkpoint_policy,
allowed_checkpoints=definition.resource_profile.allowed_checkpoints,
claim_generation=_positive_int(
runtime_plan["claim_generation"],
"claim generation",
),
claim_claimed_at_utc=None,
claim_expires_at_utc=None,
claim_heartbeat_at_utc=None,
claim_renewal_count=0,
restart_from_zero=False,
)
def _runtime_plan() -> dict[str, object]:
outer = _load_object(Path(INSTALLED_LAB_PLAN_PATH), "installed LAB run plan")
_exact_keys(
outer,
{"schema_version", "runtime_plan", "package_id", "package_sha256", "authority"},
"installed LAB run plan",
)
if (
outer["schema_version"] != "missioncore.observatory-installed-lab-run-plan/v1"
or outer["authority"] != OBSERVATION_ONLY_AUTHORITY
):
raise LabV1InstalledPackageStepError("installed LAB run plan changed")
plan = _object(outer["runtime_plan"], "portable runtime plan")
expected = {
"schema_version",
"job_id",
"request_sha256",
"identity_sha256",
"submission_receipt_sha256",
"claim_generation",
"adapter_id",
"candidate_sha256",
"setup_id",
"definition_id",
"definition_version",
"definition_sha256",
"source_session_id",
"source_catalog_sha256",
"source_bundle_sha256",
"source_capability_manifest_sha256",
"source_adapter_id",
"source_adapter_version",
"source_adapter_sha256",
"result_contract_sha256",
"phases",
"authority",
}
_exact_keys(plan, expected, "portable runtime plan")
if (
plan["schema_version"] != "missioncore.observatory-portable-worker-runtime-plan/v2"
or plan["authority"] != OBSERVATION_ONLY_AUTHORITY
):
raise LabV1InstalledPackageStepError("portable runtime plan changed")
return plan
def _definition(runtime_plan: Mapping[str, object]) -> PortableRunDefinition:
registry = PortableRunDefinitionRegistry.from_file(_DEFINITION_REGISTRY)
return registry.resolve(
_identifier(runtime_plan["setup_id"], "setup id"),
_digest(runtime_plan["definition_sha256"], "definition"),
)
def _load_contract() -> dict[str, object]:
contract = _load_object(_PACKAGE_CONTRACT, "LAB V1 package contract")
_exact_keys(
contract,
{
"schema_version",
"package_release_sha256",
"ddrnet_runner_sha256",
"result_assembler_sha256",
"executor",
"component_images",
"component_assets",
"authority",
},
"LAB V1 package contract",
)
if (
contract["schema_version"] != LAB_V1_INSTALLED_PACKAGE_CONTRACT_SCHEMA
or contract["authority"] != OBSERVATION_ONLY_AUTHORITY
):
raise LabV1InstalledPackageStepError("LAB V1 package contract changed")
_exact_keys(
_object(contract["executor"], "package executor"),
{"release_id", "release_sha256", "image_sha256"},
"package executor",
)
_exact_keys(
_object(contract["component_images"], "component images"),
{"eomt", "ddrnet"},
"component images",
)
_exact_keys(
_object(contract["component_assets"], "component assets"),
{"eomt", "ddrnet"},
"component assets",
)
return contract
def _asset_rows(value: object, component: str) -> list[dict[str, object]]:
if not isinstance(value, list):
raise LabV1InstalledPackageStepError(f"{component} assets are not an array")
rows: list[dict[str, object]] = []
for value_row in value:
row = _object(value_row, f"{component} asset")
_exact_keys(
row,
{"asset_id", "path", "kind", "verification", "identity_sha256", "byte_length"},
f"{component} asset",
)
_identifier(row["asset_id"], f"{component} asset id", dotted=True)
_digest(row["identity_sha256"], f"{component} asset identity")
rows.append(dict(row))
ids = tuple(cast(str, row["asset_id"]) for row in rows)
if ids != tuple(sorted(ids)) or len(ids) != len(set(ids)):
raise LabV1InstalledPackageStepError(f"{component} assets are not canonical")
return rows
def _load_object(path: Path, label: str) -> dict[str, object]:
if path.is_symlink() or not path.is_file():
raise LabV1InstalledPackageStepError(f"{label} is unavailable")
payload = path.read_bytes()
if not payload or len(payload) > _MAX_DOCUMENT_BYTES:
raise LabV1InstalledPackageStepError(f"{label} size is invalid")
try:
value: object = json.loads(payload)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise LabV1InstalledPackageStepError(f"{label} is invalid JSON") from exc
return _object(value, label)
def _write(path: Path, value: Mapping[str, object]) -> None:
payload = canonical_json(value)
path.write_bytes(payload)
os.chmod(path, 0o400)
if hashlib.sha256(path.read_bytes()).digest() != hashlib.sha256(payload).digest():
raise LabV1InstalledPackageStepError("LAB V1 package document write changed")
def _empty_directory(path: Path, label: str) -> Path:
root = _real_directory(path, label)
if any(root.iterdir()):
raise LabV1InstalledPackageStepError(f"{label} is not empty")
return root
def _real_directory(path: Path, label: str) -> Path:
if path.is_symlink() or not path.is_dir():
raise LabV1InstalledPackageStepError(f"{label} is unavailable")
return path.resolve(strict=True)
def _object(value: object, label: str) -> dict[str, object]:
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
raise LabV1InstalledPackageStepError(f"{label} is not an object")
return cast(dict[str, object], value)
def _exact_keys(value: Mapping[str, object], expected: set[str], label: str) -> None:
if set(value) != expected:
raise LabV1InstalledPackageStepError(f"{label} fields changed")
def _text(value: object, label: str) -> str:
if not isinstance(value, str) or not value:
raise LabV1InstalledPackageStepError(f"{label} is invalid")
return value
def _identifier(value: object, label: str, *, dotted: bool = False) -> str:
text = _text(value, label)
pattern = re.compile(r"^[a-z][a-z0-9.-]{2,127}$") if dotted else _IDENTIFIER
if pattern.fullmatch(text) is None:
raise LabV1InstalledPackageStepError(f"{label} is invalid")
return text
def _digest(value: object, label: str) -> str:
text = _text(value, label)
if _SHA256.fullmatch(text) is None:
raise LabV1InstalledPackageStepError(f"{label} is invalid")
return text
def _positive_int(value: object, label: str) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
raise LabV1InstalledPackageStepError(f"{label} is invalid")
return value
if __name__ == "__main__":
try:
raise SystemExit(main())
except LabV1InstalledPackageStepError as exc:
print(f"installed LAB V1 package rejected: {exc}", file=sys.stderr)
raise SystemExit(2) from exc
@@ -9,153 +9,31 @@ weakening the gateway: a process-local TCP bridge binds only
from __future__ import annotations
import shutil
import socket
import socketserver
import threading
from collections.abc import Sequence
from contextlib import suppress
from typing import Final
from k1link.observatory import m49_worker_service
from k1link.observatory.worker_container_proxy import (
CONTAINER_PROXY_CONNECT_TIMEOUT_SECONDS,
CONTAINER_PROXY_COPY_BYTES,
CONTAINER_PROXY_LISTEN_HOST,
CONTAINER_PROXY_LISTEN_PORT,
CONTAINER_PROXY_UPSTREAM_HOST,
CONTAINER_PROXY_UPSTREAM_PORT,
FixedObservatoryContainerLoopbackProxy,
ObservatoryWorkerContainerProxyError,
)
M49_CONTAINER_PROXY_LISTEN_HOST: Final = "127.0.0.1"
M49_CONTAINER_PROXY_LISTEN_PORT: Final = 18080
M49_CONTAINER_PROXY_UPSTREAM_HOST: Final = "host.docker.internal"
M49_CONTAINER_PROXY_UPSTREAM_PORT: Final = 18080
M49_CONTAINER_PROXY_CONNECT_TIMEOUT_SECONDS: Final = 10.0
M49_CONTAINER_PROXY_COPY_BYTES: Final = 1024 * 1024
class M49WorkerContainerProxyError(RuntimeError):
"""The fixed container loopback bridge could not be started safely."""
class _ThreadedTcpServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = True
class _FixedProxyHandler(socketserver.BaseRequestHandler):
server: _ThreadedTcpServer
def handle(self) -> None:
upstream_address = getattr(self.server, "upstream_address", None)
connect_timeout = getattr(self.server, "connect_timeout", None)
if (
not isinstance(upstream_address, tuple)
or len(upstream_address) != 2
or not isinstance(upstream_address[0], str)
or not isinstance(upstream_address[1], int)
or not isinstance(connect_timeout, float)
):
return
try:
upstream = socket.create_connection(
upstream_address,
timeout=connect_timeout,
)
except OSError:
return
with upstream:
upstream.settimeout(None)
client = self.request
if not isinstance(client, socket.socket):
return
client.settimeout(None)
client_to_upstream = threading.Thread(
target=_copy_socket,
args=(client, upstream),
daemon=True,
name="m49-proxy-client-to-host",
)
upstream_to_client = threading.Thread(
target=_copy_socket,
args=(upstream, client),
daemon=True,
name="m49-proxy-host-to-client",
)
client_to_upstream.start()
upstream_to_client.start()
client_to_upstream.join()
upstream_to_client.join()
class FixedM49ContainerLoopbackProxy:
"""Own one bounded TCP bridge for the lifetime of the Worker process."""
def __init__(
self,
*,
listen_host: str = M49_CONTAINER_PROXY_LISTEN_HOST,
listen_port: int = M49_CONTAINER_PROXY_LISTEN_PORT,
upstream_host: str = M49_CONTAINER_PROXY_UPSTREAM_HOST,
upstream_port: int = M49_CONTAINER_PROXY_UPSTREAM_PORT,
connect_timeout: float = M49_CONTAINER_PROXY_CONNECT_TIMEOUT_SECONDS,
) -> None:
if listen_host != M49_CONTAINER_PROXY_LISTEN_HOST:
raise ValueError("M4.9 container proxy must bind IPv4 loopback")
if not 0 <= listen_port <= 65_535:
raise ValueError("M4.9 container proxy listen port is invalid")
if not upstream_host or upstream_host != upstream_host.strip():
raise ValueError("M4.9 container proxy upstream host is invalid")
if not 1 <= upstream_port <= 65_535:
raise ValueError("M4.9 container proxy upstream port is invalid")
if not 0.05 <= connect_timeout <= 60.0:
raise ValueError("M4.9 container proxy timeout is invalid")
try:
server = _ThreadedTcpServer(
(listen_host, listen_port),
_FixedProxyHandler,
bind_and_activate=True,
)
except OSError as exc:
raise M49WorkerContainerProxyError(
"M4.9 container loopback proxy could not bind"
) from exc
server.upstream_address = (upstream_host, upstream_port) # type: ignore[attr-defined]
server.connect_timeout = float(connect_timeout) # type: ignore[attr-defined]
self._server = server
self._thread = threading.Thread(
target=server.serve_forever,
kwargs={"poll_interval": 0.1},
daemon=True,
name="m49-container-loopback-proxy",
)
@property
def listen_port(self) -> int:
address = self._server.server_address
if not isinstance(address, tuple) or not isinstance(address[1], int):
raise M49WorkerContainerProxyError("M4.9 proxy address is invalid")
return address[1]
def __enter__(self) -> FixedM49ContainerLoopbackProxy:
self._thread.start()
return self
def __exit__(self, *_args: object) -> None:
self._server.shutdown()
self._server.server_close()
self._thread.join(timeout=5.0)
if self._thread.is_alive():
raise M49WorkerContainerProxyError(
"M4.9 container loopback proxy did not stop"
)
def _copy_socket(source: socket.socket, destination: socket.socket) -> None:
try:
shutil.copyfileobj(
source.makefile("rb", buffering=0),
destination.makefile("wb", buffering=0),
length=M49_CONTAINER_PROXY_COPY_BYTES,
)
except OSError:
pass
finally:
with suppress(OSError):
destination.shutdown(socket.SHUT_WR)
M49_CONTAINER_PROXY_LISTEN_HOST: Final = CONTAINER_PROXY_LISTEN_HOST
M49_CONTAINER_PROXY_LISTEN_PORT: Final = CONTAINER_PROXY_LISTEN_PORT
M49_CONTAINER_PROXY_UPSTREAM_HOST: Final = CONTAINER_PROXY_UPSTREAM_HOST
M49_CONTAINER_PROXY_UPSTREAM_PORT: Final = CONTAINER_PROXY_UPSTREAM_PORT
M49_CONTAINER_PROXY_CONNECT_TIMEOUT_SECONDS: Final = (
CONTAINER_PROXY_CONNECT_TIMEOUT_SECONDS
)
M49_CONTAINER_PROXY_COPY_BYTES: Final = CONTAINER_PROXY_COPY_BYTES
M49WorkerContainerProxyError = ObservatoryWorkerContainerProxyError
FixedM49ContainerLoopbackProxy = FixedObservatoryContainerLoopbackProxy
def main(arguments: Sequence[str] | None = None) -> int:
+16 -2
View File
@@ -212,11 +212,11 @@ class M49WorkerEntrypointConfiguration:
values,
M49_WORKER_INSTALLATION_RECEIPT_FILE_ENV,
),
lab_v1_installation_receipt_file=_required_environment_path(
lab_v1_installation_receipt_file=_optional_environment_path(
values,
LAB_V1_WORKER_INSTALLATION_RECEIPT_FILE_ENV,
),
lab_v1_release_candidate_file=_required_environment_path(
lab_v1_release_candidate_file=_optional_environment_path(
values,
LAB_V1_WORKER_RELEASE_CANDIDATE_FILE_ENV,
),
@@ -1101,6 +1101,20 @@ def _required_environment_path(values: Mapping[str, str], name: str) -> Path:
return path
def _optional_environment_path(
values: Mapping[str, str],
name: str,
) -> Path | None:
value = values.get(name)
if value is None:
return None
if not value or value != value.strip():
raise M49WorkerCompositionError(f"{name} is invalid")
path = Path(value)
_absolute_path(path, name)
return path
def _digest(value: str, label: str) -> None:
if _SHA256.fullmatch(value) is None:
raise M49WorkerCompositionError(f"{label} is invalid")
@@ -34,6 +34,7 @@ from k1link.compute.jobs import CameraComputeJob, validate_camera_compute_job
from k1link.observatory.portable_result_contract import (
OBSERVATION_ONLY_AUTHORITY,
PortableResultArtifact,
PortableResultJobIdentity,
PortableResultPackageIntegrityError,
PortableResultPackageManifest,
PortableResultValidationContext,
@@ -839,52 +840,11 @@ class PortableLabV1OrchestrationPlan:
legacy_ddrnet_config,
source=source,
)
components = {item.component_id: item.sha256 for item in definition.components}
release_assets = {asset.asset_id: asset.sha256 for asset in release.assets}
phases = (
PortableLabV1PlanPhase(
phase_id="source-materialization",
component_sha256s=(definition.source_adapter.contract_sha256,),
input_roles=("camera-compute-job", "source-documents"),
output_roles=("source-input-manifest",),
),
PortableLabV1PlanPhase(
phase_id="eomt-full-session",
component_sha256s=tuple(
sorted(
(
components["eomt-recorded-orchestrator-v1"],
components["eomt-recorded-profile-v1"],
components["eomt-recorded-runner-v1"],
definition.model_manifest_sha256,
)
)
),
input_roles=("camera-compute-job",),
output_roles=("eomt-component-result",),
),
PortableLabV1PlanPhase(
phase_id="ddrnet-full-session",
component_sha256s=tuple(
sorted(
(
components["ddrnet-portable-runtime-config-v2"],
components["vegetation-mission-policy-v1"],
components["vegetation-provider-label-map-v1"],
release_assets["ddrnet-goose-runner"],
definition.model_manifest_sha256,
)
)
),
input_roles=("camera-compute-job", "ddrnet-effective-config"),
output_roles=("ddrnet-component-result",),
),
PortableLabV1PlanPhase(
phase_id="result-v2-assembly",
component_sha256s=(release_assets["lab-v1-portable-contracts"],),
input_roles=("ddrnet-component-result", "eomt-component-result"),
output_roles=("portable-result-draft",),
),
phases = _portable_lab_v1_plan_phases(
definition,
ddrnet_runner_sha256=release_assets["ddrnet-goose-runner"],
result_assembler_sha256=release_assets["lab-v1-portable-contracts"],
)
effective_sha256 = canonical_sha256(effective)
identity = _plan_identity_document(
@@ -920,6 +880,71 @@ class PortableLabV1OrchestrationPlan:
plan_sha256=canonical_sha256(identity),
)
@classmethod
def create_for_installed_package(
cls,
*,
job: SealedObservatoryRecordedJob,
definition: PortableRunDefinition,
source: PortableLabV1SourceInput,
package_release_sha256: str,
ddrnet_runner_sha256: str,
result_assembler_sha256: str,
legacy_ddrnet_config: Mapping[str, object],
) -> PortableLabV1OrchestrationPlan:
"""Create a plan after the generic package registry admitted all assets."""
_verify_definition_and_job(definition, job)
_verify_source_and_job(source, job, definition)
for value, label in (
(package_release_sha256, "installed LAB V1 package release sha256"),
(ddrnet_runner_sha256, "installed LAB V1 DDRNet runner sha256"),
(result_assembler_sha256, "installed LAB V1 result assembler sha256"),
):
_digest(value, label)
effective = build_portable_ddrnet_effective_config(
legacy_ddrnet_config,
source=source,
)
phases = _portable_lab_v1_plan_phases(
definition,
ddrnet_runner_sha256=ddrnet_runner_sha256,
result_assembler_sha256=result_assembler_sha256,
)
effective_sha256 = canonical_sha256(effective)
identity = _plan_identity_document(
observatory_job_id=job.job_id,
observatory_request_sha256=job.request_sha256,
observatory_identity_sha256=job.identity_sha256,
setup_id=definition.setup_id,
definition_id=definition.definition_id,
definition_version=definition.version,
definition_sha256=definition.definition_sha256,
result_contract_sha256=definition.result_contract.contract_sha256,
source_input_sha256=source.identity_sha256,
release_candidate_sha256=package_release_sha256,
effective_ddrnet_config_sha256=effective_sha256,
phases=phases,
release_blockers=(),
)
return cls(
observatory_job_id=job.job_id,
observatory_request_sha256=job.request_sha256,
observatory_identity_sha256=job.identity_sha256,
setup_id=definition.setup_id,
definition_id=definition.definition_id,
definition_version=definition.version,
definition_sha256=definition.definition_sha256,
result_contract_sha256=definition.result_contract.contract_sha256,
source_input=source,
release_candidate_sha256=package_release_sha256,
effective_ddrnet_config=effective,
effective_ddrnet_config_sha256=effective_sha256,
phases=phases,
blockers=(),
plan_sha256=canonical_sha256(identity),
)
@property
def executable(self) -> bool:
return not self.blockers
@@ -1053,6 +1078,60 @@ def build_portable_ddrnet_effective_config(
return copied
def _portable_lab_v1_plan_phases(
definition: PortableRunDefinition,
*,
ddrnet_runner_sha256: str,
result_assembler_sha256: str,
) -> tuple[PortableLabV1PlanPhase, ...]:
components = {item.component_id: item.sha256 for item in definition.components}
return (
PortableLabV1PlanPhase(
phase_id="source-materialization",
component_sha256s=(definition.source_adapter.contract_sha256,),
input_roles=("camera-compute-job", "source-documents"),
output_roles=("source-input-manifest",),
),
PortableLabV1PlanPhase(
phase_id="eomt-full-session",
component_sha256s=tuple(
sorted(
(
components["eomt-recorded-orchestrator-v1"],
components["eomt-recorded-profile-v1"],
components["eomt-recorded-runner-v1"],
definition.model_manifest_sha256,
)
)
),
input_roles=("camera-compute-job",),
output_roles=("eomt-component-result",),
),
PortableLabV1PlanPhase(
phase_id="ddrnet-full-session",
component_sha256s=tuple(
sorted(
(
components["ddrnet-portable-runtime-config-v2"],
components["vegetation-mission-policy-v1"],
components["vegetation-provider-label-map-v1"],
ddrnet_runner_sha256,
definition.model_manifest_sha256,
)
)
),
input_roles=("camera-compute-job", "ddrnet-effective-config"),
output_roles=("ddrnet-component-result",),
),
PortableLabV1PlanPhase(
phase_id="result-v2-assembly",
component_sha256s=(result_assembler_sha256,),
input_roles=("ddrnet-component-result", "eomt-component-result"),
output_roles=("portable-result-draft",),
),
)
@dataclass(frozen=True, slots=True)
class PortableLabV1ResultAssembly:
root: Path
@@ -1289,18 +1368,12 @@ def package_lab_v1_result(
*,
assembly: PortableLabV1ResultAssembly,
plan: PortableLabV1OrchestrationPlan,
job: ObservatoryRecordedJob,
job: PortableResultJobIdentity,
definition: PortableRunDefinition,
created_at_utc: str,
output_parent: Path,
) -> PortableWorkerResultDraft:
"""Wrap a validated assembly in the generic portable result package.
This step intentionally requires the durable server job because the current
sealed Worker claim omits its submission receipt. Until that receipt is
transported into the local adapter (or the server owns this step), release
admission remains blocked instead of weakening the package identity.
"""
"""Wrap a validated assembly in the generic portable result package."""
_verify_plan_definition(plan, definition)
if (
@@ -1490,7 +1563,7 @@ def validate_lab_v1_result_v2(context: PortableResultValidationContext) -> None:
raise PortableLabV1ResultError("portable LAB V1 identity projection changed")
def _source_input_from_document(
def portable_lab_v1_source_input_from_document(
document: Mapping[str, object],
) -> PortableLabV1SourceInput:
_exact_keys(
@@ -1598,7 +1671,10 @@ def _source_input_from_document(
)
def _orchestration_plan_from_document(
_source_input_from_document = portable_lab_v1_source_input_from_document
def portable_lab_v1_orchestration_plan_from_document(
document: Mapping[str, object],
*,
source_input: PortableLabV1SourceInput,
@@ -1727,6 +1803,9 @@ def _orchestration_plan_from_document(
return plan
_orchestration_plan_from_document = portable_lab_v1_orchestration_plan_from_document
def _plan_phase_from_document(value: object) -> PortableLabV1PlanPhase:
row = _object(value, "portable plan phase")
_exact_keys(
@@ -1821,7 +1900,7 @@ def _validate_source_documents(
or video.get("semantic_channel_id")
!= requirements.camera_semantic_channel_id
or video.get("seekable") is not True
or camera_job.source_id != requirements.camera_source_id
or camera_job.source_id != camera.get("public_source_id")
or camera_job.codec_epoch != epoch.get("ordinal")
or epoch.get("media_type") != requirements.recorded_media_type
or init.get("sha256") != requirements.recorded_media_init_sha256
@@ -1932,7 +2011,6 @@ def _verify_source_and_job(
or source.source_capability_manifest_sha256
!= job.source_capability_manifest_sha256
or source.source_adapter_sha256 != job.source_adapter_sha256
or source.camera_source_id != requirements.camera_source_id
or source.calibration_sha256 != requirements.calibration_identity_sha256
):
raise PortableLabV1PlanError(
@@ -0,0 +1,105 @@
"""Bounded recovery for verified Observatory result publication."""
from __future__ import annotations
from collections.abc import Callable
from contextlib import suppress
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from .portable_artifact_transport import (
PortableArtifactTransportError,
PortableObservatoryArtifactTransport,
)
from .portable_result_contract import PortableResultPublisherError
from .portable_result_publisher import PortableObservatoryResultPublisher
from .recorded_jobs import (
ObservatoryRecordedJobQueue,
ObservatoryRecordedQueueError,
)
@dataclass(frozen=True, slots=True)
class PortablePublicationReconciliation:
examined: int
published: int
failed: int
deferred: int
exhausted: int
@dataclass(frozen=True, slots=True)
class PortablePublicationReconciler:
"""Retry only durable publication outbox entries, never their compute jobs."""
queue: ObservatoryRecordedJobQueue
artifact_transport: PortableObservatoryArtifactTransport
result_publisher: PortableObservatoryResultPublisher
clock: Callable[[], datetime] = lambda: datetime.now(UTC)
maximum_attempts: int = 5
retry_delays_seconds: tuple[int, ...] = (0, 30, 120, 600, 1_800)
def __post_init__(self) -> None:
if self.maximum_attempts < 1:
raise ValueError("publication maximum attempts must be positive")
if (
len(self.retry_delays_seconds) != self.maximum_attempts
or self.retry_delays_seconds != tuple(sorted(self.retry_delays_seconds))
or any(value < 0 for value in self.retry_delays_seconds)
):
raise ValueError("publication retry delays are invalid")
def run_once(self, *, limit: int = 4) -> PortablePublicationReconciliation:
if not 1 <= limit <= 32:
raise ValueError("publication reconciliation limit must be within 1..32")
now = self.clock()
if now.tzinfo is None:
raise ValueError("publication reconciliation clock must be timezone-aware")
candidates = self.queue.pending_publications()[:limit]
published = 0
failed = 0
deferred = 0
exhausted = 0
for job in candidates:
attempts = job.publication_attempts
if attempts >= self.maximum_attempts:
exhausted += 1
continue
updated_at = _timestamp(job.updated_at_utc)
retry_at = updated_at + timedelta(
seconds=self.retry_delays_seconds[attempts]
)
if now < retry_at:
deferred += 1
continue
try:
package_root = self.artifact_transport.package_root_for_terminal(job)
self.result_publisher.publish(job=job, package_root=package_root)
self.queue.mark_published(job.job_id)
published += 1
except (PortableArtifactTransportError, PortableResultPublisherError) as exc:
message = (" ".join(str(exc).split()) or type(exc).__name__)[:1_000]
with suppress(ObservatoryRecordedQueueError):
self.queue.mark_publication_failed(job.job_id, message=message)
failed += 1
return PortablePublicationReconciliation(
examined=len(candidates),
published=published,
failed=failed,
deferred=deferred,
exhausted=exhausted,
)
def _timestamp(value: str) -> datetime:
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as exc:
raise ObservatoryRecordedQueueError(
"recorded publication timestamp is invalid"
) from exc
if parsed.tzinfo is None:
raise ObservatoryRecordedQueueError(
"recorded publication timestamp has no timezone"
)
return parsed
@@ -8,7 +8,7 @@ import re
from collections.abc import Callable, Mapping, Sequence
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
from typing import Final, cast
from typing import Final, Protocol, cast
from k1link.observatory.portable_run_definitions import (
PortableRunDefinition,
@@ -16,6 +16,50 @@ from k1link.observatory.portable_run_definitions import (
)
from k1link.observatory.recorded_jobs import ObservatoryRecordedJob
class PortableResultJobIdentity(Protocol):
"""Sealed job fields required to create a portable result identity."""
@property
def job_id(self) -> str: ...
@property
def request_sha256(self) -> str: ...
@property
def identity_sha256(self) -> str: ...
@property
def submission_receipt_sha256(self) -> str: ...
@property
def claim_generation(self) -> int: ...
@property
def source_session_id(self) -> str: ...
@property
def source_catalog_sha256(self) -> str: ...
@property
def source_bundle_sha256(self) -> str: ...
@property
def source_capability_manifest_sha256(self) -> str: ...
@property
def source_adapter_id(self) -> str: ...
@property
def source_adapter_version(self) -> int: ...
@property
def source_adapter_sha256(self) -> str: ...
@property
def result_id(self) -> str | None: ...
PORTABLE_RESULT_PACKAGE_SCHEMA: Final = (
"missioncore.observatory-portable-result-package/v1"
)
@@ -150,7 +194,7 @@ class PortableResultPackageManifest:
def create(
cls,
*,
job: ObservatoryRecordedJob,
job: PortableResultJobIdentity,
definition: PortableRunDefinition,
result_id: str,
created_at_utc: str,
@@ -415,7 +459,7 @@ class PortableResultContractValidatorRegistry:
)
def job_identity_document(job: ObservatoryRecordedJob) -> dict[str, object]:
def job_identity_document(job: PortableResultJobIdentity) -> dict[str, object]:
return {
"job_id": job.job_id,
"request_sha256": job.request_sha256,
@@ -425,7 +469,7 @@ def job_identity_document(job: ObservatoryRecordedJob) -> dict[str, object]:
}
def source_identity_document(job: ObservatoryRecordedJob) -> dict[str, object]:
def source_identity_document(job: PortableResultJobIdentity) -> dict[str, object]:
return {
"session_id": job.source_session_id,
"catalog_sha256": job.source_catalog_sha256,
@@ -69,7 +69,12 @@ from k1link.observatory.source_admission import (
PORTABLE_SOURCE_CAPABILITY_SCHEMA,
PORTABLE_SOURCE_DOCUMENT_DIRECTORY,
)
from k1link.sessions.models import LabSessionBinding, SessionIntegrityError, SessionSummary
from k1link.sessions.models import (
LabReplayCapability,
LabSessionBinding,
SessionIntegrityError,
SessionSummary,
)
from k1link.sessions.store import SessionStore
_COPY_CHUNK_BYTES = 1024 * 1024
@@ -157,12 +162,21 @@ class PortableObservatoryResultPublisher:
artifact_paths=artifact_paths,
profile=profile,
)
replay_capability = LabReplayCapability(
schema_version="missioncore.observation-lab-replay-capability/v2",
kind="portable-result-review",
viewer_profile="portable-result",
timeline="result-defined",
activation="explicit",
commands_enabled=False,
)
provenance = _publication_provenance(
job=job,
definition=definition,
package=package,
artifact_manifest=artifact_manifest,
profile=profile,
replay_capability=replay_capability,
)
try:
binding = self._session_store.publish_lab_instance(
@@ -174,7 +188,7 @@ class PortableObservatoryResultPublisher:
result_id=cast(str, job.result_id),
config_sha256=definition.definition_sha256,
run_created_at_utc=job.updated_at_utc,
replay_capability=None,
replay_capability=replay_capability,
provenance=provenance,
include_recorded_media=profile.include_recorded_media,
expected_source_catalog_sha256=job.source_catalog_sha256,
@@ -320,6 +334,7 @@ def resolve_published_portable_calculation_profile(
"source",
"run_definition",
"result_package",
"replay_capability",
"storage",
"method",
},
@@ -354,6 +369,10 @@ def resolve_published_portable_calculation_profile(
or binding.lab_id != profile.lab_id
or binding.config_sha256 != definition.definition_sha256
or binding.result_kind != definition.result_contract.result_kind
or binding.replay_capability is None
or binding.replay_capability.kind != "portable-result-review"
or provenance["replay_capability"]
!= binding.replay_capability.as_dict()
):
return None
return profile.as_dict()
@@ -712,6 +731,7 @@ def _publication_provenance(
package: PortableResultPackageManifest,
artifact_manifest: ArtifactManifest,
profile: PortableCalculationProfilePolicy,
replay_capability: LabReplayCapability,
) -> dict[str, object]:
result_document = next(
artifact for artifact in package.artifacts if artifact.role == RESULT_DOCUMENT_ROLE
@@ -732,10 +752,11 @@ def _publication_provenance(
"result_document_sha256": result_document.sha256,
"artifacts": [artifact.as_dict() for artifact in package.artifacts],
},
"replay_capability": replay_capability.as_dict(),
"storage": {
"mode": "central-content-addressed-artifact-store",
"include_recorded_media": profile.include_recorded_media,
"replay_capability": None,
"replay_capability": replay_capability.as_dict(),
},
"method": _laboratory_method(job, definition),
}
@@ -0,0 +1,118 @@
"""Read-only universal viewer projection for published portable LAB results."""
from __future__ import annotations
import hashlib
import json
import re
from dataclasses import dataclass
from typing import Final
from k1link.artifact_gateway import ArtifactGatewayError, CentralArtifactStore
from k1link.observatory.portable_result_contract import RESULT_DOCUMENT_ROLE
from k1link.sessions import SessionNotFoundError, SessionStore
PORTABLE_RESULT_VIEW_SCHEMA: Final = "missioncore.observatory-portable-result-view/v1"
_RESULT_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
_MAX_RESULT_DOCUMENT_BYTES: Final = 8 * 1024 * 1024
class PortableResultViewError(RuntimeError):
"""A published result cannot be admitted to the generic viewer."""
@dataclass(frozen=True, slots=True)
class PortableResultViewService:
sessions: SessionStore
artifacts: CentralArtifactStore
def read(self, result_id: str) -> dict[str, object]:
if _RESULT_ID.fullmatch(result_id) is None:
raise ValueError("portable result id is invalid")
try:
binding = self.sessions.get_lab_instance(result_id)
except SessionNotFoundError as exc:
raise PortableResultViewError("portable result is unavailable") from exc
if (
binding is None
or binding.session_id != result_id
or binding.result_id != result_id
or binding.replay_capability is None
or binding.replay_capability.kind != "portable-result-review"
):
raise PortableResultViewError("portable result has no viewer capability")
provenance = binding.provenance
package = provenance.get("result_package")
source = provenance.get("source")
run_definition = provenance.get("run_definition")
calculation_profile = provenance.get("calculation_profile")
if (
provenance.get("schema_version")
!= "missioncore.observatory-portable-result-publication/v1"
or provenance.get("replay_capability")
!= binding.replay_capability.as_dict()
or not isinstance(package, dict)
or not isinstance(source, dict)
or source.get("session_id") != binding.source_session_id
or not isinstance(run_definition, dict)
or run_definition.get("definition_sha256") != binding.config_sha256
or not isinstance(calculation_profile, dict)
):
raise PortableResultViewError("portable result provenance is invalid")
manifest_id = package.get("artifact_manifest_id")
package_sha256 = package.get("manifest_sha256")
if (
not isinstance(manifest_id, str)
or _SHA256.fullmatch(manifest_id) is None
or not isinstance(package_sha256, str)
or _SHA256.fullmatch(package_sha256) is None
):
raise PortableResultViewError("portable result artifact identity is invalid")
try:
manifest = self.artifacts.read_manifest(manifest_id)
member = manifest.member(RESULT_DOCUMENT_ROLE)
path = self.artifacts.object_path(member.sha256)
payload = path.read_bytes()
except (ArtifactGatewayError, OSError) as exc:
raise PortableResultViewError("portable result artifacts are unavailable") from exc
if (
manifest.artifact_type != "observatory-portable-result"
or manifest.subject_id != result_id
or manifest.manifest_id != manifest_id
or manifest.metadata.get("package-sha256") != package_sha256
or package.get("schema_version")
!= "missioncore.observatory-portable-result-package/v1"
or package.get("result_document_sha256") != member.sha256
or member.media_type != "application/json"
or not 0 < member.byte_length <= _MAX_RESULT_DOCUMENT_BYTES
or len(payload) != member.byte_length
or hashlib.sha256(payload).hexdigest() != member.sha256
):
raise PortableResultViewError("portable result artifact integrity changed")
try:
result_document: object = json.loads(payload.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise PortableResultViewError("portable result document is invalid") from exc
if not isinstance(result_document, dict):
raise PortableResultViewError("portable result document must be an object")
return {
"schema_version": PORTABLE_RESULT_VIEW_SCHEMA,
"result_id": result_id,
"source_session_id": binding.source_session_id,
"result_kind": binding.result_kind,
"definition_sha256": binding.config_sha256,
"viewer_capability": binding.replay_capability.as_dict(),
"calculation_profile": calculation_profile,
"artifact_manifest_id": manifest.manifest_id,
"artifacts": [
{
"role": item.role,
"media_type": item.media_type,
"sha256": item.sha256,
"byte_length": item.byte_length,
}
for item in manifest.members
],
"result_document": result_document,
}
@@ -2,9 +2,9 @@
This module is deliberately only a composition boundary. It does not enable
the Worker router, install an executor, select commands, or grant production
authority. It binds the two admitted portable profiles to their exact result
contracts, then constructs the local artifact transport and verified result
publisher from server-owned dependencies.
authority. Validators are selected by the content identity of a result
contract, never by a LAB/setup ID, before constructing the local artifact
transport and verified result publisher from server-owned dependencies.
"""
from __future__ import annotations
@@ -19,7 +19,6 @@ from typing import Final
from k1link.artifact_gateway import CentralArtifactStore
from k1link.observatory.m49_portable_result import (
M49_PORTABLE_RESULT_CONTRACT_SHA256,
M49_PORTABLE_RESULT_SCHEMA,
validate_m49_portable_result,
)
from k1link.observatory.portable_artifact_transport import (
@@ -27,13 +26,10 @@ from k1link.observatory.portable_artifact_transport import (
PortableObservatoryArtifactTransport,
)
from k1link.observatory.portable_lab_v1_executor import (
PORTABLE_LAB_V1_RESULT_SCHEMA,
validate_lab_v1_result_v2,
)
from k1link.observatory.portable_result_contract import (
OBSERVATION_ONLY_AUTHORITY,
PortableCalculationProfileRegistry,
PortableResultContractValidator,
PortableResultContractValidatorRegistration,
PortableResultContractValidatorRegistry,
PortableResultPublicationBlockedError,
@@ -42,14 +38,10 @@ from k1link.observatory.portable_result_publisher import (
PortableObservatoryResultPublisher,
)
from k1link.observatory.portable_run_definitions import (
PORTABLE_RESULT_CONTRACT_SCHEMA,
PortableRunDefinition,
PortableRunDefinitionRegistry,
PortableRunDefinitionRegistryError,
)
from k1link.observatory.portable_setup_projection import (
PORTABLE_LAB_V1_SETUP_ID,
PORTABLE_M49_SETUP_ID,
portable_calculation_profile_registry,
)
from k1link.observatory.recorded_jobs import ObservatoryRecordedJobQueue
@@ -161,77 +153,54 @@ class PortableObservatoryWorkerIntegration:
supported_setup_ids: tuple[str, ...]
@dataclass(frozen=True, slots=True)
class _ValidatorSpec:
setup_id: str
definition_id: str
definition_version: int
contract_id: str
contract_version: int
result_schema: str
result_kind: str
contract_sha256: str
validator: PortableResultContractValidator
def expected_contract(self) -> dict[str, object]:
return {
"schema_version": PORTABLE_RESULT_CONTRACT_SCHEMA,
"contract_id": self.contract_id,
"version": self.contract_version,
"result_schema": self.result_schema,
"result_kind": self.result_kind,
"publication": "observatory",
"contract_sha256": self.contract_sha256,
}
_VALIDATOR_SPECS: Final = (
_ValidatorSpec(
setup_id=PORTABLE_LAB_V1_SETUP_ID,
definition_id="lab-v1-eomt-ddrnet-portable",
definition_version=2,
contract_id="recorded-eomt-ddrnet-review-v2",
contract_version=2,
result_schema=PORTABLE_LAB_V1_RESULT_SCHEMA,
result_kind="recorded-perception-qualification",
_BUILTIN_VALIDATOR_REGISTRATIONS: Final = (
PortableResultContractValidatorRegistration(
contract_sha256=PORTABLE_LAB_V1_RESULT_CONTRACT_SHA256,
validator=validate_lab_v1_result_v2,
),
_ValidatorSpec(
setup_id=PORTABLE_M49_SETUP_ID,
definition_id="m49-tgs-portable",
definition_version=3,
contract_id="m49-tgs-portable-review-v2",
contract_version=2,
result_schema=M49_PORTABLE_RESULT_SCHEMA,
result_kind="recorded-perception-qualification",
PortableResultContractValidatorRegistration(
contract_sha256=M49_PORTABLE_RESULT_CONTRACT_SHA256,
validator=validate_m49_portable_result,
),
)
_BUILTIN_VALIDATORS_BY_CONTRACT: Final = {
registration.contract_sha256: registration.validator
for registration in _BUILTIN_VALIDATOR_REGISTRATIONS
}
def portable_result_validator_registry(
definitions: PortableRunDefinitionRegistry,
*,
registrations: tuple[PortableResultContractValidatorRegistration, ...]
| None = None,
) -> PortableResultContractValidatorRegistry:
"""Bind both product profiles to fixed result contracts and validators."""
"""Select server-installed validators by exact result-contract identity."""
registrations: list[PortableResultContractValidatorRegistration] = []
for spec in _VALIDATOR_SPECS:
try:
definition = definitions.resolve_setup(spec.setup_id)
except PortableRunDefinitionRegistryError as exc:
raise PortableWorkerIntegrationError(
f"required portable setup is unavailable: {spec.setup_id}"
) from exc
_verify_validator_definition(definition, spec)
registrations.append(
PortableResultContractValidatorRegistration(
contract_sha256=spec.contract_sha256,
validator=spec.validator,
)
)
return PortableResultContractValidatorRegistry(tuple(registrations))
installed = (
_BUILTIN_VALIDATOR_REGISTRATIONS
if registrations is None
else registrations
)
by_contract = {
registration.contract_sha256: registration
for registration in PortableResultContractValidatorRegistry(installed).registrations
}
selected = tuple(
by_contract[definition.result_contract.contract_sha256]
for definition in definitions.definitions
if definition.result_contract.contract_sha256 in by_contract
)
registry = PortableResultContractValidatorRegistry(selected)
for definition in definitions.definitions:
if definition.executor.ready:
try:
registry.resolve(definition.result_contract.contract_sha256)
except PortableResultPublicationBlockedError as exc:
raise PortableWorkerIntegrationError(
"a ready portable definition has no exact result validator"
) from exc
return registry
def observatory_worker_local_enabled(
@@ -314,42 +283,43 @@ def build_portable_observatory_worker_integration(
validators=validator_registry,
artifact_transport=transport,
result_publisher=publisher,
supported_setup_ids=tuple(spec.setup_id for spec in _VALIDATOR_SPECS),
supported_setup_ids=tuple(
definition.setup_id
for definition in definitions.definitions
if any(
registration.contract_sha256
== definition.result_contract.contract_sha256
for registration in validator_registry.registrations
)
),
)
def _verify_validator_definition(
definition: PortableRunDefinition,
spec: _ValidatorSpec,
) -> None:
if (
definition.definition_id != spec.definition_id
or definition.version != spec.definition_version
or definition.result_contract.as_dict() != spec.expected_contract()
or definition.authority.as_dict() != OBSERVATION_ONLY_AUTHORITY
):
raise PortableWorkerIntegrationError(
f"portable result contract changed for setup: {spec.setup_id}"
)
def _verify_composition(
definitions: PortableRunDefinitionRegistry,
profiles: PortableCalculationProfileRegistry,
validators: PortableResultContractValidatorRegistry,
) -> None:
expected_validators = {spec.contract_sha256: spec.validator for spec in _VALIDATOR_SPECS}
for definition in definitions.definitions:
profiles.resolve(definition)
contract_sha256 = definition.result_contract.contract_sha256
if definition.executor.ready:
try:
validators.resolve(definition.result_contract.contract_sha256)
validators.resolve(contract_sha256)
except PortableResultPublicationBlockedError as exc:
raise PortableWorkerIntegrationError(
"a ready portable definition has no exact result validator"
) from exc
for contract_sha256, expected in expected_validators.items():
if validators.resolve(contract_sha256) is not expected:
expected = _BUILTIN_VALIDATORS_BY_CONTRACT.get(contract_sha256)
actual = next(
(
registration.validator
for registration in validators.registrations
if registration.contract_sha256 == contract_sha256
),
None,
)
if expected is not None and actual is not None and actual is not expected:
raise PortableWorkerIntegrationError(
"portable result validator registration changed identity"
)
@@ -19,7 +19,7 @@ import json
import re
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from pathlib import Path, PurePosixPath
from typing import Final, Literal, Protocol
from k1link.observatory.portable_run_definitions import (
@@ -40,7 +40,7 @@ PORTABLE_WORKER_RUNTIME_CANDIDATE_SCHEMA: Final = (
"missioncore.observatory-portable-worker-runtime-candidate/v1"
)
PORTABLE_WORKER_RUNTIME_PLAN_SCHEMA: Final = (
"missioncore.observatory-portable-worker-runtime-plan/v1"
"missioncore.observatory-portable-worker-runtime-plan/v2"
)
_MAX_REGISTRY_BYTES: Final = 256 * 1024
@@ -63,6 +63,7 @@ type RuntimeAssetKind = Literal[
"container-image",
"definition-component",
"local-file",
"local-tree",
"model-artifact",
]
type AssetVerificationState = Literal["matched", "missing", "mismatched"]
@@ -116,6 +117,7 @@ class PortableWorkerAssetRequirement:
"container-image",
"definition-component",
"local-file",
"local-tree",
"model-artifact",
):
raise PortableWorkerRuntimeRegistryError("runtime asset kind is invalid")
@@ -160,6 +162,18 @@ class PortableWorkerAssetRequirement:
raise PortableWorkerRuntimeRegistryError(
"model artifact requirement is incomplete"
)
elif self.kind == "local-tree":
if self.byte_length is None or any(
value is not None
for value in (
self.component_id,
self.model_release_id,
self.model_artifact_role,
)
):
raise PortableWorkerRuntimeRegistryError(
"local tree requirement is incomplete"
)
elif any(
value is not None
for value in (
@@ -478,6 +492,8 @@ def verify_local_assets(
continue
if requirement.kind == "container-image":
matched = binding.file_path is None and binding.image_sha256 == requirement.sha256
elif requirement.kind == "local-tree":
matched = _matches_tree(requirement, binding.file_path)
else:
matched = _matches_file(requirement, binding.file_path)
checks.append(
@@ -568,12 +584,23 @@ class PortableWorkerResultDraft:
@dataclass(frozen=True, slots=True)
class PortableWorkerRuntimePlan:
job_id: str
request_sha256: str
identity_sha256: str
submission_receipt_sha256: str
claim_generation: int
adapter_id: str
candidate_sha256: str
setup_id: str
definition_id: str
definition_version: int
definition_sha256: str
source_session_id: str
source_catalog_sha256: str
source_bundle_sha256: str
source_capability_manifest_sha256: str
source_adapter_id: str
source_adapter_version: int
source_adapter_sha256: str
result_contract_sha256: str
phases: tuple[str, ...]
@@ -582,19 +609,38 @@ class PortableWorkerRuntimePlan:
for value, label in (
(self.adapter_id, "runtime plan adapter id"),
(self.setup_id, "runtime plan setup id"),
(self.definition_id, "runtime plan definition id"),
(self.source_adapter_id, "runtime plan source adapter id"),
):
_pattern(value, _IDENTIFIER, label)
for value, label in (
(self.request_sha256, "runtime plan request sha256"),
(self.identity_sha256, "runtime plan identity sha256"),
(self.submission_receipt_sha256, "runtime plan submission receipt sha256"),
(self.candidate_sha256, "runtime plan candidate sha256"),
(self.definition_sha256, "runtime plan definition sha256"),
(self.source_catalog_sha256, "runtime plan source catalog sha256"),
(self.source_bundle_sha256, "runtime plan source bundle sha256"),
(
self.source_capability_manifest_sha256,
"runtime plan source capability sha256",
),
(self.source_adapter_sha256, "runtime plan source adapter sha256"),
(self.result_contract_sha256, "runtime plan result contract sha256"),
):
_digest(value, label)
_pattern(self.source_session_id, _SESSION_ID, "runtime plan source session id")
for integer_value, label in (
(self.claim_generation, "runtime plan claim generation"),
(self.definition_version, "runtime plan definition version"),
(self.source_adapter_version, "runtime plan source adapter version"),
):
if (
isinstance(integer_value, bool)
or not isinstance(integer_value, int)
or integer_value < 1
):
raise PortableWorkerRuntimeRegistryError(f"{label} is invalid")
if not self.phases or len(self.phases) != len(set(self.phases)):
raise PortableWorkerRuntimeRegistryError(
"runtime plan phases must be non-empty and unique"
@@ -606,12 +652,23 @@ class PortableWorkerRuntimePlan:
return {
"schema_version": PORTABLE_WORKER_RUNTIME_PLAN_SCHEMA,
"job_id": self.job_id,
"request_sha256": self.request_sha256,
"identity_sha256": self.identity_sha256,
"submission_receipt_sha256": self.submission_receipt_sha256,
"claim_generation": self.claim_generation,
"adapter_id": self.adapter_id,
"candidate_sha256": self.candidate_sha256,
"setup_id": self.setup_id,
"definition_id": self.definition_id,
"definition_version": self.definition_version,
"definition_sha256": self.definition_sha256,
"source_session_id": self.source_session_id,
"source_catalog_sha256": self.source_catalog_sha256,
"source_bundle_sha256": self.source_bundle_sha256,
"source_capability_manifest_sha256": self.source_capability_manifest_sha256,
"source_adapter_id": self.source_adapter_id,
"source_adapter_version": self.source_adapter_version,
"source_adapter_sha256": self.source_adapter_sha256,
"result_contract_sha256": self.result_contract_sha256,
"phases": list(self.phases),
"authority": dict(_AUTHORITY),
@@ -687,12 +744,23 @@ class PortableWorkerExecutorAdapter:
)
plan = PortableWorkerRuntimePlan(
job_id=job.job_id,
request_sha256=job.request_sha256,
identity_sha256=job.identity_sha256,
submission_receipt_sha256=job.submission_receipt_sha256,
claim_generation=job.claim_generation,
adapter_id=self.candidate.adapter_id,
candidate_sha256=self.candidate.candidate_sha256,
setup_id=job.setup_id,
definition_id=job.definition_id,
definition_version=job.definition_version,
definition_sha256=job.definition_sha256,
source_session_id=job.source_session_id,
source_catalog_sha256=job.source_catalog_sha256,
source_bundle_sha256=job.source_bundle_sha256,
source_capability_manifest_sha256=job.source_capability_manifest_sha256,
source_adapter_id=job.source_adapter_id,
source_adapter_version=job.source_adapter_version,
source_adapter_sha256=job.source_adapter_sha256,
result_contract_sha256=self.candidate.result_contract_sha256,
phases=tuple(phase.phase_id for phase in self.candidate.phases),
)
@@ -758,6 +826,160 @@ def _matches_file(
return False
def _matches_tree(
requirement: PortableWorkerAssetRequirement,
path: Path | None,
) -> bool:
"""Verify an install-time sealed tree without re-reading multi-GB assets."""
if path is None or requirement.byte_length is None:
return False
root = path.expanduser().absolute()
receipt_path = root / "tree-receipt.json"
manifest_path = root / "tree-manifest.tsv"
try:
if root.is_symlink() or not root.is_dir():
return False
if (
receipt_path.is_symlink()
or not receipt_path.is_file()
or manifest_path.is_symlink()
or not manifest_path.is_file()
):
return False
receipt_payload = receipt_path.read_bytes()
manifest_payload = manifest_path.read_bytes()
if not 0 < len(receipt_payload) <= 64 * 1024:
return False
if not 0 < len(manifest_payload) <= 16 * 1024 * 1024:
return False
receipt_value: object = json.loads(receipt_payload.decode("utf-8"))
if not isinstance(receipt_value, dict):
return False
receipt = receipt_value
base_keys = {
"schema_version",
"asset_id",
"identity_algorithm",
"identity_sha256",
"file_count",
"byte_length",
"manifest_relative_path",
}
provenance_keys = {"source_image_sha256", "source_path", "binaries"}
receipt_keys = set(receipt)
if receipt_keys != base_keys and receipt_keys != base_keys | provenance_keys:
return False
if (
receipt["schema_version"] != "missioncore.sealed-tree-runtime/v1"
or receipt["asset_id"] != requirement.asset_id
or receipt["identity_algorithm"]
!= "relative-path-tab-size-tab-file-sha256-lf/v1"
or receipt["identity_sha256"] != requirement.sha256
or receipt["byte_length"] != requirement.byte_length
or receipt["manifest_relative_path"] != "tree-manifest.tsv"
or hashlib.sha256(manifest_payload).hexdigest() != requirement.sha256
):
return False
rows = _sealed_tree_rows(manifest_payload)
if receipt["file_count"] != len(rows):
return False
if sum(byte_length for _, byte_length, _ in rows) != requirement.byte_length:
return False
if receipt_keys == base_keys | provenance_keys and not _matches_tree_provenance(
receipt,
rows,
):
return False
for relative_path, byte_length, _sha256 in rows:
member = root.joinpath(*PurePosixPath(relative_path).parts)
if member.is_symlink() or not member.is_file() or member.stat().st_size != byte_length:
return False
return True
except (OSError, UnicodeDecodeError, json.JSONDecodeError, ValueError):
return False
def _matches_tree_provenance(
receipt: Mapping[str, object],
rows: tuple[tuple[str, int, str], ...],
) -> bool:
source_image_sha256 = receipt.get("source_image_sha256")
source_path_value = receipt.get("source_path")
binaries = receipt.get("binaries")
if (
not isinstance(source_image_sha256, str)
or _SHA256.fullmatch(source_image_sha256) is None
or not isinstance(source_path_value, str)
or not source_path_value
or not isinstance(binaries, dict)
or not binaries
):
return False
source_path = PurePosixPath(source_path_value)
if not source_path.is_absolute() or ".." in source_path.parts:
return False
by_path = {
relative_path: (byte_length, sha256)
for relative_path, byte_length, sha256 in rows
}
for binary_id, value in binaries.items():
if (
not isinstance(binary_id, str)
or _IDENTIFIER.fullmatch(binary_id) is None
or not isinstance(value, dict)
or set(value) != {"relative_path", "byte_length", "sha256"}
):
return False
relative_path = value["relative_path"]
byte_length = value["byte_length"]
sha256 = value["sha256"]
if (
not isinstance(relative_path, str)
or not isinstance(byte_length, int)
or isinstance(byte_length, bool)
or byte_length < 0
or not isinstance(sha256, str)
or _SHA256.fullmatch(sha256) is None
or by_path.get(relative_path) != (byte_length, sha256)
):
return False
return True
def _sealed_tree_rows(payload: bytes) -> tuple[tuple[str, int, str], ...]:
text = payload.decode("utf-8")
if not text.endswith("\n"):
raise ValueError("sealed tree manifest is not newline terminated")
rows: list[tuple[str, int, str]] = []
previous_path: str | None = None
for line in text.splitlines():
parts = line.split("\t")
if len(parts) != 3:
raise ValueError("sealed tree manifest row changed")
relative_path, byte_length_text, sha256 = parts
path = PurePosixPath(relative_path)
if (
not relative_path
or path.is_absolute()
or ".." in path.parts
or "\\" in relative_path
or relative_path in {"tree-manifest.tsv", "tree-receipt.json"}
or previous_path is not None
and relative_path <= previous_path
or _SHA256.fullmatch(sha256) is None
):
raise ValueError("sealed tree manifest identity changed")
byte_length = int(byte_length_text)
if byte_length < 0 or str(byte_length) != byte_length_text:
raise ValueError("sealed tree manifest byte length changed")
rows.append((relative_path, byte_length, sha256))
previous_path = relative_path
if not rows:
raise ValueError("sealed tree manifest is empty")
return tuple(rows)
def _candidate(value: object) -> PortableWorkerRuntimeCandidate:
row = _object(value, "runtime candidate")
_exact_keys(
@@ -844,6 +1066,7 @@ def _asset(value: object) -> PortableWorkerAssetRequirement:
"container-image",
"definition-component",
"local-file",
"local-tree",
"model-artifact",
):
raise PortableWorkerRuntimeRegistryError("runtime asset kind is invalid")
+322 -17
View File
@@ -44,6 +44,10 @@ OBSERVATORY_LIVE_LEASE_REQUEST_SCHEMA: Final = "missioncore.observatory-live-k1-
RECORDED_JOB_DATABASE_NAME: Final = "observatory-recorded-jobs.sqlite3"
MAX_RECORDED_JOBS: Final = 10_000
MAX_RECORDED_CLAIM_RECEIPTS: Final = 50_000
# Every capability expands to four SQLite bind parameters. Keeping the public
# bound at 128 stays comfortably below SQLite's traditional 999-variable limit
# even when the runtime was compiled with conservative defaults.
MAX_RECORDED_EXECUTOR_CAPABILITIES: Final = 128
MAX_LIVE_LEASES: Final = 10_000
MAX_RECORDED_JOB_STORAGE_BYTES: Final = 128 * 1024 * 1024
RECORDED_JOB_SQLITE_LOCK_TIMEOUT_SECONDS: Final = 0.1
@@ -67,6 +71,7 @@ type RecordedJobState = Literal[
"reconciliation-required",
]
type CheckpointPolicy = Literal["cooperative", "non-checkpointable"]
type PublicationState = Literal["not-required", "pending", "failed", "published"]
type LiveLeaseState = Literal["pending", "active", "completed", "failed", "cancelled"]
type LiveTerminalOutcome = Literal["completed", "failed", "cancelled"]
@@ -139,6 +144,12 @@ CREATE TABLE IF NOT EXISTS observatory_recorded_jobs (
terminal_code TEXT,
terminal_message TEXT,
terminal_claim_token_sha256 TEXT,
publication_state TEXT NOT NULL DEFAULT 'not-required'
CHECK (publication_state IN ('not-required', 'pending', 'failed', 'published')),
publication_attempts INTEGER NOT NULL DEFAULT 0
CHECK (publication_attempts >= 0),
publication_error TEXT,
published_at_utc TEXT,
created_at_utc TEXT NOT NULL,
updated_at_utc TEXT NOT NULL
);
@@ -256,6 +267,33 @@ class ObservatoryRecordedPreemptionError(ObservatoryRecordedQueueError):
"""The scheduler could not prove immediate release for live K1."""
@dataclass(frozen=True, slots=True, order=True)
class RecordedExecutorIdentity:
"""Path-free executor capability shared by scheduling and Worker code."""
release_sha256: str
image_sha256: str
model_manifest_sha256: str
resource_profile_sha256: str
def __post_init__(self) -> None:
for value, label in (
(self.release_sha256, "executor release sha256"),
(self.image_sha256, "executor image sha256"),
(self.model_manifest_sha256, "model manifest sha256"),
(self.resource_profile_sha256, "resource profile sha256"),
):
_validate_digest(value, label)
def as_dict(self) -> dict[str, str]:
return {
"release_sha256": self.release_sha256,
"image_sha256": self.image_sha256,
"model_manifest_sha256": self.model_manifest_sha256,
"resource_profile_sha256": self.resource_profile_sha256,
}
@dataclass(frozen=True, slots=True)
class RecordedRunDefinition:
"""Server-owned executable identity; it contains no executable text or path."""
@@ -432,6 +470,10 @@ class ObservatoryRecordedJob:
terminal_code: str | None
terminal_message: str | None
terminal_claim_token_sha256: str | None
publication_state: PublicationState
publication_attempts: int
publication_error: str | None
published_at_utc: str | None
created_at_utc: str
updated_at_utc: str
priority_class: Literal["recorded"] = "recorded"
@@ -557,6 +599,59 @@ class ObservatoryRecordedJob:
_SHA256,
"terminal claim token sha256",
)
if self.publication_state not in (
"not-required",
"pending",
"failed",
"published",
):
raise ObservatoryRecordedQueueIntegrityError(
"recorded-job publication state is invalid"
)
if self.publication_attempts < 0:
raise ObservatoryRecordedQueueIntegrityError(
"recorded-job publication attempt count is invalid"
)
if self.publication_error is not None:
_validate_text(
self.publication_error,
"recorded-job publication error",
max_length=1_000,
)
if self.published_at_utc is not None:
_validate_timestamp(self.published_at_utc, "recorded-job publication timestamp")
publication_shape = {
"not-required": (
self.publication_attempts == 0
and self.publication_error is None
and self.published_at_utc is None
),
"pending": (
self.publication_error is None and self.published_at_utc is None
),
"failed": (
self.publication_attempts >= 1
and self.publication_error is not None
and self.published_at_utc is None
),
"published": (
self.publication_attempts >= 1
and self.publication_error is None
and self.published_at_utc is not None
),
}
if not publication_shape[self.publication_state]:
raise ObservatoryRecordedQueueIntegrityError(
"recorded-job publication receipt is inconsistent"
)
if self.publication_state != "not-required" and (
self.state != "succeeded"
or self.result_id is None
or self.result_sha256 is None
):
raise ObservatoryRecordedQueueIntegrityError(
"recorded-job publication lifecycle has no execution result"
)
_validate_timestamp(self.created_at_utc, "created timestamp")
_validate_timestamp(self.updated_at_utc, "updated timestamp")
@@ -630,6 +725,12 @@ class ObservatoryRecordedJob:
if self.terminal_code is None
else {"code": self.terminal_code, "message": self.terminal_message}
),
"publication": {
"state": self.publication_state,
"attempts": self.publication_attempts,
"error": self.publication_error,
"published_at_utc": self.published_at_utc,
},
"created_at_utc": self.created_at_utc,
"updated_at_utc": self.updated_at_utc,
"authority": dict(_AUTHORITY),
@@ -1192,12 +1293,23 @@ class ObservatoryRecordedJobQueue:
*,
claimant_id: str,
claim_request_id: str,
supported_executor_identities: tuple[RecordedExecutorIdentity, ...] | None = None,
) -> ObservatoryRecordedClaim | None:
"""Claim one recorded job atomically; even an empty claim is idempotent."""
"""Claim one compatible job atomically; even an empty claim is idempotent.
``None`` preserves the legacy v1 claim semantics during rollout. A
concrete tuple is the capability-aware v2 contract; an empty tuple
deliberately claims nothing.
"""
_validate_pattern(claimant_id, _IDENTIFIER, "claimant id")
_validate_pattern(claim_request_id, _IDEMPOTENCY_KEY, "claim request id")
request_sha256 = _claim_request_sha256(claimant_id, claim_request_id)
capabilities = _canonical_executor_capabilities(supported_executor_identities)
request_sha256 = _claim_request_sha256(
claimant_id,
claim_request_id,
supported_executor_identities=capabilities,
)
with self._transaction() as connection:
now = self._timestamp()
self._recover_stale_claims(connection, now=now)
@@ -1226,11 +1338,10 @@ class ObservatoryRecordedJobQueue:
"LIMIT 1"
).fetchone()
if active_owner is None:
row = connection.execute(
"SELECT job_id FROM observatory_recorded_jobs "
"WHERE state = 'queued' "
"ORDER BY priority_rank, created_at_utc, job_id LIMIT 1"
).fetchone()
row = self._next_compatible_queued_job(
connection,
supported_executor_identities=capabilities,
)
if row is None:
connection.execute(
"INSERT INTO observatory_recorded_claim_receipts "
@@ -1289,6 +1400,44 @@ class ObservatoryRecordedJobQueue:
job=self._get_job(connection, job_id),
)
@staticmethod
def _next_compatible_queued_job(
connection: sqlite3.Connection,
*,
supported_executor_identities: tuple[RecordedExecutorIdentity, ...] | None,
) -> sqlite3.Row | None:
if supported_executor_identities is None:
row: sqlite3.Row | None = connection.execute(
"SELECT job_id FROM observatory_recorded_jobs "
"WHERE state = 'queued' "
"ORDER BY priority_rank, created_at_utc, job_id LIMIT 1"
).fetchone()
return row
if not supported_executor_identities:
return None
predicates = " OR ".join(
"(executor_release_sha256 = ? AND executor_image_sha256 = ? "
"AND model_manifest_sha256 = ? AND resource_profile_sha256 = ?)"
for _identity in supported_executor_identities
)
parameters = tuple(
value
for identity in supported_executor_identities
for value in (
identity.release_sha256,
identity.image_sha256,
identity.model_manifest_sha256,
identity.resource_profile_sha256,
)
)
row = connection.execute(
"SELECT job_id FROM observatory_recorded_jobs "
f"WHERE state = 'queued' AND ({predicates}) "
"ORDER BY priority_rank, created_at_utc, job_id LIMIT 1",
parameters,
).fetchone()
return row
def renew_claim(
self,
job_id: str,
@@ -1653,6 +1802,96 @@ class ObservatoryRecordedJobQueue:
terminal_message="Recorded result was sealed by the Worker.",
)
def complete_for_publication(
self,
job_id: str,
*,
claim_token: str,
result_id: str,
result_sha256: str,
) -> ObservatoryRecordedJob:
"""Seal execution and atomically enqueue its verified publication."""
_validate_pattern(result_id, _SESSION_ID, "result id")
_validate_digest(result_sha256, "result sha256")
return self._terminal_job_transition(
job_id,
claim_token=claim_token,
state="succeeded",
result_id=result_id,
result_sha256=result_sha256,
terminal_code="result-sealed",
terminal_message="Recorded result was sealed by the Worker.",
publication_state="pending",
)
def mark_publication_failed(
self,
job_id: str,
*,
message: str,
) -> ObservatoryRecordedJob:
"""Record one failed outbox attempt without losing the execution result."""
_validate_pattern(job_id, _JOB_ID, "recorded job id")
_validate_text(message, "recorded publication error", max_length=1_000)
with self._transaction() as connection:
job = self._get_job(connection, job_id)
if job.state != "succeeded" or job.publication_state not in {
"pending",
"failed",
}:
raise ObservatoryRecordedQueueConflictError(
"recorded result is not awaiting publication"
)
connection.execute(
"UPDATE observatory_recorded_jobs "
"SET publication_state = 'failed', "
"publication_attempts = publication_attempts + 1, "
"publication_error = ?, published_at_utc = NULL, "
"updated_at_utc = ? WHERE job_id = ?",
(message, self._timestamp(), job_id),
)
return self._get_job(connection, job_id)
def mark_published(self, job_id: str) -> ObservatoryRecordedJob:
"""Acknowledge one idempotently published outbox entry."""
_validate_pattern(job_id, _JOB_ID, "recorded job id")
with self._transaction() as connection:
job = self._get_job(connection, job_id)
if job.state != "succeeded":
raise ObservatoryRecordedQueueConflictError(
"recorded execution has not succeeded"
)
if job.publication_state == "published":
return job
if job.publication_state not in {"pending", "failed"}:
raise ObservatoryRecordedQueueConflictError(
"recorded result has no publication outbox entry"
)
now = self._timestamp()
connection.execute(
"UPDATE observatory_recorded_jobs "
"SET publication_state = 'published', "
"publication_attempts = publication_attempts + 1, "
"publication_error = NULL, published_at_utc = ?, "
"updated_at_utc = ? WHERE job_id = ?",
(now, now, job_id),
)
return self._get_job(connection, job_id)
def pending_publications(self) -> tuple[ObservatoryRecordedJob, ...]:
"""Return durable outbox entries in deterministic retry order."""
with self._read_connection() as connection:
rows = connection.execute(
"SELECT * FROM observatory_recorded_jobs "
"WHERE publication_state IN ('pending', 'failed') "
"ORDER BY created_at_utc, job_id"
).fetchall()
return tuple(_job_from_row(row) for row in rows)
def fail(
self,
job_id: str,
@@ -2038,6 +2277,7 @@ class ObservatoryRecordedJobQueue:
result_sha256: str | None,
terminal_code: str,
terminal_message: str,
publication_state: Literal["not-required", "pending"] = "not-required",
) -> ObservatoryRecordedJob:
_validate_pattern(job_id, _JOB_ID, "recorded job id")
_validate_pattern(claim_token, _TOKEN, "claim token")
@@ -2053,6 +2293,11 @@ class ObservatoryRecordedJobQueue:
and job.terminal_code == terminal_code
and job.terminal_message == terminal_message
and job.terminal_claim_token_sha256 == token_sha256
and (
job.publication_state == "not-required"
if publication_state == "not-required"
else job.publication_state in {"pending", "failed", "published"}
)
)
if exact_replay:
return job
@@ -2091,7 +2336,9 @@ class ObservatoryRecordedJobQueue:
"terminal_claim_token_sha256 = ?, active_claim_token = NULL, "
"active_claimant_id = NULL, claimed_at_utc = NULL, "
"claim_expires_at_utc = NULL, claim_heartbeat_at_utc = NULL, "
"claim_renewal_count = 0, updated_at_utc = ? WHERE job_id = ?",
"claim_renewal_count = 0, publication_state = ?, "
"publication_attempts = 0, publication_error = NULL, "
"published_at_utc = NULL, updated_at_utc = ? WHERE job_id = ?",
(
state,
result_id,
@@ -2099,6 +2346,7 @@ class ObservatoryRecordedJobQueue:
terminal_code,
terminal_message,
token_sha256,
publication_state,
now,
job_id,
),
@@ -2387,6 +2635,7 @@ class ObservatoryRecordedJobQueue:
with self._connect() as connection:
connection.executescript(_SCHEMA_SQL)
self._migrate_claim_lease_schema(connection)
self._migrate_publication_schema(connection)
self._validate_schema(connection)
self._validate_existing_capacity(connection)
connection.commit()
@@ -2403,7 +2652,7 @@ class ObservatoryRecordedJobQueue:
def _validate_schema(self, connection: sqlite3.Connection) -> None:
expected = {
"observatory_recorded_jobs": 46,
"observatory_recorded_jobs": 50,
"observatory_recorded_claim_receipts": 6,
"observatory_recorded_reconciliations": 18,
"observatory_live_leases": 13,
@@ -2508,6 +2757,35 @@ class ObservatoryRecordedJobQueue:
"legacy active claim is stored in an invalid state"
)
def _migrate_publication_schema(self, connection: sqlite3.Connection) -> None:
"""Add the durable result-publication outbox to existing queues."""
columns = {
str(row["name"])
for row in connection.execute(
"SELECT name FROM pragma_table_info('observatory_recorded_jobs')"
).fetchall()
}
additions = (
(
"publication_state",
"TEXT NOT NULL DEFAULT 'not-required' "
"CHECK (publication_state IN "
"('not-required', 'pending', 'failed', 'published'))",
),
(
"publication_attempts",
"INTEGER NOT NULL DEFAULT 0 CHECK (publication_attempts >= 0)",
),
("publication_error", "TEXT"),
("published_at_utc", "TEXT"),
)
for name, definition in additions:
if name not in columns:
connection.execute(
f"ALTER TABLE observatory_recorded_jobs ADD COLUMN {name} {definition}"
)
def _validate_existing_capacity(self, connection: sqlite3.Connection) -> None:
for table, limit, label in (
("observatory_recorded_jobs", self._max_jobs, "recorded job"),
@@ -2724,6 +3002,10 @@ def _job_from_row(row: sqlite3.Row) -> ObservatoryRecordedJob:
terminal_code=row["terminal_code"],
terminal_message=row["terminal_message"],
terminal_claim_token_sha256=row["terminal_claim_token_sha256"],
publication_state=row["publication_state"],
publication_attempts=row["publication_attempts"],
publication_error=row["publication_error"],
published_at_utc=row["published_at_utc"],
created_at_utc=row["created_at_utc"],
updated_at_utc=row["updated_at_utc"],
)
@@ -2801,14 +3083,37 @@ def _submission_receipt_sha256(
)
def _claim_request_sha256(claimant_id: str, claim_request_id: str) -> str:
return _sha256(
{
"schema_version": OBSERVATORY_RECORDED_CLAIM_SCHEMA,
"claim_request_id": claim_request_id,
"claimant_id": claimant_id,
}
)
def _claim_request_sha256(
claimant_id: str,
claim_request_id: str,
*,
supported_executor_identities: tuple[RecordedExecutorIdentity, ...] | None = None,
) -> str:
document: dict[str, object] = {
"schema_version": OBSERVATORY_RECORDED_CLAIM_SCHEMA,
"claim_request_id": claim_request_id,
"claimant_id": claimant_id,
}
if supported_executor_identities is not None:
document["supported_executor_identities"] = [
identity.as_dict() for identity in supported_executor_identities
]
return _sha256(document)
def _canonical_executor_capabilities(
identities: tuple[RecordedExecutorIdentity, ...] | None,
) -> tuple[RecordedExecutorIdentity, ...] | None:
if identities is None:
return None
if len(identities) > MAX_RECORDED_EXECUTOR_CAPABILITIES:
raise ValueError("too many recorded executor capabilities")
if any(not isinstance(identity, RecordedExecutorIdentity) for identity in identities):
raise ValueError("recorded executor capability is invalid")
canonical = tuple(sorted(identities))
if len(canonical) != len(set(canonical)):
raise ValueError("recorded executor capabilities must be unique")
return canonical
def _recorded_job_states() -> frozenset[str]:
+34 -20
View File
@@ -29,6 +29,7 @@ from k1link.observatory.recorded_jobs import (
OBSERVATORY_RECORDED_CLAIM_SCHEMA,
OBSERVATORY_RECORDED_JOB_REQUEST_SCHEMA,
OBSERVATORY_RECORDED_JOB_SCHEMA,
RecordedExecutorIdentity,
)
WORKER_006_CONTOUR_ID: Final = "worker-006"
@@ -53,6 +54,7 @@ type WorkerCycleState = Literal[
"failed",
"rejected",
"lease-lost",
"publication-pending",
]
type RecordedJobWireState = Literal[
"accepted",
@@ -97,24 +99,7 @@ class ObservatoryWorkerExecutorUnavailableError(ObservatoryWorkerAgentError):
"""No local adapter matches the exact sealed executor identity."""
@dataclass(frozen=True, slots=True)
class ObservatoryWorkerExecutorIdentity:
"""The only identity that may select executable Worker code."""
release_sha256: str
image_sha256: str
model_manifest_sha256: str
resource_profile_sha256: str
def __post_init__(self) -> None:
for label, value in (
("executor release", self.release_sha256),
("executor image", self.image_sha256),
("model manifest", self.model_manifest_sha256),
("resource profile", self.resource_profile_sha256),
):
if re.fullmatch(_SHA256_PATTERN, value) is None:
raise ValueError(f"{label} SHA-256 is invalid")
ObservatoryWorkerExecutorIdentity = RecordedExecutorIdentity
@dataclass(frozen=True, slots=True)
@@ -201,6 +186,12 @@ class ObservatoryWorkerExecutorRegistry:
"exact executor identity is not locally allowlisted"
)
@property
def supported_identities(self) -> tuple[ObservatoryWorkerExecutorIdentity, ...]:
"""Canonical capability snapshot sent with every claim request."""
return tuple(sorted(registration.identity for registration in self.registrations))
class ObservatoryWorkerTransport(Protocol):
"""State-transition port implemented by HTTP, IPC, or a test transport."""
@@ -210,6 +201,7 @@ class ObservatoryWorkerTransport(Protocol):
*,
claimant_id: str,
claim_request_id: str,
supported_executor_identities: tuple[ObservatoryWorkerExecutorIdentity, ...],
) -> Mapping[str, object] | None: ...
def start(
@@ -332,6 +324,13 @@ class _ClaimLeasePayload(_StrictPayload):
renewal_count: int = Field(ge=0)
class _PublicationPayload(_StrictPayload):
state: Literal["not-required", "pending", "failed", "published"]
attempts: int = Field(ge=0)
error: str | None
published_at_utc: Timestamp | None
class _RecordedJobPayload(_StrictPayload):
schema_version: Literal["missioncore.observatory-recorded-job/v1"]
job_id: str = Field(pattern=_JOB_ID_PATTERN)
@@ -356,6 +355,7 @@ class _RecordedJobPayload(_StrictPayload):
claim_lease: _ClaimLeasePayload | None
result: _ResultPayload | None
terminal: _TerminalPayload | None
publication: _PublicationPayload
created_at_utc: Timestamp
updated_at_utc: Timestamp
authority: _AuthorityPayload
@@ -423,6 +423,7 @@ class ObservatoryWorkerAgent:
payload = self._transport.claim_next(
claimant_id=WORKER_006_CONTOUR_ID,
claim_request_id=claim_request_id,
supported_executor_identities=self._executors.supported_identities,
)
if payload is None:
return ObservatoryWorkerCycleReport(
@@ -430,7 +431,11 @@ class ObservatoryWorkerAgent:
claim_request_id=claim_request_id,
)
try:
claim = _validate_claim(payload, claim_request_id=claim_request_id)
claim = _validate_claim(
payload,
claim_request_id=claim_request_id,
supported_executor_identities=self._executors.supported_identities,
)
except ObservatoryWorkerClaimRejectedError:
return ObservatoryWorkerCycleReport(
state="rejected",
@@ -554,7 +559,11 @@ class ObservatoryWorkerAgent:
"Worker success acknowledgement changed result identity"
)
return ObservatoryWorkerCycleReport(
state="succeeded",
state=(
"publication-pending"
if succeeded.publication.state in {"pending", "failed"}
else "succeeded"
),
claim_request_id=claim_request_id,
job_id=claim.job.job_id,
result_id=result.result_id,
@@ -651,6 +660,7 @@ def _validate_claim(
payload: Mapping[str, object],
*,
claim_request_id: str,
supported_executor_identities: tuple[ObservatoryWorkerExecutorIdentity, ...],
) -> _ValidatedClaim:
try:
claim = _RecordedClaimPayload.model_validate(dict(payload))
@@ -663,6 +673,10 @@ def _validate_claim(
"schema_version": OBSERVATORY_RECORDED_CLAIM_SCHEMA,
"claim_request_id": claim_request_id,
"claimant_id": WORKER_006_CONTOUR_ID,
"supported_executor_identities": [
identity.as_dict()
for identity in sorted(supported_executor_identities)
],
}
)
if claim.request_sha256 != expected_claim_request_sha256:
@@ -0,0 +1,157 @@
"""Fixed loopback bridge for Observatory Worker containers on Docker Desktop.
The authenticated Worker HTTP gateway accepts plaintext only on a loopback
URL. Docker Desktop exposes the Windows host as ``host.docker.internal``, so a
container-owned bridge binds one fixed loopback socket and forwards it to the
Mac-owned reverse SSH tunnel on the Worker host. No address, port, credential,
or destination is supplied by a queued job.
"""
from __future__ import annotations
import shutil
import socket
import socketserver
import threading
from contextlib import suppress
from typing import Final
CONTAINER_PROXY_LISTEN_HOST: Final = "127.0.0.1"
CONTAINER_PROXY_LISTEN_PORT: Final = 18080
CONTAINER_PROXY_UPSTREAM_HOST: Final = "host.docker.internal"
CONTAINER_PROXY_UPSTREAM_PORT: Final = 18080
CONTAINER_PROXY_CONNECT_TIMEOUT_SECONDS: Final = 10.0
CONTAINER_PROXY_COPY_BYTES: Final = 1024 * 1024
class ObservatoryWorkerContainerProxyError(RuntimeError):
"""The fixed container loopback bridge could not be started safely."""
class _ThreadedTcpServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = True
class _FixedProxyHandler(socketserver.BaseRequestHandler):
server: _ThreadedTcpServer
def handle(self) -> None:
upstream_address = getattr(self.server, "upstream_address", None)
connect_timeout = getattr(self.server, "connect_timeout", None)
if (
not isinstance(upstream_address, tuple)
or len(upstream_address) != 2
or not isinstance(upstream_address[0], str)
or not isinstance(upstream_address[1], int)
or not isinstance(connect_timeout, float)
):
return
try:
upstream = socket.create_connection(
upstream_address,
timeout=connect_timeout,
)
except OSError:
return
with upstream:
upstream.settimeout(None)
client = self.request
if not isinstance(client, socket.socket):
return
client.settimeout(None)
client_to_upstream = threading.Thread(
target=_copy_socket,
args=(client, upstream),
daemon=True,
name="observatory-proxy-client-to-host",
)
upstream_to_client = threading.Thread(
target=_copy_socket,
args=(upstream, client),
daemon=True,
name="observatory-proxy-host-to-client",
)
client_to_upstream.start()
upstream_to_client.start()
client_to_upstream.join()
upstream_to_client.join()
class FixedObservatoryContainerLoopbackProxy:
"""Own one bounded TCP bridge for the lifetime of a Worker process."""
def __init__(
self,
*,
listen_host: str = CONTAINER_PROXY_LISTEN_HOST,
listen_port: int = CONTAINER_PROXY_LISTEN_PORT,
upstream_host: str = CONTAINER_PROXY_UPSTREAM_HOST,
upstream_port: int = CONTAINER_PROXY_UPSTREAM_PORT,
connect_timeout: float = CONTAINER_PROXY_CONNECT_TIMEOUT_SECONDS,
) -> None:
if listen_host != CONTAINER_PROXY_LISTEN_HOST:
raise ValueError("Observatory container proxy must bind IPv4 loopback")
if not 0 <= listen_port <= 65_535:
raise ValueError("Observatory container proxy listen port is invalid")
if upstream_host != CONTAINER_PROXY_UPSTREAM_HOST and upstream_host != "127.0.0.1":
raise ValueError("Observatory container proxy upstream host is invalid")
if not 1 <= upstream_port <= 65_535:
raise ValueError("Observatory container proxy upstream port is invalid")
if not 0.05 <= connect_timeout <= 60.0:
raise ValueError("Observatory container proxy timeout is invalid")
try:
server = _ThreadedTcpServer(
(listen_host, listen_port),
_FixedProxyHandler,
bind_and_activate=True,
)
except OSError as exc:
raise ObservatoryWorkerContainerProxyError(
"Observatory container loopback proxy could not bind"
) from exc
server.upstream_address = (upstream_host, upstream_port) # type: ignore[attr-defined]
server.connect_timeout = float(connect_timeout) # type: ignore[attr-defined]
self._server = server
self._thread = threading.Thread(
target=server.serve_forever,
kwargs={"poll_interval": 0.1},
daemon=True,
name="observatory-container-loopback-proxy",
)
@property
def listen_port(self) -> int:
address = self._server.server_address
if not isinstance(address, tuple) or not isinstance(address[1], int):
raise ObservatoryWorkerContainerProxyError(
"Observatory proxy address is invalid"
)
return address[1]
def __enter__(self) -> FixedObservatoryContainerLoopbackProxy:
self._thread.start()
return self
def __exit__(self, *_args: object) -> None:
self._server.shutdown()
self._server.server_close()
self._thread.join(timeout=5.0)
if self._thread.is_alive():
raise ObservatoryWorkerContainerProxyError(
"Observatory container loopback proxy did not stop"
)
def _copy_socket(source: socket.socket, destination: socket.socket) -> None:
try:
shutil.copyfileobj(
source.makefile("rb", buffering=0),
destination.makefile("wb", buffering=0),
length=CONTAINER_PROXY_COPY_BYTES,
)
except OSError:
pass
finally:
with suppress(OSError):
destination.shutdown(socket.SHUT_WR)
@@ -54,6 +54,7 @@ from k1link.observatory.source_admission import (
from k1link.observatory.worker_agent import (
WORKER_006_CONTOUR_ID,
ObservatoryWorkerExecutionResult,
ObservatoryWorkerExecutorIdentity,
ObservatoryWorkerTransport,
SealedObservatoryRecordedJob,
)
@@ -205,14 +206,19 @@ class ObservatoryWorkerHttpGateway(ObservatoryWorkerTransport):
*,
claimant_id: str,
claim_request_id: str,
supported_executor_identities: tuple[ObservatoryWorkerExecutorIdentity, ...],
) -> Mapping[str, object] | None:
self._require_claimant(claimant_id)
payload = self._json_request(
"POST",
"/api/v1/worker/observatory/recorded-jobs/claims",
json_body={
"schema_version": "missioncore.observatory-worker-claim-request/v1",
"schema_version": "missioncore.observatory-worker-claim-request/v2",
"claim_request_id": claim_request_id,
"supported_executor_identities": [
identity.as_dict()
for identity in sorted(supported_executor_identities)
],
},
allow_empty=True,
)
+126
View File
@@ -21,6 +21,11 @@ from urllib.parse import urlsplit
import httpx
from k1link.observatory.installed_lab_packages import (
InstalledLabPackage,
InstalledLabPackageError,
InstalledLabPackageRegistry,
)
from k1link.observatory.portable_run_definitions import (
PortableRunDefinition,
PortableRunDefinitionRegistry,
@@ -77,6 +82,15 @@ class ObservatoryWorkerExecutorBuilder(Protocol):
) -> ObservatoryWorkerExecutorRegistration: ...
class ObservatoryWorkerPackageExecutorFactory(Protocol):
"""One generic launcher factory shared by every conforming LAB package."""
def __call__(
self,
context: ObservatoryWorkerPackageExecutorBuildContext,
) -> ObservatoryWorkerExecutorRegistration: ...
@dataclass(frozen=True, slots=True)
class ObservatoryWorkerExecutorBuildContext:
"""Fixed local inputs shared with one install-time executor builder."""
@@ -91,6 +105,17 @@ class ObservatoryWorkerExecutorBuildContext:
_absolute_path(self.work_root, "Worker build work root")
@dataclass(frozen=True, slots=True)
class ObservatoryWorkerPackageExecutorBuildContext(ObservatoryWorkerExecutorBuildContext):
"""Generic package plus the same fixed Worker-owned transport boundary."""
package: InstalledLabPackage
def __post_init__(self) -> None:
ObservatoryWorkerExecutorBuildContext.__post_init__(self)
self.package.bind(self.definition, self.candidate)
@dataclass(frozen=True, slots=True)
class ObservatoryWorkerExecutorBuilderRegistration:
"""Local setup-to-builder binding; queued jobs cannot populate this map."""
@@ -299,6 +324,107 @@ def compose_installed_observatory_worker_service_from_builders(
del bearer_token
def compose_installed_observatory_worker_service_from_packages(
*,
configuration: ObservatoryWorkerServiceConfiguration,
definitions: PortableRunDefinitionRegistry,
runtime_registry: PortableWorkerRuntimeRegistry,
packages: InstalledLabPackageRegistry,
executor_factory: ObservatoryWorkerPackageExecutorFactory,
http_transport: httpx.BaseTransport | None = None,
) -> InstalledObservatoryWorkerService:
"""Compose all ready LABs through one package-aware executor factory."""
bearer_token = load_observatory_worker_bearer_token(configuration.bearer_token_file)
gateway: ObservatoryWorkerHttpGateway | None = None
try:
gateway = ObservatoryWorkerHttpGateway(
base_url=configuration.base_url,
bearer_token=bearer_token,
work_root=configuration.work_root,
transport=http_transport,
)
executors = build_ready_executor_registry_from_packages(
definitions=definitions,
runtime_registry=runtime_registry,
packages=packages,
executor_factory=executor_factory,
source_transport=gateway,
result_transport=gateway,
work_root=configuration.work_root,
)
return InstalledObservatoryWorkerService(
configuration=configuration,
gateway=gateway,
agent=ObservatoryWorkerAgent(transport=gateway, executors=executors),
)
except Exception:
if gateway is not None:
gateway.close()
raise
finally:
del bearer_token
def build_ready_executor_registry_from_packages(
*,
definitions: PortableRunDefinitionRegistry,
runtime_registry: PortableWorkerRuntimeRegistry,
packages: InstalledLabPackageRegistry,
executor_factory: ObservatoryWorkerPackageExecutorFactory,
source_transport: PortableWorkerSourceMaterializer,
result_transport: PortableWorkerResultPublisher,
work_root: Path,
) -> ObservatoryWorkerExecutorRegistry:
"""Build this Worker's installed ready subset with one generic factory."""
_absolute_path(work_root, "Worker package build work root")
ready = definitions.ready_recorded_definitions()
ready_keys = {(item.setup_id, item.definition_sha256) for item in ready}
package_keys = {
(package.setup_id, package.definition_sha256) for package in packages.packages
}
if not package_keys.issubset(ready_keys):
raise ObservatoryWorkerServiceError(
"installed LAB packages must bind only ready RunDefinitions"
)
registrations: list[ObservatoryWorkerExecutorRegistration] = []
for package in sorted(
packages.packages,
key=lambda item: (item.setup_id, item.definition_sha256),
):
definition = definitions.resolve(
package.setup_id,
package.definition_sha256,
)
candidate = runtime_registry.resolve(
definition.setup_id,
definition.definition_sha256,
)
try:
package.bind(definition, candidate)
except InstalledLabPackageError as exc:
raise ObservatoryWorkerServiceError(
"installed LAB package is not bound to its ready runtime"
) from exc
built = executor_factory(
ObservatoryWorkerPackageExecutorBuildContext(
definition=definition,
candidate=candidate,
source_transport=source_transport,
result_transport=result_transport,
work_root=work_root,
package=package,
)
)
if built.identity != package.executor_identity:
raise ObservatoryWorkerServiceError(
"generic package factory returned another executor identity"
)
registrations.append(built)
return ObservatoryWorkerExecutorRegistry(tuple(registrations))
def build_ready_executor_registry(
*,
definitions: PortableRunDefinitionRegistry,
+55
View File
@@ -47,6 +47,9 @@ from k1link.observatory.m49_queue_binding import (
M49QueueBindingError,
M49RecordedQueueBindingService,
)
from k1link.observatory.portable_publication_reconciler import (
PortablePublicationReconciler,
)
from k1link.observatory.portable_queue_binding import (
PortableQueueBindingError,
PortableRecordedQueueBindingService,
@@ -58,6 +61,7 @@ from k1link.observatory.portable_result_contract import (
from k1link.observatory.portable_result_publisher import (
resolve_published_portable_calculation_profile,
)
from k1link.observatory.portable_result_view import PortableResultViewService
from k1link.observatory.portable_run_definitions import (
PortableRunDefinitionRegistry,
PortableRunDefinitionRegistryError,
@@ -478,6 +482,18 @@ except (PortableWorkerIntegrationError, OSError, ValueError) as exc:
# Failure remains isolated from K1, Simulation and legacy LAB.
OBSERVATORY_PORTABLE_WORKER_INTEGRATION = None
OBSERVATORY_PORTABLE_WORKER_INTEGRATION_ERROR = str(exc)
OBSERVATORY_PUBLICATION_RECONCILER = (
None
if (
OBSERVATORY_RECORDED_JOB_QUEUE is None
or OBSERVATORY_PORTABLE_WORKER_INTEGRATION is None
)
else PortablePublicationReconciler(
queue=OBSERVATORY_RECORDED_JOB_QUEUE,
artifact_transport=OBSERVATORY_PORTABLE_WORKER_INTEGRATION.artifact_transport,
result_publisher=OBSERVATORY_PORTABLE_WORKER_INTEGRATION.result_publisher,
)
)
OBSERVATORY_WORKER_API_GATE_ENABLED = OBSERVATORY_WORKER_LOCAL_ENABLED
OBSERVATORY_WORKER_CLAIM_LEASE_READY = (
OBSERVATORY_WORKER_API_GATE_ENABLED
@@ -539,6 +555,7 @@ try:
registry=OBSERVATORY_PORTABLE_DEFINITION_REGISTRY,
capability_probe=OBSERVATORY_PORTABLE_BINDING_SERVICE,
dispatch_available=OBSERVATORY_WORKER_DISPATCH_READY,
equipment_capture_registry=session_store.equipment_capture_registry,
)
OBSERVATORY_PORTABLE_SETUP_PROJECTOR_ERROR = None
except (
@@ -825,9 +842,22 @@ async def _recording_preparation_reconciler() -> None:
await asyncio.sleep(2.0)
async def _portable_result_publication_reconciler() -> None:
service = OBSERVATORY_PUBLICATION_RECONCILER
if service is None:
return
while True:
# Durable state remains pending/failed and is retried on the next
# bounded pass or through the explicit operator action.
with suppress(OSError, ValueError):
await asyncio.to_thread(service.run_once)
await asyncio.sleep(15.0)
@asynccontextmanager
async def app_lifespan(_: FastAPI) -> AsyncIterator[None]:
reconciler: asyncio.Task[None] | None = None
publication_reconciler: asyncio.Task[None] | None = None
try:
configure_scanner_diagnostics(session_store.data_dir / "logs")
session_recording_preparation_manager.start()
@@ -841,6 +871,9 @@ async def app_lifespan(_: FastAPI) -> AsyncIterator[None]:
# expensive on field captures. Start it immediately in the background
# instead of holding the ASGI startup gate.
reconciler = asyncio.create_task(_recording_preparation_reconciler())
publication_reconciler = asyncio.create_task(
_portable_result_publication_reconciler()
)
yield
finally:
await map_gateway_proxy.close()
@@ -848,6 +881,10 @@ async def app_lifespan(_: FastAPI) -> AsyncIterator[None]:
reconciler.cancel()
with suppress(asyncio.CancelledError):
await reconciler
if publication_reconciler is not None:
publication_reconciler.cancel()
with suppress(asyncio.CancelledError):
await publication_reconciler
await asyncio.to_thread(session_recording_preparation_manager.close)
await asyncio.to_thread(lidar_local_surface_read_service.close)
plugin_environment.close()
@@ -1036,6 +1073,24 @@ app.include_router(
portable_setup_projector=OBSERVATORY_PORTABLE_SETUP_PROJECTOR,
portable_setup_projector_error=OBSERVATORY_PORTABLE_SETUP_PROJECTOR_ERROR,
portable_binding_service=OBSERVATORY_PORTABLE_BINDING_SERVICE,
portable_result_view=(
None
if session_artifact_gateway is None
else PortableResultViewService(
sessions=session_store,
artifacts=session_artifact_gateway.store,
)
),
portable_artifact_transport=(
None
if OBSERVATORY_PORTABLE_WORKER_INTEGRATION is None
else OBSERVATORY_PORTABLE_WORKER_INTEGRATION.artifact_transport
),
portable_result_publisher=(
None
if OBSERVATORY_PORTABLE_WORKER_INTEGRATION is None
else OBSERVATORY_PORTABLE_WORKER_INTEGRATION.result_publisher
),
)
)
if OBSERVATORY_WORKER_DISPATCH_READY:
+110 -1
View File
@@ -1,9 +1,10 @@
from __future__ import annotations
from typing import Any, Literal
from typing import Annotated, Any, Literal
from fastapi import APIRouter, HTTPException, Query, Response
from fastapi import Path as ApiPath
from fastapi.responses import JSONResponse
from pydantic import BaseModel, ConfigDict, Field
from k1link.observatory import (
@@ -22,12 +23,24 @@ from k1link.observatory.m49_queue_binding import (
M49QueueBindingIntegrityError,
M49RecordedQueueBindingService,
)
from k1link.observatory.portable_artifact_transport import (
PortableArtifactTransportError,
PortableObservatoryArtifactTransport,
)
from k1link.observatory.portable_queue_binding import (
PortableQueueBindingError,
PortableQueueBindingIntegrityError,
PortableQueueBindingStaleCheckError,
PortableRecordedQueueBindingService,
)
from k1link.observatory.portable_result_contract import PortableResultPublisherError
from k1link.observatory.portable_result_publisher import (
PortableObservatoryResultPublisher,
)
from k1link.observatory.portable_result_view import (
PortableResultViewError,
PortableResultViewService,
)
from k1link.observatory.portable_run_definitions import (
PortableRunDefinitionUnavailableError,
)
@@ -158,10 +171,32 @@ def build_observatory_router(
portable_setup_projector: PortableSetupProjector | PortableLabV1SetupProjector | None = None,
portable_setup_projector_error: str | None = None,
portable_binding_service: PortableRecordedQueueBindingService | None = None,
portable_result_view: PortableResultViewService | None = None,
portable_artifact_transport: PortableObservatoryArtifactTransport | None = None,
portable_result_publisher: PortableObservatoryResultPublisher | None = None,
) -> APIRouter:
"""Build bounded catalog-only mutations for typed Observatory projections."""
router = APIRouter(tags=["observatory"])
if (portable_artifact_transport is None) != (portable_result_publisher is None):
raise ValueError("portable publication dependencies must be configured together")
if portable_result_view is not None:
@router.get("/api/v1/observatory/portable-results/{result_id}")
def get_portable_result_view(
result_id: Annotated[
str,
ApiPath(pattern=r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$"),
],
) -> dict[str, object]:
try:
return portable_result_view.read(result_id)
except PortableResultViewError as exc:
raise HTTPException(
status_code=409,
detail="Portable-результат недоступен для универсального просмотра.",
) from exc
def source_summary(session_id: str) -> SessionSummary:
try:
@@ -1092,6 +1127,80 @@ def build_observatory_router(
detail="Durable-очередь расчётов недоступна.",
) from exc
if (
portable_artifact_transport is not None
and portable_result_publisher is not None
):
@router.post(
"/api/v1/observatory/runs/{job_id}/publication/retry",
response_model=None,
)
def retry_observatory_result_publication(
job_id: str = ApiPath(
min_length=48,
max_length=48,
pattern=r"^observatory-run-[a-f0-9]{32}$",
),
) -> dict[str, object] | JSONResponse:
"""Retry verification/publication only; never repeat compute."""
try:
job = recorded_job_queue.get(job_id)
except ObservatoryRecordedQueueNotFoundError as exc:
raise HTTPException(
status_code=404,
detail="Расчёт Обсерватории не найден.",
) from exc
except (ObservatoryRecordedQueueError, ValueError) as exc:
raise HTTPException(
status_code=503,
detail="Durable-очередь расчётов недоступна.",
) from exc
if job.publication_state == "published":
return job.as_dict()
if job.state != "succeeded" or job.publication_state not in {
"pending",
"failed",
}:
raise HTTPException(
status_code=409,
detail="Результат не ожидает повторной публикации.",
)
try:
package_root = portable_artifact_transport.package_root_for_terminal(
job
)
portable_result_publisher.publish(
job=job,
package_root=package_root,
)
except PortableResultPublisherError as exc:
message = (" ".join(str(exc).split()) or "Publication failed.")[:1_000]
try:
failed = recorded_job_queue.mark_publication_failed(
job_id,
message=message,
)
except (ObservatoryRecordedQueueError, ValueError) as queue_exc:
raise HTTPException(
status_code=503,
detail="Состояние публикации не удалось сохранить.",
) from queue_exc
return JSONResponse(status_code=202, content=failed.as_dict())
except PortableArtifactTransportError as exc:
raise HTTPException(
status_code=409,
detail="Пакет результата недоступен для повторной публикации.",
) from exc
try:
return recorded_job_queue.mark_published(job_id).as_dict()
except (ObservatoryRecordedQueueError, ValueError) as exc:
raise HTTPException(
status_code=503,
detail="Состояние публикации не удалось сохранить.",
) from exc
elif recorded_job_queue_error is not None:
@router.post("/api/v1/observatory/runs")
+104 -21
View File
@@ -20,9 +20,9 @@ from typing import Annotated, Final, Literal
from fastapi import APIRouter, Depends, Header, HTTPException, Request, Response
from fastapi import Path as ApiPath
from fastapi.responses import FileResponse
from fastapi.responses import FileResponse, JSONResponse
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, model_validator
from k1link.observatory.portable_artifact_transport import (
MAX_RESULT_MANIFEST_BYTES,
@@ -38,6 +38,7 @@ from k1link.observatory.portable_result_publisher import (
PortableObservatoryResultPublisher,
)
from k1link.observatory.recorded_jobs import (
MAX_RECORDED_EXECUTOR_CAPABILITIES,
ObservatoryRecordedCheckpointError,
ObservatoryRecordedJobQueue,
ObservatoryRecordedPreemptionError,
@@ -48,9 +49,12 @@ from k1link.observatory.recorded_jobs import (
ObservatoryRecordedQueueIntegrityError,
ObservatoryRecordedQueueNotFoundError,
ObservatoryRecordedQueueStaleClaimError,
RecordedExecutorIdentity,
)
OBSERVATORY_WORKER_CLAIM_REQUEST_SCHEMA: Final = "missioncore.observatory-worker-claim-request/v1"
OBSERVATORY_WORKER_CAPABILITY_CLAIM_REQUEST_SCHEMA: Final = (
"missioncore.observatory-worker-claim-request/v2"
)
OBSERVATORY_WORKER_START_REQUEST_SCHEMA: Final = "missioncore.observatory-worker-start-request/v1"
OBSERVATORY_WORKER_RENEW_REQUEST_SCHEMA: Final = "missioncore.observatory-worker-renew-request/v1"
OBSERVATORY_WORKER_CHECKPOINT_REQUEST_SCHEMA: Final = (
@@ -143,13 +147,36 @@ class _StrictWorkerRequest(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
class ObservatoryWorkerExecutorCapability(_StrictWorkerRequest):
release_sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
image_sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
model_manifest_sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
resource_profile_sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
def recorded_identity(self) -> RecordedExecutorIdentity:
return RecordedExecutorIdentity(**self.model_dump())
class ObservatoryWorkerClaimRequest(_StrictWorkerRequest):
schema_version: Literal["missioncore.observatory-worker-claim-request/v1"]
schema_version: Literal["missioncore.observatory-worker-claim-request/v2"]
claim_request_id: str = Field(
min_length=1,
max_length=160,
pattern=_CLAIM_REQUEST_ID_PATTERN,
)
supported_executor_identities: tuple[ObservatoryWorkerExecutorCapability, ...] = Field(
max_length=MAX_RECORDED_EXECUTOR_CAPABILITIES,
)
@model_validator(mode="after")
def validate_capability_snapshot(self) -> ObservatoryWorkerClaimRequest:
identities = tuple(
capability.recorded_identity()
for capability in self.supported_executor_identities
)
if len(identities) != len(set(identities)):
raise ValueError("executor capabilities must be unique")
return self
class ObservatoryWorkerStartRequest(_StrictWorkerRequest):
@@ -257,6 +284,10 @@ def build_observatory_worker_router(
lambda: queue.claim_next(
claimant_id=authentication.contour_id,
claim_request_id=request.claim_request_id,
supported_executor_identities=tuple(
capability.recorded_identity()
for capability in request.supported_executor_identities
),
)
)
if claim is None:
@@ -303,11 +334,11 @@ def build_observatory_worker_router(
)
).as_dict()
@router.post("/recorded-jobs/{job_id}/succeed")
@router.post("/recorded-jobs/{job_id}/succeed", response_model=None)
def succeed_job(
request: ObservatoryWorkerSucceedRequest,
job_id: Annotated[str, ApiPath(pattern=_JOB_ID_PATTERN)],
) -> dict[str, object]:
) -> dict[str, object] | JSONResponse:
if artifact_transport is not None:
_artifact_call(
lambda: artifact_transport.require_completed_for_success(
@@ -318,29 +349,76 @@ def build_observatory_worker_router(
claimant_id=authentication.contour_id,
)
)
succeeded = _queue_call(
lambda: queue.succeed(
job_id,
claim_token=request.claim_token,
result_id=request.result_id,
result_sha256=request.result_sha256,
)
)
if artifact_transport is not None and result_publisher is not None:
succeeded = _queue_call(
lambda: queue.complete_for_publication(
job_id,
claim_token=request.claim_token,
result_id=request.result_id,
result_sha256=request.result_sha256,
)
)
if succeeded.publication_state == "published":
return succeeded.as_dict()
package_root = _artifact_call(
lambda: artifact_transport.package_root_for_terminal(succeeded)
)
try:
result_publisher.publish(job=succeeded, package_root=package_root)
except PortableResultPublisherError as exc:
publication_error = _publication_error(exc)
failed = _queue_call(
lambda: queue.mark_publication_failed(
job_id,
message=publication_error,
)
)
return JSONResponse(status_code=202, content=failed.as_dict())
return _queue_call(lambda: queue.mark_published(job_id)).as_dict()
return _queue_call(
lambda: queue.succeed(
job_id,
claim_token=request.claim_token,
result_id=request.result_id,
result_sha256=request.result_sha256,
)
).as_dict()
if artifact_transport is not None and result_publisher is not None:
@router.post(
"/recorded-jobs/{job_id}/publication/retry",
response_model=None,
)
def retry_publication(
job_id: Annotated[str, ApiPath(pattern=_JOB_ID_PATTERN)],
) -> dict[str, object] | JSONResponse:
job = _queue_call(lambda: queue.get(job_id))
if job.publication_state == "published":
return job.as_dict()
if job.state != "succeeded" or job.publication_state not in {
"pending",
"failed",
}:
raise HTTPException(
status_code=503,
detail=(
"Recorded result is sealed but its verified publication "
"requires reconciliation."
),
) from exc
return succeeded.as_dict()
status_code=409,
detail="Recorded result is not awaiting publication.",
)
package_root = _artifact_call(
lambda: artifact_transport.package_root_for_terminal(job)
)
try:
result_publisher.publish(job=job, package_root=package_root)
except PortableResultPublisherError as exc:
publication_error = _publication_error(exc)
failed = _queue_call(
lambda: queue.mark_publication_failed(
job_id,
message=publication_error,
)
)
return JSONResponse(status_code=202, content=failed.as_dict())
return _queue_call(lambda: queue.mark_published(job_id)).as_dict()
@router.post("/recorded-jobs/{job_id}/fail")
def fail_job(
@@ -650,6 +728,11 @@ def _raise(exc: Exception) -> None:
raise exc
def _publication_error(exc: PortableResultPublisherError) -> str:
message = " ".join(str(exc).split())
return (message or "Portable result publication failed.")[:1_000]
async def _read_bounded_body(request: Request, maximum_bytes: int) -> bytes:
content_length = request.headers.get("content-length")
if content_length is not None:
@@ -789,12 +789,23 @@ def test_result_v2_assembler_and_exact_validator_round_trip(
)
plan = PortableWorkerRuntimePlan(
job_id=sealed.job_id,
request_sha256=sealed.request_sha256,
identity_sha256=sealed.identity_sha256,
submission_receipt_sha256=sealed.submission_receipt_sha256,
claim_generation=sealed.claim_generation,
adapter_id="m49-tgs-worker006-portable-v2",
candidate_sha256="f" * 64,
setup_id=sealed.setup_id,
definition_id=sealed.definition_id,
definition_version=sealed.definition_version,
definition_sha256=sealed.definition_sha256,
source_session_id=sealed.source_session_id,
source_catalog_sha256=sealed.source_catalog_sha256,
source_bundle_sha256=sealed.source_bundle_sha256,
source_capability_manifest_sha256=sealed.source_capability_manifest_sha256,
source_adapter_id=sealed.source_adapter_id,
source_adapter_version=sealed.source_adapter_version,
source_adapter_sha256=sealed.source_adapter_sha256,
result_contract_sha256=definition.result_contract.contract_sha256,
phases=M49_PORTABLE_RUNTIME_PHASES,
)
@@ -0,0 +1,362 @@
from __future__ import annotations
import hashlib
import json
from dataclasses import replace
from pathlib import Path
from typing import cast
import httpx
import pytest
from k1link.observatory.installed_lab_package_runner import (
DockerEngineInstalledLabLauncher,
InstalledLabDockerLaunch,
InstalledLabDockerMount,
InstalledLabLocalAssetBinding,
InstalledLabPackageProfileRunner,
InstalledLabPackageRunnerError,
)
from k1link.observatory.installed_lab_packages import (
INSTALLED_LAB_PACKAGE_REGISTRY_SCHEMA,
InstalledLabContainer,
InstalledLabPackageMount,
seal_installed_lab_package,
)
from k1link.observatory.portable_result_contract import (
PORTABLE_RESULT_PACKAGE_IDENTITY_SCHEMA,
PortableResultArtifact,
PortableResultPackageManifest,
canonical_json,
result_identity_document,
run_definition_document,
)
from k1link.observatory.portable_run_definitions import (
PortableRunDefinitionRegistry,
canonical_sha256,
)
from k1link.observatory.portable_worker_runtime import (
PortableWorkerRuntimePlan,
PortableWorkerRuntimeRegistry,
PortableWorkerSourceStage,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
DEFINITIONS_FILE = REPOSITORY_ROOT / "config" / "observatory-portable-run-definitions.json"
RUNTIME_FILE = REPOSITORY_ROOT / "config" / "observatory-worker-runtime-candidates.json"
def _package_and_runtime():
definitions = PortableRunDefinitionRegistry.from_file(DEFINITIONS_FILE)
runtime = PortableWorkerRuntimeRegistry.from_file(
RUNTIME_FILE,
definitions=definitions,
)
definition = definitions.resolve_setup("m49-tgs-portable-v2")
candidate = runtime.resolve(definition.setup_id, definition.definition_sha256)
assert candidate.executor is not None
mounted_asset = next(
asset for asset in candidate.reusable_assets if asset.kind != "container-image"
)
writer = InstalledLabContainer(
container_id="portable-result-writer",
role="result-writer",
image_sha256=candidate.executor.image_sha256,
argv=("/missioncore/package/run", INSTALLED_LAB_PACKAGE_REGISTRY_SCHEMA),
depends_on=(),
mounts=(
InstalledLabPackageMount(
asset_id=mounted_asset.asset_id,
target=f"/missioncore/package/assets/{mounted_asset.asset_id}",
),
),
network="none",
gpu_count=0,
memory_bytes=8 * 1024**3,
nano_cpus=2_000_000_000,
pids_limit=512,
shm_bytes=64 * 1024**2,
tmpfs_bytes=512 * 1024**2,
timeout_seconds=3600,
)
package = seal_installed_lab_package(
package_id="m49-generic-runner-test",
package_version=1,
setup_id=definition.setup_id,
definition_id=definition.definition_id,
definition_version=definition.version,
definition_sha256=definition.definition_sha256,
runtime_candidate_sha256=candidate.candidate_sha256,
source_adapter_sha256=definition.source_adapter.contract_sha256,
result_contract_sha256=definition.result_contract.contract_sha256,
executor_identity=candidate.executor_identity(),
execution_mode="single-container",
asset_ids=tuple(asset.asset_id for asset in candidate.reusable_assets),
containers=(writer,),
)
return definition, candidate, package, mounted_asset.asset_id
def _fixed_stack(package):
writer_template = package.containers[0]
step = replace(
writer_template,
container_id="compute-step",
role="step",
)
writer = replace(writer_template, depends_on=(step.container_id,))
return seal_installed_lab_package(
package_id=package.package_id,
package_version=package.package_version,
setup_id=package.setup_id,
definition_id=package.definition_id,
definition_version=package.definition_version,
definition_sha256=package.definition_sha256,
runtime_candidate_sha256=package.runtime_candidate_sha256,
source_adapter_sha256=package.source_adapter_sha256,
result_contract_sha256=package.result_contract_sha256,
executor_identity=package.executor_identity,
execution_mode="fixed-stack",
asset_ids=package.asset_ids,
containers=(step, writer),
)
def _plan(definition, candidate) -> PortableWorkerRuntimePlan:
return PortableWorkerRuntimePlan(
job_id=f"observatory-run-{'1' * 32}",
request_sha256="4" * 64,
identity_sha256="5" * 64,
submission_receipt_sha256="6" * 64,
claim_generation=1,
adapter_id=candidate.adapter_id,
candidate_sha256=candidate.candidate_sha256,
setup_id=definition.setup_id,
definition_id=definition.definition_id,
definition_version=definition.version,
definition_sha256=definition.definition_sha256,
source_session_id="source-a",
source_catalog_sha256="7" * 64,
source_bundle_sha256="2" * 64,
source_capability_manifest_sha256="3" * 64,
source_adapter_id=definition.source_adapter.adapter_id,
source_adapter_version=definition.source_adapter.version,
source_adapter_sha256=definition.source_adapter.contract_sha256,
result_contract_sha256=definition.result_contract.contract_sha256,
phases=tuple(phase.phase_id for phase in candidate.phases),
)
def _write_result(root: Path, *, plan: PortableWorkerRuntimePlan, definition) -> None:
result_id = "portable-result-generic-runner"
artifact_payload = canonical_json(
{"schema_version": definition.result_contract.result_schema, "verified": True}
)
artifact_path = root / "artifacts" / "result.json"
artifact_path.parent.mkdir(parents=True)
artifact_path.write_bytes(artifact_payload)
artifact = PortableResultArtifact(
role="result-document",
relative_path="artifacts/result.json",
media_type="application/json",
byte_length=len(artifact_payload),
sha256=hashlib.sha256(artifact_payload).hexdigest(),
)
job = {
"job_id": plan.job_id,
"request_sha256": plan.request_sha256,
"identity_sha256": plan.identity_sha256,
"submission_receipt_sha256": plan.submission_receipt_sha256,
"claim_generation": plan.claim_generation,
}
source = {
"session_id": plan.source_session_id,
"catalog_sha256": plan.source_catalog_sha256,
"bundle_sha256": plan.source_bundle_sha256,
"capability_manifest_sha256": plan.source_capability_manifest_sha256,
"adapter": {
"adapter_id": definition.source_adapter.adapter_id,
"version": definition.source_adapter.version,
"adapter_sha256": definition.source_adapter.contract_sha256,
},
}
run_definition = run_definition_document(definition)
result = result_identity_document(definition, result_id)
authority = definition.authority.as_dict()
created_at_utc = "2026-09-01T12:00:00Z"
identity_document = {
"schema_version": PORTABLE_RESULT_PACKAGE_IDENTITY_SCHEMA,
"created_at_utc": created_at_utc,
"job": job,
"source": source,
"run_definition": run_definition,
"result": result,
"authority": authority,
"artifacts": [artifact.as_dict()],
}
manifest = PortableResultPackageManifest(
identity_sha256=canonical_sha256(identity_document),
created_at_utc=created_at_utc,
job=job,
source=source,
run_definition=run_definition,
result=result,
authority=authority,
artifacts=(artifact,),
)
(root / "manifest.json").write_bytes(manifest.canonical_bytes)
def test_generic_fixed_stack_runs_topologically_and_returns_verified_package(
tmp_path: Path,
) -> None:
definition, candidate, single, mounted_asset_id = _package_and_runtime()
package = _fixed_stack(single)
source_root = tmp_path / "materialized-source"
source_root.mkdir()
asset_path = tmp_path / "installed-asset"
asset_path.write_bytes(b"asset")
plan = _plan(definition, candidate)
launches: list[InstalledLabDockerLaunch] = []
def launcher(launch: InstalledLabDockerLaunch) -> None:
launches.append(launch)
if launch.container.role == "result-writer":
output = next(
mount for mount in launch.mounts if mount.container_path == "/missioncore/output"
)
_write_result(Path(output.engine_path), plan=plan, definition=definition)
runner = InstalledLabPackageProfileRunner(
package=package,
definition=definition,
controller_work_root=tmp_path,
engine_work_root=str(tmp_path),
local_assets=(
InstalledLabLocalAssetBinding(
asset_id=mounted_asset_id,
controller_path=asset_path,
engine_path=str(asset_path),
),
),
launcher=launcher,
token_factory=lambda: "abcdef0123456789",
)
draft = runner.run(
plan,
PortableWorkerSourceStage(
root=source_root,
source_bundle_sha256=plan.source_bundle_sha256,
source_capability_manifest_sha256=plan.source_capability_manifest_sha256,
source_adapter_sha256=definition.source_adapter.contract_sha256,
),
)
assert [launch.container.container_id for launch in launches] == [
"compute-step",
"portable-result-writer",
]
writer_launch = launches[-1]
assert any(
mount.container_path == "/missioncore/input/steps/compute-step"
and mount.read_only
for mount in writer_launch.mounts
)
assert draft.result_id == "portable-result-generic-runner"
assert draft.result_contract_sha256 == definition.result_contract.contract_sha256
assert (draft.root / "manifest.json").is_file()
for launch in launches:
writable = [mount for mount in launch.mounts if not mount.read_only]
assert [mount.container_path for mount in writable] == ["/missioncore/output"]
def test_generic_docker_launcher_uses_hardened_one_shot_contract() -> None:
_definition, _candidate, package, _mounted_asset_id = _package_and_runtime()
requests: list[httpx.Request] = []
create_document: dict[str, object] = {}
container_id = "f" * 64
def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
if request.url.path.startswith("/v1.47/images/"):
return httpx.Response(
200,
json={"Id": f"sha256:{package.containers[0].image_sha256}"},
)
if request.url.path == "/v1.47/containers/create":
create_document.update(json.loads(request.content))
return httpx.Response(201, json={"Id": container_id, "Warnings": None})
if request.url.path.endswith("/start"):
return httpx.Response(204)
if request.url.path.endswith("/wait"):
return httpx.Response(200, json={"StatusCode": 0, "Error": None})
if request.method == "DELETE":
return httpx.Response(204)
raise AssertionError(f"unexpected request: {request.method} {request.url}")
launch = InstalledLabDockerLaunch(
package_id=package.package_id,
container=package.containers[0],
mounts=(
InstalledLabDockerMount("/engine/plan.json", "/missioncore/input/run-plan.json", True),
InstalledLabDockerMount("/engine/source", "/missioncore/input/source", True),
InstalledLabDockerMount("/engine/output", "/missioncore/output", False),
InstalledLabDockerMount("/engine/asset", "/missioncore/package/assets/runner", True),
),
labels={
"com.nodedc.authority": "observation-only",
"com.nodedc.component": package.containers[0].container_id,
"com.nodedc.definition-sha256": package.definition_sha256,
"com.nodedc.job-id": f"observatory-run-{'1' * 32}",
"com.nodedc.managed-by": "mission-core-worker",
"com.nodedc.package-sha256": package.package_sha256,
"com.nodedc.product": "mission-core",
"com.nodedc.stack": "observatory",
},
name_token="0123456789abcdef",
)
DockerEngineInstalledLabLauncher(transport_factory=lambda: httpx.MockTransport(handler))(launch)
assert [request.method for request in requests] == [
"GET",
"POST",
"POST",
"POST",
"DELETE",
]
assert create_document["Image"] == f"sha256:{package.containers[0].image_sha256}"
assert create_document["NetworkDisabled"] is True
host = cast(dict[str, object], create_document["HostConfig"])
assert host["NetworkMode"] == "none"
assert host["ReadonlyRootfs"] is True
assert host["CapDrop"] == ["ALL"]
assert host["SecurityOpt"] == ["no-new-privileges:true"]
assert host["Privileged"] is False
assert host["DeviceRequests"] == []
mounts = cast(list[dict[str, object]], host["Mounts"])
assert sum(not cast(bool, mount["ReadOnly"]) for mount in mounts) == 1
assert next(mount for mount in mounts if not mount["ReadOnly"])["Target"] == (
"/missioncore/output"
)
def test_generic_docker_launcher_verifies_image_inventory_without_container_calls() -> None:
image_sha256s = ("1" * 64, "2" * 64)
requests: list[httpx.Request] = []
def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
digest = request.url.path.split("sha256:", 1)[1].split("/", 1)[0]
return httpx.Response(200, json={"Id": f"sha256:{digest}"})
DockerEngineInstalledLabLauncher(
transport_factory=lambda: httpx.MockTransport(handler)
).verify_images(image_sha256s)
assert [request.method for request in requests] == ["GET", "GET"]
assert all("/images/sha256:" in request.url.path for request in requests)
with pytest.raises(InstalledLabPackageRunnerError, match="inventory"):
DockerEngineInstalledLabLauncher(
transport_factory=lambda: httpx.MockTransport(handler)
).verify_images(tuple(reversed(image_sha256s)))
@@ -0,0 +1,216 @@
from __future__ import annotations
import json
from dataclasses import replace
from pathlib import Path
from typing import cast
import pytest
from k1link.observatory.installed_lab_packages import (
INSTALLED_LAB_PACKAGE_REGISTRY_SCHEMA,
InstalledLabContainer,
InstalledLabPackageError,
InstalledLabPackageMount,
InstalledLabPackageRegistry,
seal_installed_lab_package,
)
from k1link.observatory.portable_run_definitions import PortableRunDefinitionRegistry
from k1link.observatory.portable_worker_runtime import (
PortableWorkerResultPublisher,
PortableWorkerRuntimeRegistry,
PortableWorkerSourceMaterializer,
)
from k1link.observatory.worker_agent import (
ObservatoryWorkerExecutionResult,
ObservatoryWorkerExecutorRegistration,
SealedObservatoryRecordedJob,
)
from k1link.observatory.worker_service import (
ObservatoryWorkerPackageExecutorBuildContext,
build_ready_executor_registry_from_packages,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
DEFINITIONS_FILE = REPOSITORY_ROOT / "config" / "observatory-portable-run-definitions.json"
RUNTIME_FILE = REPOSITORY_ROOT / "config" / "observatory-worker-runtime-candidates.json"
def _registries() -> tuple[PortableRunDefinitionRegistry, PortableWorkerRuntimeRegistry]:
definitions = PortableRunDefinitionRegistry.from_file(DEFINITIONS_FILE)
runtime = PortableWorkerRuntimeRegistry.from_file(
RUNTIME_FILE,
definitions=definitions,
)
return definitions, runtime
def _m49_package():
definitions, runtime = _registries()
definition = definitions.resolve_setup("m49-tgs-portable-v2")
candidate = runtime.resolve(definition.setup_id, definition.definition_sha256)
assert candidate.executor is not None
non_image_asset = next(
asset for asset in candidate.reusable_assets if asset.kind != "container-image"
)
container = InstalledLabContainer(
container_id="portable-result-writer",
role="result-writer",
image_sha256=candidate.executor.image_sha256,
argv=("/missioncore/package/run", INSTALLED_LAB_PACKAGE_REGISTRY_SCHEMA),
depends_on=(),
mounts=(
InstalledLabPackageMount(
asset_id=non_image_asset.asset_id,
target=f"/missioncore/package/assets/{non_image_asset.asset_id}",
),
),
network="none",
gpu_count=0,
memory_bytes=8 * 1024**3,
nano_cpus=2_000_000_000,
pids_limit=512,
shm_bytes=64 * 1024**2,
tmpfs_bytes=512 * 1024**2,
timeout_seconds=3600,
)
return seal_installed_lab_package(
package_id="m49-container-contract-test",
package_version=1,
setup_id=definition.setup_id,
definition_id=definition.definition_id,
definition_version=definition.version,
definition_sha256=definition.definition_sha256,
runtime_candidate_sha256=candidate.candidate_sha256,
source_adapter_sha256=definition.source_adapter.contract_sha256,
result_contract_sha256=definition.result_contract.contract_sha256,
executor_identity=candidate.executor_identity(),
execution_mode="single-container",
asset_ids=tuple(asset.asset_id for asset in candidate.reusable_assets),
containers=(container,),
)
def test_installed_package_binds_exact_ready_definition_and_runtime() -> None:
definitions, runtime = _registries()
package = _m49_package()
definition = definitions.resolve(package.setup_id, package.definition_sha256)
candidate = runtime.resolve(package.setup_id, package.definition_sha256)
package.bind(definition, candidate)
document = package.identity_document()
serialized = json.dumps(document, sort_keys=True)
assert document["container_io"] == {
"schema_version": "missioncore.observatory-installed-lab-container-io/v2",
"source_root": "/missioncore/input/source",
"plan_path": "/missioncore/input/run-plan.json",
"step_input_root": "/missioncore/input/steps",
"result_root": "/missioncore/output",
"work_root": "/missioncore/work",
}
assert "host_path" not in serialized
assert "source_session_id" not in serialized
def test_installed_package_registry_round_trips_and_rejects_host_inputs(
tmp_path: Path,
) -> None:
definitions, runtime = _registries()
package = _m49_package()
document = {
"schema_version": INSTALLED_LAB_PACKAGE_REGISTRY_SCHEMA,
"packages": [{**package.identity_document(), "package_sha256": package.package_sha256}],
}
path = tmp_path / "installed-packages.json"
path.write_text(json.dumps(document), encoding="utf-8")
loaded = InstalledLabPackageRegistry.from_file(
path,
definitions=definitions,
runtime_registry=runtime,
)
assert loaded.resolve(package.setup_id, package.definition_sha256) == package
document["packages"][0]["containers"][0]["host_path"] = "/unsafe"
path.write_text(json.dumps(document), encoding="utf-8")
with pytest.raises(InstalledLabPackageError, match="forbidden"):
InstalledLabPackageRegistry.from_file(
path,
definitions=definitions,
runtime_registry=runtime,
)
def test_package_topology_and_digest_fail_closed() -> None:
package = _m49_package()
with pytest.raises(InstalledLabPackageError, match="digest"):
replace(package, package_sha256="f" * 64)
first = replace(
package.containers[0],
container_id="first-service",
role="step",
depends_on=("portable-result-writer",),
)
writer = replace(
package.containers[0],
depends_on=("first-service",),
)
with pytest.raises(InstalledLabPackageError, match="cycle"):
seal_installed_lab_package(
package_id=package.package_id,
package_version=package.package_version,
setup_id=package.setup_id,
definition_id=package.definition_id,
definition_version=package.definition_version,
definition_sha256=package.definition_sha256,
runtime_candidate_sha256=package.runtime_candidate_sha256,
source_adapter_sha256=package.source_adapter_sha256,
result_contract_sha256=package.result_contract_sha256,
executor_identity=package.executor_identity,
execution_mode="fixed-stack",
asset_ids=package.asset_ids,
containers=(first, writer),
)
with pytest.raises(InstalledLabPackageError, match="unsafe"):
InstalledLabPackageMount(
asset_id=package.asset_ids[0],
target="../../host",
)
def test_one_generic_factory_builds_installed_ready_subset(tmp_path: Path) -> None:
definitions, runtime = _registries()
package = _m49_package()
contexts: list[ObservatoryWorkerPackageExecutorBuildContext] = []
class Executor:
def execute(
self,
job: SealedObservatoryRecordedJob,
) -> ObservatoryWorkerExecutionResult:
raise AssertionError(job)
def factory(
context: ObservatoryWorkerPackageExecutorBuildContext,
) -> ObservatoryWorkerExecutorRegistration:
contexts.append(context)
return ObservatoryWorkerExecutorRegistration(
context.package.executor_identity,
Executor(),
)
executors = build_ready_executor_registry_from_packages(
definitions=definitions,
runtime_registry=runtime,
packages=InstalledLabPackageRegistry((package,)),
executor_factory=factory,
source_transport=cast(PortableWorkerSourceMaterializer, object()),
result_transport=cast(PortableWorkerResultPublisher, object()),
work_root=tmp_path,
)
assert executors.supported_identities == (package.executor_identity,)
assert [context.package.package_id for context in contexts] == [package.package_id]
@@ -0,0 +1,63 @@
from __future__ import annotations
import socket
import socketserver
import threading
import pytest
import k1link.observatory.installed_lab_worker_container_main as container_main
from k1link.observatory.worker_container_proxy import (
FixedObservatoryContainerLoopbackProxy,
)
class _EchoHandler(socketserver.BaseRequestHandler):
def handle(self) -> None:
payload = self.request.recv(1024)
self.request.sendall(payload)
def test_generic_proxy_bridges_loopback_to_fixed_upstream() -> None:
upstream = socketserver.ThreadingTCPServer(("127.0.0.1", 0), _EchoHandler)
upstream_thread = threading.Thread(target=upstream.serve_forever, daemon=True)
upstream_thread.start()
try:
upstream_port = upstream.server_address[1]
assert isinstance(upstream_port, int)
with FixedObservatoryContainerLoopbackProxy(
listen_port=0,
upstream_host="127.0.0.1",
upstream_port=upstream_port,
) as proxy, socket.create_connection(("127.0.0.1", proxy.listen_port)) as client:
client.sendall(b"generic-observatory-proxy")
client.shutdown(socket.SHUT_WR)
assert client.recv(1024) == b"generic-observatory-proxy"
finally:
upstream.shutdown()
upstream.server_close()
upstream_thread.join(timeout=5.0)
def test_generic_entrypoint_owns_proxy_around_package_worker(
monkeypatch: pytest.MonkeyPatch,
) -> None:
lifecycle: list[str] = []
class _Proxy:
def __enter__(self) -> _Proxy:
lifecycle.append("proxy-started")
return self
def __exit__(self, *_args: object) -> None:
lifecycle.append("proxy-stopped")
def worker(arguments: object) -> int:
lifecycle.append(f"worker:{arguments!r}")
return 23
monkeypatch.setattr(container_main, "FixedObservatoryContainerLoopbackProxy", _Proxy)
monkeypatch.setattr(container_main.installed_lab_worker_service, "main", worker)
assert container_main.main(("--once",)) == 23
assert lifecycle == ["proxy-started", "worker:('--once',)", "proxy-stopped"]
@@ -0,0 +1,131 @@
from __future__ import annotations
import json
from pathlib import Path
import pytest
from k1link.observatory.installed_lab_worker_service import (
INSTALLED_LAB_ASSET_BINDINGS_SCHEMA,
InstalledLabWorkerAssetBindings,
InstalledLabWorkerCompositionError,
InstalledLabWorkerEntrypointConfiguration,
InstalledLabWorkerValidationConfiguration,
)
def _binding_document() -> dict[str, object]:
return {
"schema_version": INSTALLED_LAB_ASSET_BINDINGS_SCHEMA,
"engine_work_root": "D:\\NDC_MISSIONCORE\\runtime\\worker\\work",
"assets": [
{
"asset_id": "eomt-image",
"controller_path": None,
"engine_path": None,
"image_sha256": "1" * 64,
},
{
"asset_id": "ddrnet-checkpoint",
"controller_path": "/runtime/assets/ddrnet-checkpoint.pth",
"engine_path": (
"D:\\NDC_MISSIONCORE\\runtime\\assets\\ddrnet-checkpoint.pth"
),
"image_sha256": None,
},
],
}
def test_asset_binding_file_loads_only_reviewed_paths_or_image_identities(
tmp_path: Path,
) -> None:
path = tmp_path / "bindings.json"
path.write_text(json.dumps(_binding_document()), encoding="utf-8")
bindings = InstalledLabWorkerAssetBindings.from_file(path)
assert bindings.engine_work_root.endswith("\\work")
assert tuple(item.asset_id for item in bindings.assets) == (
"ddrnet-checkpoint",
"eomt-image",
)
assert bindings.assets[0].controller_path == Path(
"/runtime/assets/ddrnet-checkpoint.pth"
)
assert bindings.assets[1].image_sha256 == "1" * 64
def test_asset_binding_file_rejects_ambiguous_locator(tmp_path: Path) -> None:
document = _binding_document()
assets = document["assets"]
assert isinstance(assets, list)
first = assets[0]
assert isinstance(first, dict)
first["controller_path"] = "/runtime/image"
first["engine_path"] = "D:\\runtime\\image"
path = tmp_path / "bindings.json"
path.write_text(json.dumps(document), encoding="utf-8")
with pytest.raises(InstalledLabWorkerCompositionError, match="ambiguous"):
InstalledLabWorkerAssetBindings.from_file(path)
def test_entrypoint_environment_contains_paths_but_no_executable_selector(
tmp_path: Path,
) -> None:
environment = {
"MISSIONCORE_OBSERVATORY_WORKER_TOKEN_FILE": str(tmp_path / "worker.token"),
"MISSIONCORE_OBSERVATORY_WORKER_WORK_ROOT": str(tmp_path / "work"),
"MISSIONCORE_OBSERVATORY_WORKER_DEFINITIONS_FILE": str(tmp_path / "definitions.json"),
"MISSIONCORE_OBSERVATORY_WORKER_RUNTIME_REGISTRY_FILE": str(
tmp_path / "runtime.json"
),
"MISSIONCORE_OBSERVATORY_WORKER_PACKAGE_REGISTRY_FILE": str(
tmp_path / "packages.json"
),
"MISSIONCORE_OBSERVATORY_WORKER_PACKAGE_ASSET_BINDINGS_FILE": str(
tmp_path / "bindings.json"
),
}
configuration = InstalledLabWorkerEntrypointConfiguration.from_environment(environment)
assert configuration.package_registry_file == tmp_path / "packages.json"
assert configuration.asset_bindings_file == tmp_path / "bindings.json"
assert not any("COMMAND" in key or "MODULE" in key for key in environment)
def test_validation_environment_requires_no_token_or_backend_url(tmp_path: Path) -> None:
environment = {
"MISSIONCORE_OBSERVATORY_WORKER_WORK_ROOT": str(tmp_path / "work"),
"MISSIONCORE_OBSERVATORY_WORKER_DEFINITIONS_FILE": str(tmp_path / "definitions.json"),
"MISSIONCORE_OBSERVATORY_WORKER_RUNTIME_REGISTRY_FILE": str(
tmp_path / "runtime.json"
),
"MISSIONCORE_OBSERVATORY_WORKER_PACKAGE_REGISTRY_FILE": str(
tmp_path / "packages.json"
),
"MISSIONCORE_OBSERVATORY_WORKER_PACKAGE_ASSET_BINDINGS_FILE": str(
tmp_path / "bindings.json"
),
}
configuration = InstalledLabWorkerValidationConfiguration.from_environment(environment)
assert configuration.work_root == tmp_path / "work"
assert not any("TOKEN" in key or "BASE_URL" in key for key in environment)
def test_asset_binding_file_rejects_unresolved_engine_path(tmp_path: Path) -> None:
document = _binding_document()
assets = document["assets"]
assert isinstance(assets, list)
file_asset = assets[1]
assert isinstance(file_asset, dict)
file_asset["engine_path"] = "D:\\runtime\\lab$release\\asset"
path = tmp_path / "bindings.json"
path.write_text(json.dumps(document), encoding="utf-8")
with pytest.raises(InstalledLabWorkerCompositionError, match="locator is invalid"):
InstalledLabWorkerAssetBindings.from_file(path)
@@ -544,6 +544,19 @@ def test_entrypoint_environment_requires_all_absolute_fixed_files(tmp_path: Path
assert configuration.lab_v1_release_candidate_file == (
service_module._FIXED_LAB_V1_RELEASE_CANDIDATE_FILE # noqa: SLF001
)
m49_only = service_module.M49WorkerEntrypointConfiguration.from_environment(
{
key: value
for key, value in environment.items()
if key
not in {
service_module.LAB_V1_WORKER_INSTALLATION_RECEIPT_FILE_ENV,
service_module.LAB_V1_WORKER_RELEASE_CANDIDATE_FILE_ENV,
}
}
)
assert m49_only.lab_v1_installation_receipt_file is None
assert m49_only.lab_v1_release_candidate_file is None
environment[service_module.LAB_V1_WORKER_RELEASE_CANDIDATE_FILE_ENV] = str(
tmp_path / "lab-v1-release.json"
)
@@ -126,7 +126,7 @@ def _source_documents(definition: PortableRunDefinition) -> tuple[bytes, bytes]:
"spatial_replay": {},
"camera": {
"artifact_id": "camera-recording",
"public_source_id": "sensor.camera.right",
"public_source_id": "recorded.camera.right",
"generation_sha256": CAMERA_GENERATION_SHA256,
"synchronization": "host-arrival-best-effort",
"epoch": {
@@ -198,22 +198,22 @@ def _camera_job(tmp_path: Path) -> CameraComputeJob:
root.mkdir()
files = [
{
"path": "input/camera/sensor.camera.right/epoch-1/summary.json",
"path": "input/camera/recorded.camera.right/epoch-1/summary.json",
"byte_length": 2,
"sha256": "1" * 64,
},
{
"path": "input/camera/sensor.camera.right/epoch-1/index.jsonl",
"path": "input/camera/recorded.camera.right/epoch-1/index.jsonl",
"byte_length": 3,
"sha256": "2" * 64,
},
{
"path": "input/camera/sensor.camera.right/epoch-1/init.mp4",
"path": "input/camera/recorded.camera.right/epoch-1/init.mp4",
"byte_length": 4,
"sha256": CAMERA_INIT_SHA256,
},
{
"path": "input/camera/sensor.camera.right/epoch-1/segments/1.m4s",
"path": "input/camera/recorded.camera.right/epoch-1/segments/1.m4s",
"byte_length": 7,
"sha256": CAMERA_SEGMENT_SHA256,
},
@@ -221,7 +221,7 @@ def _camera_job(tmp_path: Path) -> CameraComputeJob:
input_document = {
"kind": "canonical-camera-epoch",
"session_id": SOURCE_SESSION_ID,
"source_id": "sensor.camera.right",
"source_id": "recorded.camera.right",
"codec_epoch": 1,
"synchronization": "host-arrival-best-effort",
"media_type": "video/mp4; codecs=\"avc1.641028\"",
@@ -254,7 +254,7 @@ def _camera_job(tmp_path: Path) -> CameraComputeJob:
job_root=root,
manifest_path=root / "job.json",
session_id=SOURCE_SESSION_ID,
source_id="sensor.camera.right",
source_id="recorded.camera.right",
codec_epoch=1,
input_sha256=input_sha256,
input_byte_length=16,
@@ -477,7 +477,7 @@ def _component_outputs(
"job_id": plan.source_input.camera_job_id,
"input_sha256": plan.source_input.camera_input_sha256,
"session_id": SOURCE_SESSION_ID,
"source_id": "sensor.camera.right",
"source_id": "recorded.camera.right",
"codec_epoch": 1,
"timestamp_basis": "session-time-seconds",
"timeline_start_seconds": 1.0,
@@ -857,7 +857,7 @@ def _release_for_definition(
)
def test_release_candidate_matches_repository_but_stays_honestly_blocked(
def test_legacy_release_candidate_reports_repository_drift_and_stays_blocked(
tmp_path: Path,
) -> None:
release = PortableLabV1ReleaseCandidate.from_file(
@@ -870,7 +870,16 @@ def test_release_candidate_matches_repository_but_stays_honestly_blocked(
repository_assets = {
asset.asset_id for asset in release.assets if asset.repository_path is not None
}
assert set(inspection.matched_assets) == repository_assets
mismatched_assets = {
blocker.removeprefix("asset-").removesuffix("-mismatched")
for blocker in inspection.blockers
if blocker.startswith("asset-") and blocker.endswith("-mismatched")
}
assert set(inspection.matched_assets) | mismatched_assets == repository_assets
assert {
"lab-v1-portable-contracts",
"portable-worker-runtime",
} <= mismatched_assets
assert inspection.ready is False
assert "executor-image-unsealed" in inspection.blockers
assert "commit-bound-source-unavailable" in inspection.blockers
@@ -1132,7 +1141,7 @@ def test_shared_worker_stage_materializes_a_version_bound_camera_job(
materialized.camera_job_root
/ "input"
/ "camera"
/ "sensor.camera.right"
/ "recorded.camera.right"
/ "epoch-1"
/ "init.mp4"
).read_bytes() == init_payload
@@ -1140,7 +1149,7 @@ def test_shared_worker_stage_materializes_a_version_bound_camera_job(
materialized.camera_job_root
/ "input"
/ "camera"
/ "sensor.camera.right"
/ "recorded.camera.right"
/ "epoch-1"
/ "segments"
/ "1.m4s"
@@ -1273,14 +1282,25 @@ def test_profile_runner_sequences_exact_components_and_packages_sealed_job(
)
runtime_plan = PortableWorkerRuntimePlan(
job_id=sealed.job_id,
request_sha256=sealed.request_sha256,
identity_sha256=sealed.identity_sha256,
submission_receipt_sha256=sealed.submission_receipt_sha256,
claim_generation=sealed.claim_generation,
adapter_id="lab-v1-eomt-ddrnet-worker006-v1",
candidate_sha256="f" * 64,
setup_id=sealed.setup_id,
definition_id=sealed.definition_id,
definition_version=sealed.definition_version,
definition_sha256=sealed.definition_sha256,
source_session_id=sealed.source_session_id,
source_catalog_sha256=sealed.source_catalog_sha256,
source_bundle_sha256=sealed.source_bundle_sha256,
source_capability_manifest_sha256=(
sealed.source_capability_manifest_sha256
),
source_adapter_id=sealed.source_adapter_id,
source_adapter_version=sealed.source_adapter_version,
source_adapter_sha256=sealed.source_adapter_sha256,
result_contract_sha256=definition.result_contract.contract_sha256,
phases=PORTABLE_LAB_V1_RUNTIME_PHASES,
)
@@ -115,9 +115,8 @@ class _Launcher:
def _ready_definition() -> PortableRunDefinition:
base = PortableRunDefinitionRegistry.from_file(DEFINITIONS_PATH).resolve(
base = PortableRunDefinitionRegistry.from_file(DEFINITIONS_PATH).resolve_setup(
service_module.PORTABLE_LAB_V1_SETUP_ID,
"3692d41cec3949f348a36eb60a501fb2cd483fed1645679b0ec58061a2fc6dc2",
)
executor = PortableExecutorAvailability(
contour_id=base.executor.contour_id,
@@ -52,7 +52,30 @@ INIT_SHA256 = "e2279963e16d84c91d68e7dbb1f7efed840533387dfeb844b7398bff45fbde38"
def _blocked_registry() -> PortableRunDefinitionRegistry:
return PortableRunDefinitionRegistry.from_file(REGISTRY_PATH)
production = PortableRunDefinitionRegistry.from_file(REGISTRY_PATH)
ready = production.resolve_setup("lab-v1-eomt-ddrnet-portable-v1")
executor = PortableExecutorAvailability(
contour_id=ready.executor.contour_id,
state="not-installed",
release_id=None,
release_sha256=None,
image_sha256=None,
reason_code="executor-not-installed",
reason="Executor release is not sealed or installed on Worker 006.",
)
identity = ready.identity_document()
identity["executor"] = executor.identity_document()
blocked = replace(
ready,
executor=executor,
definition_sha256=canonical_sha256(identity),
)
return PortableRunDefinitionRegistry(
(
blocked,
production.resolve_setup("m49-tgs-portable-v2"),
)
)
def _ready_registry() -> PortableRunDefinitionRegistry:
@@ -29,6 +29,7 @@ from k1link.observatory.portable_result_publisher import (
PortableObservatoryResultPublisher,
resolve_published_portable_calculation_profile,
)
from k1link.observatory.portable_result_view import PortableResultViewService
from k1link.observatory.portable_run_definitions import (
PortableRunDefinition,
PortableRunDefinitionRegistry,
@@ -372,7 +373,18 @@ def test_verified_package_publishes_immutable_binding_and_profile_provenance(
assert first.binding.session_id == RESULT_ID
assert first.binding.source_session_id == SOURCE_SESSION_ID
assert first.binding.config_sha256 == definition.definition_sha256
assert first.binding.replay_capability is None
assert first.binding.replay_capability is not None
assert first.binding.replay_capability.as_dict() == {
"schema_version": "missioncore.observation-lab-replay-capability/v2",
"kind": "portable-result-review",
"viewer_profile": "portable-result",
"timeline": "result-defined",
"activation": "explicit",
"commands_enabled": False,
}
assert first.binding.provenance["replay_capability"] == (
first.binding.replay_capability.as_dict()
)
assert first.binding.provenance["calculation_profile"] == {
"schema_version": OBSERVATORY_CALCULATION_PROFILE_SCHEMA,
"setup_id": definition.setup_id,
@@ -388,6 +400,15 @@ def test_verified_package_publishes_immutable_binding_and_profile_provenance(
assert store.get_lab_instance(RESULT_ID) == first.binding
assert store.get_session(SOURCE_SESSION_ID).summary.lab is None
view = PortableResultViewService(
sessions=store,
artifacts=CentralArtifactStore(tmp_path / "central-artifacts"),
).read(RESULT_ID)
assert view["result_id"] == RESULT_ID
assert view["source_session_id"] == SOURCE_SESSION_ID
assert view["definition_sha256"] == definition.definition_sha256
assert view["viewer_capability"] == first.binding.replay_capability.as_dict()
summary = store.get_session(RESULT_ID).summary
assert summary.display_name == (
"Portable result source · полный маршрут и воспроизведение"
@@ -19,7 +19,7 @@ from k1link.observatory.portable_run_definitions import (
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
REGISTRY_PATH = REPOSITORY_ROOT / "config" / "observatory-portable-run-definitions.json"
DEFINITION_SHA256 = "3692d41cec3949f348a36eb60a501fb2cd483fed1645679b0ec58061a2fc6dc2"
DEFINITION_SHA256 = "269d71a24b4e63cff54e01273f9d4b35fc6cdd72bc6fadec206169ae0777e6ac"
MODEL_MANIFEST_SHA256 = "3fd2d43af73bd73f89d9ffae95d8770cfdeb46033ec967509124fac6ae4afe56"
M49_DEFINITION_SHA256 = "f56d6321bd794ccdfb7d2e3b05d044b11f616ffb81ee29517386cc253046d4eb"
M49_MODEL_MANIFEST_SHA256 = "489a43448f720a9b5c7993dc8279d167b77191a586f0d87b6d38b81cf728e2f1"
@@ -47,6 +47,26 @@ def _first(document: dict[str, object]) -> dict[str, object]:
return first
def _blocked_lab_definition():
ready = _registry().resolve_setup("lab-v1-eomt-ddrnet-portable-v1")
executor = PortableExecutorAvailability(
contour_id=ready.executor.contour_id,
state="not-installed",
release_id=None,
release_sha256=None,
image_sha256=None,
reason_code="executor-not-installed",
reason="Executor release is not sealed or installed on Worker 006.",
)
identity = ready.identity_document()
identity["executor"] = executor.identity_document()
return replace(
ready,
executor=executor,
definition_sha256=canonical_sha256(identity),
)
def test_production_definition_is_source_independent_and_requirements_are_immutable() -> None:
definition = _registry().definitions[0]
requirements = definition.source_requirements
@@ -279,32 +299,43 @@ def test_duplicate_definition_and_incomplete_ready_executor_are_rejected(
executor = first["executor"]
assert isinstance(executor, dict)
executor["state"] = "ready"
executor["release_id"] = None
executor["release_sha256"] = None
executor["image_sha256"] = None
executor["reason_code"] = None
executor["reason"] = None
with pytest.raises(PortableRunDefinitionRegistryError, match="release identity"):
PortableRunDefinitionRegistry.from_file(_write(tmp_path, incomplete))
def test_blocked_lab_definition_does_not_hide_ready_m49_definition() -> None:
def test_production_definitions_are_both_ready_and_convertible() -> None:
registry = _registry()
definition = registry.definitions[0]
assert definition.executor.state == "not-installed"
assert definition.executor.release_id is None
assert definition.executor.release_sha256 is None
assert definition.executor.image_sha256 is None
with pytest.raises(
PortableRunDefinitionUnavailableError,
match="not sealed or installed",
):
definition.to_recorded_run_definition()
assert definition.executor.state == "ready"
assert definition.executor.release_id == "lab-v1-installed-package-v1"
assert definition.executor.release_sha256 == (
"667858623962cd6d9849a8985b803f59e429916b5c56f76a6fc6c80c0c54526b"
)
assert definition.executor.image_sha256 == (
"5ad7d95baac63af13812cb693d492add4e806a333aba8e60edb2ea1aba754373"
)
assert definition.to_recorded_run_definition().setup_id == definition.setup_id
ready = registry.ready_recorded_definitions()
assert tuple(row.setup_id for row in ready) == ("m49-tgs-portable-v2",)
assert tuple(row.setup_id for row in ready) == (
"lab-v1-eomt-ddrnet-portable-v1",
"m49-tgs-portable-v2",
)
assert registry.to_recorded_registry().definitions == ready
def test_conversion_to_recorded_definition_requires_and_preserves_sealed_identities() -> None:
blocked = _registry().definitions[0]
blocked = _blocked_lab_definition()
with pytest.raises(
PortableRunDefinitionUnavailableError,
match="not sealed or installed",
):
blocked.to_recorded_run_definition()
ready_executor = PortableExecutorAvailability(
contour_id="worker-006",
state="ready",
@@ -339,30 +370,11 @@ def test_conversion_to_recorded_definition_requires_and_preserves_sealed_identit
def test_blocked_definition_does_not_hide_an_unrelated_ready_definition() -> None:
registry = _registry()
blocked_lab = registry.resolve_setup("lab-v1-eomt-ddrnet-portable-v1")
blocked_lab = _blocked_lab_definition()
ready_m49 = registry.resolve_setup("m49-tgs-portable-v2")
ready_executor = PortableExecutorAvailability(
contour_id="worker-006",
state="ready",
release_id="lab-v1-eomt-ddrnet-executor-v1",
release_sha256="1" * 64,
image_sha256="2" * 64,
reason_code=None,
reason=None,
)
identity = blocked_lab.identity_document()
identity["executor"] = ready_executor.identity_document()
ready_lab = replace(
blocked_lab,
executor=ready_executor,
definition_sha256=canonical_sha256(identity),
)
mixed = PortableRunDefinitionRegistry((ready_lab, ready_m49))
mixed = PortableRunDefinitionRegistry((blocked_lab, ready_m49))
expected = (
ready_lab.to_recorded_run_definition(),
ready_m49.to_recorded_run_definition(),
)
expected = (ready_m49.to_recorded_run_definition(),)
assert mixed.ready_recorded_definitions() == expected
assert mixed.to_recorded_registry().definitions == expected
assert mixed.resolve_setup("m49-tgs-portable-v2") is ready_m49
@@ -394,7 +406,7 @@ def test_production_lab_v1_model_component_and_result_identities_are_exact() ->
"ea583966bc3409f5cf563cbf4fad05e366907e67187082eb692aff53d9f5d875"
)
assert components["eomt-recorded-runner-v1"].sha256 == (
"651e8e06c3912dffb036b7fd08f2c0623f7563d8306cc7aee05db562798518f4"
"1e64869de48d10f1531c742e6067c4c3ae2a709c5eb0d770d1fab74b4a2431ff"
)
assert components["k1-camera-1-calibration-v1"].sha256 == (
"05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
+28 -2
View File
@@ -92,7 +92,7 @@ def test_portable_setup_catalog_exposes_capability_and_executor_separately() ->
setup = document["setups"][0]
assert setup["origin"] == "portable-definition"
assert setup["source_compatibility"]["outcome"] == "pass"
assert setup["executor"]["state"] == "not-installed"
assert setup["executor"]["state"] == "ready"
assert setup["existing_results"] == []
assert setup["preflight"]["outcome"] == "blocked"
assert setup["preflight"]["submission_allowed"] is False
@@ -153,6 +153,32 @@ def _ready_lab_registry() -> PortableRunDefinitionRegistry:
)
def _blocked_lab_registry() -> PortableRunDefinitionRegistry:
ready = PortableRunDefinitionRegistry.from_file(REGISTRY_PATH).resolve_setup(
"lab-v1-eomt-ddrnet-portable-v1"
)
executor = PortableExecutorAvailability(
contour_id="worker-006",
state="not-installed",
release_id=None,
release_sha256=None,
image_sha256=None,
reason_code="executor-not-installed",
reason="Immutable executor release не установлен.",
)
identity = ready.identity_document()
identity["executor"] = executor.identity_document()
return PortableRunDefinitionRegistry(
(
replace(
ready,
executor=executor,
definition_sha256=canonical_sha256(identity),
),
)
)
class _PortableBinding:
def __init__(
self,
@@ -278,7 +304,7 @@ def test_portable_api_check_sha_fences_ready_submission(tmp_path: Path) -> None:
def test_portable_api_rejects_blocked_lab_executor_before_binding_submit(
tmp_path: Path,
) -> None:
full_registry = PortableRunDefinitionRegistry.from_file(REGISTRY_PATH)
full_registry = _blocked_lab_registry()
ready_registry = _ready_lab_registry()
queue = ObservatoryRecordedJobQueue(
tmp_path,
@@ -79,15 +79,18 @@ def test_local_worker_gate_is_fail_closed_and_accepts_only_exact_one() -> None:
)
def test_validator_registry_fails_when_one_required_profile_is_absent() -> None:
def test_validator_registry_selects_only_contracts_present_in_definitions() -> None:
definitions = _definitions()
only_lab_v1 = PortableRunDefinitionRegistry((definitions.definitions[0],))
with pytest.raises(
PortableWorkerIntegrationError,
match="required portable setup is unavailable",
):
portable_result_validator_registry(only_lab_v1)
validators = portable_result_validator_registry(only_lab_v1)
assert validators.registrations == (
PortableResultContractValidatorRegistration(
PORTABLE_LAB_V1_RESULT_CONTRACT_SHA256,
validate_lab_v1_result_v2,
),
)
def test_server_integration_constructs_dormant_transport_and_publisher(
+145 -54
View File
@@ -13,6 +13,7 @@ from k1link.observatory.portable_run_definitions import (
canonical_sha256,
)
from k1link.observatory.portable_worker_runtime import (
PortableWorkerAssetRequirement,
PortableWorkerAssetVerification,
PortableWorkerExecutorAdapter,
PortableWorkerExecutorSeal,
@@ -25,6 +26,7 @@ from k1link.observatory.portable_worker_runtime import (
PortableWorkerRuntimeRegistryError,
PortableWorkerRuntimeUnavailableError,
PortableWorkerSourceStage,
_matches_tree,
inspect_runtime_candidate,
)
from k1link.observatory.worker_agent import (
@@ -49,6 +51,27 @@ def _runtime() -> PortableWorkerRuntimeRegistry:
)
def _blocked_candidate(candidate):
phases = tuple(
PortableWorkerRuntimePhase(phase.phase_id, "missing")
for phase in candidate.phases
)
blockers = ("executor-release-unsealed",)
identity = candidate.identity_document()
identity["state"] = "blocked"
identity["executor"] = None
identity["phases"] = [phase.as_dict() for phase in phases]
identity["blockers"] = list(blockers)
return replace(
candidate,
state="blocked",
executor=None,
phases=phases,
blockers=blockers,
candidate_sha256=canonical_sha256(identity),
)
def _all_keys(value: object) -> set[str]:
if isinstance(value, dict):
return set(value) | {nested for child in value.values() for nested in _all_keys(child)}
@@ -57,7 +80,7 @@ def _all_keys(value: object) -> set[str]:
return set()
def test_production_candidates_bind_exact_definitions_and_only_m49_is_ready() -> None:
def test_production_candidates_bind_exact_definitions_and_both_are_ready() -> None:
registry = _runtime()
assert {candidate.setup_id for candidate in registry.candidates} == {
@@ -66,15 +89,16 @@ def test_production_candidates_bind_exact_definitions_and_only_m49_is_ready() ->
}
by_setup = {candidate.setup_id: candidate for candidate in registry.candidates}
lab_v1 = by_setup["lab-v1-eomt-ddrnet-portable-v1"]
assert lab_v1.ready is False
assert lab_v1.executor is None
assert "executor-release-unsealed" in lab_v1.blockers
assert any(phase.state == "missing" for phase in lab_v1.phases)
with pytest.raises(
PortableWorkerRuntimeUnavailableError,
match="no executor identity",
):
lab_v1.executor_identity()
assert lab_v1.ready is True
assert lab_v1.executor is not None
assert lab_v1.blockers == ()
assert all(phase.state == "implemented" for phase in lab_v1.phases)
assert lab_v1.executor_identity().release_sha256 == (
"4788005a24ae3e1664aa7aaaf728fb816243bfde8555c80a608f40d1061925b3"
)
assert lab_v1.executor_identity().image_sha256 == (
"840175ccac731b16b7f2815eaebc6e4ef426c49b57daf1911064ff5b7945a8fd"
)
m49 = by_setup["m49-tgs-portable-v2"]
assert m49.ready is True
@@ -180,15 +204,15 @@ def test_m49_portable_runner_source_release_is_content_addressed() -> None:
assert hashlib.sha256(path.read_bytes()).hexdigest() == item["sha256"]
def test_lab_candidate_verifies_reusable_repository_assets_without_claiming_executor() -> None:
def test_ready_lab_candidate_still_requires_complete_local_asset_admission() -> None:
candidate = _runtime().resolve(
"lab-v1-eomt-ddrnet-portable-v1",
"3692d41cec3949f348a36eb60a501fb2cd483fed1645679b0ec58061a2fc6dc2",
"11460f0fa03f713b4f64c8742bc30cc31eac66321931e1edd035553f5c72974a",
)
bindings = {
"ddrnet-goose-image": PortableWorkerLocalAssetBinding(
asset_id="ddrnet-goose-image",
image_sha256=("591cb382c099eeb05e7ec16e2371e0b2da54d2bb5c49ec0f4ac88dbf72b0f0cd"),
"agent-image": PortableWorkerLocalAssetBinding(
asset_id="agent-image",
image_sha256=("840175ccac731b16b7f2815eaebc6e4ef426c49b57daf1911064ff5b7945a8fd"),
),
"ddrnet-goose-runner": PortableWorkerLocalAssetBinding(
asset_id="ddrnet-goose-runner",
@@ -201,39 +225,22 @@ def test_lab_candidate_verifies_reusable_repository_assets_without_claiming_exec
/ "run_goose_vegetation_benchmark.py"
),
),
"eomt-image": PortableWorkerLocalAssetBinding(
asset_id="eomt-image",
image_sha256=("58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794"),
),
"eomt-orchestrator": PortableWorkerLocalAssetBinding(
asset_id="eomt-orchestrator",
"ddrnet-portable-config": PortableWorkerLocalAssetBinding(
asset_id="ddrnet-portable-config",
file_path=(
REPOSITORY_ROOT
/ "experiments"
/ "config"
/ "perception"
/ "worker"
/ "Invoke-E4FullSessionSegmentation.ps1"
/ "lab-v1-eomt-ddrnet-portable-v2.json"
),
),
"eomt-profile": PortableWorkerLocalAssetBinding(
asset_id="eomt-profile",
file_path=(
REPOSITORY_ROOT
/ "experiments"
/ "perception"
/ "worker"
/ "e3_k1_camera1_profile.json"
),
"ddrnet-step-image": PortableWorkerLocalAssetBinding(
asset_id="ddrnet-step-image",
image_sha256=("759eeac66145221b3c64f226b9ba9220363af36e4fda9353ef8169551587901c"),
),
"eomt-runner": PortableWorkerLocalAssetBinding(
asset_id="eomt-runner",
file_path=(
REPOSITORY_ROOT
/ "experiments"
/ "perception"
/ "worker"
/ "run_e4_full_session_segmentation.py"
),
"eomt-step-image": PortableWorkerLocalAssetBinding(
asset_id="eomt-step-image",
image_sha256=("29a7e8caef51e1809f11b66498ef8bf3f9b2c5d3de40acdab2968cdd34b9623b"),
),
"vegetation-policy": PortableWorkerLocalAssetBinding(
asset_id="vegetation-policy",
@@ -258,18 +265,16 @@ def test_lab_candidate_verifies_reusable_repository_assets_without_claiming_exec
admission = inspect_runtime_candidate(candidate, bindings)
states = {item.asset_id: item.state for item in admission.assets}
assert states["ddrnet-goose-image"] == "matched"
assert states["agent-image"] == "matched"
assert states["ddrnet-goose-runner"] == "matched"
assert states["eomt-image"] == "matched"
assert states["eomt-orchestrator"] == "matched"
assert states["eomt-profile"] == "matched"
assert states["eomt-runner"] == "matched"
assert states["ddrnet-portable-config"] == "missing"
assert states["ddrnet-portable-config"] == "matched"
assert states["ddrnet-step-image"] == "matched"
assert states["eomt-step-image"] == "matched"
assert states["ddrnet-checkpoint"] == "missing"
assert states["eomt-model-weights"] == "missing"
assert states["eomt-model-cache"] == "missing"
assert admission.ready is False
assert "ddrnet-component-port-uninstalled" in admission.blockers
assert "worker-installation-receipt-unavailable" in admission.blockers
assert "asset-ddrnet-checkpoint-missing" in admission.blockers
assert "asset-eomt-model-cache-missing" in admission.blockers
def test_local_asset_tampering_is_reported_without_execution(tmp_path: Path) -> None:
@@ -305,6 +310,78 @@ def test_local_asset_tampering_is_reported_without_execution(tmp_path: Path) ->
assert "asset-travel-tgs-image-mismatched" in admission.blockers
def test_sealed_local_tree_uses_manifest_receipt_and_member_metadata(
tmp_path: Path,
) -> None:
tree = tmp_path / "model-tree"
tree.mkdir()
(tree / "model.bin").write_bytes(b"model")
(tree / "profile.json").write_bytes(b"{}\n")
manifest = (
b"model.bin\t5\t9372c470eeadd5ec5f36cb0b9adf10545c93c5132503830bf1465fe7654b117b\n"
b"profile.json\t3\tca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356\n"
)
identity_sha256 = hashlib.sha256(manifest).hexdigest()
(tree / "tree-manifest.tsv").write_bytes(manifest)
(tree / "tree-receipt.json").write_text(
json.dumps(
{
"schema_version": "missioncore.sealed-tree-runtime/v1",
"asset_id": "eomt-model-tree",
"identity_algorithm": "relative-path-tab-size-tab-file-sha256-lf/v1",
"identity_sha256": identity_sha256,
"file_count": 2,
"byte_length": 8,
"manifest_relative_path": "tree-manifest.tsv",
}
),
encoding="utf-8",
)
requirement = PortableWorkerAssetRequirement(
asset_id="eomt-model-tree",
kind="local-tree",
sha256=identity_sha256,
byte_length=8,
component_id=None,
model_release_id=None,
model_artifact_role=None,
)
assert _matches_tree(requirement, tree) is True
receipt = json.loads((tree / "tree-receipt.json").read_text(encoding="utf-8"))
receipt.update(
{
"source_image_sha256": "1" * 64,
"source_path": "/usr/lib/ffmpeg/7.0",
"binaries": {
"model-bin": {
"relative_path": "model.bin",
"byte_length": 5,
"sha256": (
"9372c470eeadd5ec5f36cb0b9adf10545c93c5132503830bf1465fe7654b117b"
),
}
},
}
)
(tree / "tree-receipt.json").write_text(json.dumps(receipt), encoding="utf-8")
assert _matches_tree(requirement, tree) is True
receipt["unexpected"] = "not-admitted"
(tree / "tree-receipt.json").write_text(json.dumps(receipt), encoding="utf-8")
assert _matches_tree(requirement, tree) is False
receipt.pop("unexpected")
(tree / "tree-receipt.json").write_text(json.dumps(receipt), encoding="utf-8")
(tree / "profile.json").write_bytes(b"drift")
assert _matches_tree(requirement, tree) is False
def test_runtime_registry_rejects_digest_drift_and_executable_instructions(
tmp_path: Path,
) -> None:
@@ -328,12 +405,23 @@ def test_runtime_registry_rejects_digest_drift_and_executable_instructions(
def test_runtime_plan_is_identity_only_and_observation_only() -> None:
plan = PortableWorkerRuntimePlan(
job_id="observatory-run-" + ("a" * 32),
request_sha256="6" * 64,
identity_sha256="7" * 64,
submission_receipt_sha256="8" * 64,
claim_generation=1,
adapter_id="m49-tgs-worker006-portable-v2",
candidate_sha256="1" * 64,
setup_id="m49-tgs-portable-v2",
definition_id="m49-tgs-portable",
definition_version=3,
definition_sha256="2" * 64,
source_session_id="source-a",
source_catalog_sha256="9" * 64,
source_bundle_sha256="3" * 64,
source_capability_manifest_sha256="4" * 64,
source_adapter_id="xgrids-k1-recorded-observatory-v2",
source_adapter_version=2,
source_adapter_sha256="a" * 64,
result_contract_sha256="5" * 64,
phases=("source-delivery", "portable-tgs-runner"),
).as_dict()
@@ -373,10 +461,10 @@ def test_ready_local_adapter_composes_only_local_ports_and_exact_job(
definition_sha256=canonical_sha256(definition_identity),
)
blocked = _runtime().resolve(
blocked = _blocked_candidate(_runtime().resolve(
"lab-v1-eomt-ddrnet-portable-v1",
base_definition.definition_sha256,
)
))
executor_seal = PortableWorkerExecutorSeal(
release_id="lab-v1-portable-executor-v1",
release_sha256="1" * 64,
@@ -512,7 +600,10 @@ def test_ready_local_adapter_composes_only_local_ports_and_exact_job(
result_sha256=result_sha256,
)
with pytest.raises(PortableWorkerRuntimeUnavailableError):
with pytest.raises(
PortableWorkerRuntimeRegistryError,
match="blocked local runtime cannot bind a ready RunDefinition",
):
PortableWorkerExecutorAdapter(
candidate=blocked,
definition=base_definition,
@@ -0,0 +1,109 @@
from __future__ import annotations
from datetime import UTC, datetime
from pathlib import Path
from types import SimpleNamespace
from typing import Any, cast
from k1link.observatory.portable_publication_reconciler import (
PortablePublicationReconciler,
)
NOW = datetime(2026, 9, 1, 12, 0, tzinfo=UTC)
class _Queue:
def __init__(self, jobs: tuple[SimpleNamespace, ...]) -> None:
self.jobs = jobs
self.published: list[str] = []
self.failed: list[str] = []
def pending_publications(self) -> tuple[SimpleNamespace, ...]:
return self.jobs
def mark_published(self, job_id: str) -> None:
self.published.append(job_id)
def mark_publication_failed(self, job_id: str, *, message: str) -> None:
assert message
self.failed.append(job_id)
class _Transport:
def __init__(self, root: Path) -> None:
self.root = root
self.calls: list[str] = []
def package_root_for_terminal(self, job: SimpleNamespace) -> Path:
self.calls.append(job.job_id)
return self.root
class _Publisher:
def __init__(self) -> None:
self.calls: list[str] = []
def publish(self, *, job: SimpleNamespace, package_root: Path) -> None:
assert package_root.is_dir()
self.calls.append(job.job_id)
def test_reconciler_publishes_pending_result_without_compute(tmp_path: Path) -> None:
package = tmp_path / "package"
package.mkdir()
job = _job("pending", attempts=0, updated_at="2026-09-01T11:59:00Z")
queue = _Queue((job,))
transport = _Transport(package)
publisher = _Publisher()
reconciler = PortablePublicationReconciler(
queue=cast(Any, queue),
artifact_transport=cast(Any, transport),
result_publisher=cast(Any, publisher),
clock=lambda: NOW,
)
result = reconciler.run_once()
assert result.published == 1
assert result.failed == 0
assert queue.published == [job.job_id]
assert transport.calls == [job.job_id]
assert publisher.calls == [job.job_id]
def test_reconciler_defers_backoff_and_exhausts_bounded_attempts(tmp_path: Path) -> None:
package = tmp_path / "package"
package.mkdir()
deferred = _job("failed", attempts=1, updated_at="2026-09-01T11:59:50Z")
exhausted = _job("failed", attempts=5, updated_at="2026-09-01T10:00:00Z")
queue = _Queue((deferred, exhausted))
transport = _Transport(package)
publisher = _Publisher()
reconciler = PortablePublicationReconciler(
queue=cast(Any, queue),
artifact_transport=cast(Any, transport),
result_publisher=cast(Any, publisher),
clock=lambda: NOW,
)
result = reconciler.run_once()
assert result.deferred == 1
assert result.exhausted == 1
assert result.published == 0
assert transport.calls == []
assert publisher.calls == []
def _job(
publication_state: str,
*,
attempts: int,
updated_at: str,
) -> SimpleNamespace:
return SimpleNamespace(
job_id=f"observatory-run-{attempts:032x}",
publication_state=publication_state,
publication_attempts=attempts,
updated_at_utc=updated_at,
)
@@ -0,0 +1,87 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, cast
from fastapi import FastAPI
from fastapi.testclient import TestClient
from k1link.sessions.store import SessionStore
from k1link.web.observatory_api import build_observatory_router
JOB_ID = "observatory-run-0123456789abcdef0123456789abcdef"
class _Job:
state = "succeeded"
publication_state = "failed"
def as_dict(self) -> dict[str, object]:
return {
"job_id": JOB_ID,
"state": self.state,
"publication": {"state": self.publication_state},
}
class _Queue:
def __init__(self) -> None:
self.job = _Job()
self.published: list[str] = []
def get(self, job_id: str) -> _Job:
assert job_id == JOB_ID
return self.job
def mark_published(self, job_id: str) -> _Job:
assert job_id == JOB_ID
self.published.append(job_id)
self.job.publication_state = "published"
return self.job
class _Transport:
def __init__(self, package_root: Path) -> None:
self.package_root = package_root
self.calls: list[str] = []
def package_root_for_terminal(self, job: _Job) -> Path:
assert job.state == "succeeded"
self.calls.append(JOB_ID)
return self.package_root
class _Publisher:
def __init__(self) -> None:
self.calls: list[tuple[_Job, Path]] = []
def publish(self, *, job: _Job, package_root: Path) -> None:
self.calls.append((job, package_root))
def test_operator_retry_republishes_without_creating_compute_work(tmp_path: Path) -> None:
queue = _Queue()
package_root = tmp_path / "sealed-result"
package_root.mkdir()
transport = _Transport(package_root)
publisher = _Publisher()
app = FastAPI()
app.include_router(
build_observatory_router(
SessionStore(tmp_path / "repository", data_dir=tmp_path / "data"),
recorded_job_queue=cast(Any, queue),
recorded_binding_service=cast(Any, object()),
portable_artifact_transport=cast(Any, transport),
portable_result_publisher=cast(Any, publisher),
)
)
response = TestClient(app).post(
f"/api/v1/observatory/runs/{JOB_ID}/publication/retry"
)
assert response.status_code == 200
assert response.json()["publication"]["state"] == "published"
assert transport.calls == [JOB_ID]
assert publisher.calls == [(queue.job, package_root)]
assert queue.published == [JOB_ID]
+124
View File
@@ -21,6 +21,7 @@ from k1link.observatory.recorded_jobs import (
ObservatoryRecordedQueueStaleClaimError,
ObservatoryRecordedReconciliationRequest,
ObservatoryRecordedResourceReleaseAttestation,
RecordedExecutorIdentity,
RecordedRunDefinition,
RecordedRunDefinitionRegistry,
)
@@ -392,6 +393,129 @@ def test_claim_is_exactly_idempotent_including_empty_result(tmp_path: Path) -> N
)
def test_capability_aware_claim_skips_incompatible_queued_job(tmp_path: Path) -> None:
first, second = _definitions().definitions
compatible = replace(
second,
setup_id="compatible-portable-v1",
definition_id="compatible-portable",
definition_sha256="8" * 64,
executor_release_sha256="9" * 64,
executor_image_sha256="a" * 64,
model_manifest_sha256="b" * 64,
resource_profile_sha256="c" * 64,
)
queue = ObservatoryRecordedJobQueue(
tmp_path,
definitions=RecordedRunDefinitionRegistry((first, compatible)),
clock=lambda: NOW,
)
incompatible_job, _ = queue.submit(_intent(idempotency_key="capability:first"), enqueue=True)
compatible_job, _ = queue.submit(
_intent(
idempotency_key="capability:second",
setup_id=compatible.setup_id,
definition_sha256=compatible.definition_sha256,
),
enqueue=True,
)
capability = RecordedExecutorIdentity(
release_sha256=compatible.executor_release_sha256,
image_sha256=compatible.executor_image_sha256,
model_manifest_sha256=compatible.model_manifest_sha256,
resource_profile_sha256=compatible.resource_profile_sha256,
)
claim = queue.claim_next(
claimant_id="recorded-worker",
claim_request_id="capability-aware-poll",
supported_executor_identities=(capability,),
)
assert claim is not None
assert claim.job.job_id == compatible_job.job_id
assert queue.get(incompatible_job.job_id).state == "queued"
def test_capability_claim_identity_binds_snapshot_and_empty_snapshot_claims_nothing(
tmp_path: Path,
) -> None:
queue = _queue(tmp_path)
job, _ = queue.submit(_intent(), enqueue=True)
assert queue.claim_next(
claimant_id="recorded-worker",
claim_request_id="capability-empty-poll",
supported_executor_identities=(),
) is None
capability = RecordedExecutorIdentity(
release_sha256=EXECUTOR_RELEASE_SHA,
image_sha256=EXECUTOR_IMAGE_SHA,
model_manifest_sha256=MODEL_MANIFEST_SHA,
resource_profile_sha256=RESOURCE_PROFILE_SHA,
)
with pytest.raises(ObservatoryRecordedQueueConflictError, match="claim"):
queue.claim_next(
claimant_id="recorded-worker",
claim_request_id="capability-empty-poll",
supported_executor_identities=(capability,),
)
legacy_claim = queue.claim_next(
claimant_id="recorded-worker",
claim_request_id="legacy-poll-after-empty-capability",
)
assert legacy_claim is not None
assert legacy_claim.job.job_id == job.job_id
def test_execution_success_uses_durable_idempotent_publication_outbox(
tmp_path: Path,
) -> None:
queue = _queue(tmp_path)
job, _ = queue.submit(_intent(), enqueue=True)
claim = queue.claim_next(
claimant_id="recorded-worker",
claim_request_id="publication-outbox-claim",
)
assert claim is not None
queue.start(job.job_id, claim_token=claim.claim_token)
completed = queue.complete_for_publication(
job.job_id,
claim_token=claim.claim_token,
result_id="portable-result-001",
result_sha256=RESULT_SHA,
)
assert completed.state == "succeeded"
assert completed.publication_state == "pending"
assert completed.publication_attempts == 0
assert queue.pending_publications() == (completed,)
failed = queue.mark_publication_failed(
job.job_id,
message="catalog temporarily unavailable",
)
assert failed.publication_state == "failed"
assert failed.publication_attempts == 1
assert failed.publication_error == "catalog temporarily unavailable"
replay = queue.complete_for_publication(
job.job_id,
claim_token=claim.claim_token,
result_id="portable-result-001",
result_sha256=RESULT_SHA,
)
assert replay == failed
published = queue.mark_published(job.job_id)
assert published.publication_state == "published"
assert published.publication_attempts == 2
assert published.publication_error is None
assert published.published_at_utc == NOW
assert queue.mark_published(job.job_id) == published
assert queue.pending_publications() == ()
def test_claim_lease_renews_idempotently_and_requeues_expired_unstarted_job(
tmp_path: Path,
) -> None:
+9 -11
View File
@@ -114,10 +114,12 @@ class FakeTransport:
*,
claimant_id: str,
claim_request_id: str,
supported_executor_identities: tuple[ObservatoryWorkerExecutorIdentity, ...],
) -> Mapping[str, object] | None:
claim = self.queue.claim_next(
claimant_id=claimant_id,
claim_request_id=claim_request_id,
supported_executor_identities=supported_executor_identities,
)
if claim is None:
return None
@@ -397,7 +399,7 @@ def test_worker_agent_leaves_empty_queue_untouched(tmp_path: Path) -> None:
assert executor.jobs == []
def test_exact_release_mismatch_fails_closed_without_start(tmp_path: Path) -> None:
def test_exact_release_mismatch_is_not_claimed_or_failed(tmp_path: Path) -> None:
queue = _queue(tmp_path)
job_id = _enqueue(queue)
transport = FakeTransport(queue)
@@ -409,19 +411,13 @@ def test_exact_release_mismatch_fails_closed_without_start(tmp_path: Path) -> No
identity=_identity(release_sha256="b" * 64),
).run_once()
assert report.state == "failed"
assert report.failure_code == "executor-not-allowlisted"
assert report.state == "empty"
assert report.failure_code is None
assert transport.starts == []
assert transport.successes == []
assert transport.failures == [
(
job_id,
"executor-not-allowlisted",
"Exact executor identity is not installed on Worker 006.",
)
]
assert transport.failures == []
assert executor.jobs == []
assert queue.get(job_id).state == "failed"
assert queue.get(job_id).state == "queued"
def _spoof_claimant(payload: dict[str, object]) -> dict[str, object]:
@@ -525,9 +521,11 @@ class BlockingEmptyTransport:
*,
claimant_id: str,
claim_request_id: str,
supported_executor_identities: tuple[ObservatoryWorkerExecutorIdentity, ...],
) -> Mapping[str, object] | None:
assert claimant_id == WORKER_006_CONTOUR_ID
assert claim_request_id == "worker-006:overlap-test"
assert supported_executor_identities == ()
self.entered.set()
assert self.release.wait(timeout=2)
return None
+139 -21
View File
@@ -11,6 +11,7 @@ from fastapi.testclient import TestClient
from k1link.observatory.portable_artifact_transport import (
PortableArtifactTransportUnavailableError,
)
from k1link.observatory.portable_result_contract import PortableResultPublisherError
from k1link.observatory.recorded_jobs import (
ObservatoryRecordedJobIntent,
ObservatoryRecordedJobQueue,
@@ -32,7 +33,7 @@ WORKER_HEADERS = {
"Authorization": f"Bearer {WORKER_TOKEN}",
OBSERVATORY_WORKER_CONTOUR_HEADER: "worker-006",
}
CLAIM_SCHEMA = "missioncore.observatory-worker-claim-request/v1"
CLAIM_SCHEMA = "missioncore.observatory-worker-claim-request/v2"
START_SCHEMA = "missioncore.observatory-worker-start-request/v1"
RENEW_SCHEMA = "missioncore.observatory-worker-renew-request/v1"
CHECKPOINT_SCHEMA = "missioncore.observatory-worker-checkpoint-request/v1"
@@ -61,6 +62,22 @@ def _definition() -> RecordedRunDefinition:
)
def _claim_request(claim_request_id: str) -> dict[str, object]:
definition = _definition()
return {
"schema_version": CLAIM_SCHEMA,
"claim_request_id": claim_request_id,
"supported_executor_identities": [
{
"release_sha256": definition.executor_release_sha256,
"image_sha256": definition.executor_image_sha256,
"model_manifest_sha256": definition.model_manifest_sha256,
"resource_profile_sha256": definition.resource_profile_sha256,
}
],
}
def _services(tmp_path: Path) -> tuple[TestClient, ObservatoryRecordedJobQueue]:
definition = _definition()
queue = ObservatoryRecordedJobQueue(
@@ -85,6 +102,28 @@ class _ArtifactTransportWithoutCompletedPackage:
raise PortableArtifactTransportUnavailableError("package is incomplete")
class _CompletedArtifactTransport:
def __init__(self, package_root: Path) -> None:
self.package_root = package_root
def require_completed_for_success(self, **_values: object) -> Path:
return self.package_root
def package_root_for_terminal(self, _job: object) -> Path:
return self.package_root
class _TransientResultPublisher:
def __init__(self) -> None:
self.attempts = 0
def publish(self, **_values: object) -> object:
self.attempts += 1
if self.attempts == 1:
raise PortableResultPublisherError("catalog temporarily unavailable")
return object()
def _services_with_artifact_transport(
tmp_path: Path,
) -> tuple[TestClient, ObservatoryRecordedJobQueue]:
@@ -107,6 +146,30 @@ def _services_with_artifact_transport(
return TestClient(app), queue
def _services_with_transient_publisher(
tmp_path: Path,
) -> tuple[TestClient, ObservatoryRecordedJobQueue, _TransientResultPublisher]:
definition = _definition()
queue = ObservatoryRecordedJobQueue(
tmp_path,
definitions=RecordedRunDefinitionRegistry((definition,)),
)
publisher = _TransientResultPublisher()
app = FastAPI()
app.include_router(
build_observatory_worker_router(
queue,
authentication=ObservatoryWorkerAuthentication(
bearer_token_sha256=WORKER_TOKEN_SHA256,
contour_id="worker-006",
),
artifact_transport=_CompletedArtifactTransport(tmp_path / "package"), # type: ignore[arg-type]
result_publisher=publisher, # type: ignore[arg-type]
)
)
return TestClient(app), queue, publisher
def _enqueue(
queue: ObservatoryRecordedJobQueue,
*,
@@ -137,10 +200,7 @@ def _claim(
response = client.post(
"/api/v1/worker/observatory/recorded-jobs/claims",
headers=WORKER_HEADERS,
json={
"schema_version": CLAIM_SCHEMA,
"claim_request_id": claim_request_id,
},
json=_claim_request(claim_request_id),
)
assert response.status_code == 200
return cast(dict[str, Any], response.json())
@@ -151,10 +211,7 @@ def test_worker_authentication_requires_digest_and_configured_contour(
) -> None:
client, queue = _services(tmp_path)
_enqueue(queue)
request = {
"schema_version": CLAIM_SCHEMA,
"claim_request_id": "worker-006:auth-claim",
}
request = _claim_request("worker-006:auth-claim")
missing = client.post(
"/api/v1/worker/observatory/recorded-jobs/claims",
@@ -198,10 +255,7 @@ def test_claim_is_idempotent_and_request_schema_rejects_execution_inputs(
) -> None:
client, queue = _services(tmp_path)
job_id = _enqueue(queue)
request = {
"schema_version": CLAIM_SCHEMA,
"claim_request_id": "worker-006:stable-claim",
}
request = _claim_request("worker-006:stable-claim")
first = client.post(
"/api/v1/worker/observatory/recorded-jobs/claims",
@@ -235,14 +289,30 @@ def test_claim_is_idempotent_and_request_schema_rejects_execution_inputs(
assert queue.get(job_id).state == "claimed"
def test_legacy_claim_without_capability_snapshot_cannot_take_another_profile(
tmp_path: Path,
) -> None:
client, queue = _services(tmp_path)
job_id = _enqueue(queue)
response = client.post(
"/api/v1/worker/observatory/recorded-jobs/claims",
headers=WORKER_HEADERS,
json={
"schema_version": "missioncore.observatory-worker-claim-request/v1",
"claim_request_id": "worker-006:legacy-unbounded-claim",
},
)
assert response.status_code == 422
assert queue.get(job_id).state == "queued"
def test_empty_claim_is_204_and_empty_receipt_remains_idempotent(
tmp_path: Path,
) -> None:
client, queue = _services(tmp_path)
request = {
"schema_version": CLAIM_SCHEMA,
"claim_request_id": "worker-006:empty-poll",
}
request = _claim_request("worker-006:empty-poll")
first = client.post(
"/api/v1/worker/observatory/recorded-jobs/claims",
@@ -258,10 +328,7 @@ def test_empty_claim_is_204_and_empty_receipt_remains_idempotent(
fresh = client.post(
"/api/v1/worker/observatory/recorded-jobs/claims",
headers=WORKER_HEADERS,
json={
"schema_version": CLAIM_SCHEMA,
"claim_request_id": "worker-006:fresh-poll",
},
json=_claim_request("worker-006:fresh-poll"),
)
assert first.status_code == 204
@@ -456,6 +523,57 @@ def test_artifact_transport_blocks_success_without_completed_package(
assert queue.get(job_id).state == "running"
def test_publication_failure_is_durable_and_retry_does_not_rerun_execution(
tmp_path: Path,
) -> None:
client, queue, publisher = _services_with_transient_publisher(tmp_path)
job_id = _enqueue(queue)
claim_token = _claim(client)["claim_token"]
client.post(
f"/api/v1/worker/observatory/recorded-jobs/{job_id}/start",
headers=WORKER_HEADERS,
json={"schema_version": START_SCHEMA, "claim_token": claim_token},
)
sealed = client.post(
f"/api/v1/worker/observatory/recorded-jobs/{job_id}/succeed",
headers=WORKER_HEADERS,
json={
"schema_version": SUCCEED_SCHEMA,
"claim_token": claim_token,
"result_id": "portable-result-publication-retry",
"result_sha256": "b" * 64,
},
)
assert sealed.status_code == 202
assert sealed.json()["state"] == "succeeded"
assert sealed.json()["publication"] == {
"state": "failed",
"attempts": 1,
"error": "catalog temporarily unavailable",
"published_at_utc": None,
}
assert queue.get(job_id).claim_generation == 1
retried = client.post(
f"/api/v1/worker/observatory/recorded-jobs/{job_id}/publication/retry",
headers=WORKER_HEADERS,
)
repeated = client.post(
f"/api/v1/worker/observatory/recorded-jobs/{job_id}/publication/retry",
headers=WORKER_HEADERS,
)
assert retried.status_code == 200
assert retried.json()["publication"]["state"] == "published"
assert retried.json()["publication"]["attempts"] == 2
assert repeated.status_code == 200
assert repeated.json()["publication"]["attempts"] == 2
assert queue.get(job_id).claim_generation == 1
assert publisher.attempts == 2
def test_worker_can_fail_claimed_job_idempotently(tmp_path: Path) -> None:
client, queue = _services(tmp_path)
job_id = _enqueue(queue)
@@ -99,6 +99,7 @@ def _cache_claim(gateway: ObservatoryWorkerHttpGateway) -> None:
payload = gateway.claim_next(
claimant_id=WORKER_006_CONTOUR_ID,
claim_request_id="worker-006:http-transport-test",
supported_executor_identities=(),
)
assert payload is not None