feat(worker): compose all ready observatory profiles
This commit is contained in:
@@ -42,22 +42,21 @@ from k1link.observatory.portable_run_definitions import (
|
||||
)
|
||||
from k1link.observatory.portable_worker_runtime import (
|
||||
PortableWorkerLocalAssetBinding,
|
||||
PortableWorkerRuntimeAdmission,
|
||||
PortableWorkerRuntimeCandidate,
|
||||
PortableWorkerRuntimeRegistry,
|
||||
inspect_runtime_candidate,
|
||||
)
|
||||
from k1link.observatory.worker_agent import (
|
||||
WORKER_006_CONTOUR_ID,
|
||||
ObservatoryWorkerAgent,
|
||||
ObservatoryWorkerExecutorRegistration,
|
||||
ObservatoryWorkerExecutorRegistry,
|
||||
)
|
||||
from k1link.observatory.worker_http_transport import ObservatoryWorkerHttpGateway
|
||||
from k1link.observatory.worker_service import (
|
||||
InstalledObservatoryWorkerService,
|
||||
ObservatoryWorkerExecutorBuildContext,
|
||||
ObservatoryWorkerExecutorBuilderRegistration,
|
||||
ObservatoryWorkerServiceConfiguration,
|
||||
load_observatory_worker_bearer_token,
|
||||
require_ready_executor_coverage,
|
||||
compose_installed_observatory_worker_service_from_builders,
|
||||
)
|
||||
|
||||
M49_WORKER_DEFINITIONS_FILE_ENV: Final = "MISSIONCORE_OBSERVATORY_WORKER_DEFINITIONS_FILE"
|
||||
@@ -268,6 +267,44 @@ class M49WorkerInstallationReceipt:
|
||||
return bindings
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _M49WorkerExecutorBuilder:
|
||||
"""Bind the already admitted fixed M4.9 installation to shared Worker ports."""
|
||||
|
||||
definition: PortableRunDefinition
|
||||
candidate: PortableWorkerRuntimeCandidate
|
||||
admission: PortableWorkerRuntimeAdmission
|
||||
installation: M49PortableRunnerInstallation
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
context: ObservatoryWorkerExecutorBuildContext,
|
||||
) -> ObservatoryWorkerExecutorRegistration:
|
||||
if (
|
||||
context.definition.setup_id != self.definition.setup_id
|
||||
or context.definition.definition_sha256 != self.definition.definition_sha256
|
||||
or context.candidate.setup_id != self.candidate.setup_id
|
||||
or context.candidate.candidate_sha256 != self.candidate.candidate_sha256
|
||||
):
|
||||
raise M49WorkerCompositionError(
|
||||
"M4.9 builder context differs from its admitted installation"
|
||||
)
|
||||
adapter = compose_m49_portable_executor_adapter(
|
||||
candidate=context.candidate,
|
||||
definition=context.definition,
|
||||
admission=self.admission,
|
||||
source_transport=context.source_transport,
|
||||
result_transport=context.result_transport,
|
||||
installation=self.installation,
|
||||
source_output_parent=context.work_root / "m49-portable" / "source-output",
|
||||
created_at_utc=_utc_now,
|
||||
)
|
||||
return ObservatoryWorkerExecutorRegistration(
|
||||
identity=context.candidate.executor_identity(),
|
||||
adapter=adapter,
|
||||
)
|
||||
|
||||
|
||||
def load_m49_worker_installation_receipt(
|
||||
path: Path,
|
||||
) -> M49WorkerInstallationReceipt:
|
||||
@@ -397,9 +434,10 @@ def load_m49_worker_installation_receipt(
|
||||
def compose_installed_m49_worker_service(
|
||||
configuration: M49WorkerEntrypointConfiguration,
|
||||
*,
|
||||
executor_builders: Sequence[ObservatoryWorkerExecutorBuilderRegistration] = (),
|
||||
http_transport: httpx.BaseTransport | None = None,
|
||||
) -> InstalledObservatoryWorkerService:
|
||||
"""Compose one fixed M4.9 executor around one shared HTTP gateway."""
|
||||
"""Compose M4.9 and any other ready installed profile into one Worker agent."""
|
||||
|
||||
if os.name != "posix":
|
||||
raise M49WorkerCompositionError("portable M4.9 Worker requires a POSIX runtime")
|
||||
@@ -436,50 +474,22 @@ def compose_installed_m49_worker_service(
|
||||
),
|
||||
output_parent=configuration.worker.work_root / "m49-portable" / "runner-output",
|
||||
)
|
||||
token = load_observatory_worker_bearer_token(configuration.worker.bearer_token_file)
|
||||
gateway: ObservatoryWorkerHttpGateway | None = None
|
||||
try:
|
||||
gateway = ObservatoryWorkerHttpGateway(
|
||||
base_url=configuration.worker.base_url,
|
||||
bearer_token=token,
|
||||
work_root=configuration.worker.work_root,
|
||||
transport=http_transport,
|
||||
)
|
||||
adapter = compose_m49_portable_executor_adapter(
|
||||
candidate=candidate,
|
||||
m49_builder = ObservatoryWorkerExecutorBuilderRegistration(
|
||||
setup_id=M49_WORKER_SETUP_ID,
|
||||
builder=_M49WorkerExecutorBuilder(
|
||||
definition=definition,
|
||||
candidate=candidate,
|
||||
admission=admission,
|
||||
source_transport=gateway,
|
||||
result_transport=gateway,
|
||||
installation=installation,
|
||||
source_output_parent=(
|
||||
configuration.worker.work_root / "m49-portable" / "source-output"
|
||||
),
|
||||
created_at_utc=_utc_now,
|
||||
)
|
||||
executors = ObservatoryWorkerExecutorRegistry(
|
||||
(
|
||||
ObservatoryWorkerExecutorRegistration(
|
||||
identity=candidate.executor_identity(),
|
||||
adapter=adapter,
|
||||
),
|
||||
)
|
||||
)
|
||||
require_ready_executor_coverage(
|
||||
definitions=definitions,
|
||||
executors=executors,
|
||||
)
|
||||
return InstalledObservatoryWorkerService(
|
||||
configuration=configuration.worker,
|
||||
gateway=gateway,
|
||||
agent=ObservatoryWorkerAgent(transport=gateway, executors=executors),
|
||||
)
|
||||
except Exception:
|
||||
if gateway is not None:
|
||||
gateway.close()
|
||||
raise
|
||||
finally:
|
||||
del token
|
||||
),
|
||||
)
|
||||
return compose_installed_observatory_worker_service_from_builders(
|
||||
configuration=configuration.worker,
|
||||
definitions=definitions,
|
||||
runtime_registry=runtime_registry,
|
||||
builders=(m49_builder, *executor_builders),
|
||||
http_transport=http_transport,
|
||||
)
|
||||
|
||||
|
||||
def run_installed_m49_worker(
|
||||
@@ -520,10 +530,11 @@ def _verify_ready_identity(
|
||||
definition: PortableRunDefinition,
|
||||
candidate: PortableWorkerRuntimeCandidate,
|
||||
) -> None:
|
||||
ready = definitions.ready_recorded_definitions()
|
||||
ready_identities = {
|
||||
(item.setup_id, item.definition_sha256) for item in definitions.ready_recorded_definitions()
|
||||
}
|
||||
if (
|
||||
len(ready) != 1
|
||||
or ready[0].setup_id != M49_WORKER_SETUP_ID
|
||||
(definition.setup_id, definition.definition_sha256) not in ready_identities
|
||||
or definition.setup_id != M49_WORKER_SETUP_ID
|
||||
or definition.executor.contour_id != WORKER_006_CONTOUR_ID
|
||||
or candidate.adapter_id != M49_WORKER_ADAPTER_ID
|
||||
@@ -531,7 +542,7 @@ def _verify_ready_identity(
|
||||
or not candidate.ready
|
||||
):
|
||||
raise M49WorkerCompositionError(
|
||||
"fixed M4.9 Worker requires exactly one ready M4.9 definition"
|
||||
"fixed M4.9 Worker requires its exact ready M4.9 definition"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -12,20 +12,30 @@ from __future__ import annotations
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
from collections.abc import Callable, Mapping
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from threading import Event
|
||||
from typing import Final
|
||||
from typing import Final, Protocol
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import httpx
|
||||
|
||||
from k1link.observatory.portable_run_definitions import PortableRunDefinitionRegistry
|
||||
from k1link.observatory.portable_run_definitions import (
|
||||
PortableRunDefinition,
|
||||
PortableRunDefinitionRegistry,
|
||||
)
|
||||
from k1link.observatory.portable_worker_runtime import (
|
||||
PortableWorkerResultPublisher,
|
||||
PortableWorkerRuntimeCandidate,
|
||||
PortableWorkerRuntimeRegistry,
|
||||
PortableWorkerSourceMaterializer,
|
||||
)
|
||||
from k1link.observatory.worker_agent import (
|
||||
ObservatoryWorkerAgent,
|
||||
ObservatoryWorkerCycleReport,
|
||||
ObservatoryWorkerExecutorIdentity,
|
||||
ObservatoryWorkerExecutorRegistration,
|
||||
ObservatoryWorkerExecutorRegistry,
|
||||
ObservatoryWorkerExecutorUnavailableError,
|
||||
)
|
||||
@@ -37,9 +47,7 @@ from k1link.observatory.worker_http_transport import (
|
||||
OBSERVATORY_WORKER_BASE_URL_ENV: Final = "MISSIONCORE_OBSERVATORY_WORKER_BASE_URL"
|
||||
OBSERVATORY_WORKER_TOKEN_FILE_ENV: Final = "MISSIONCORE_OBSERVATORY_WORKER_TOKEN_FILE"
|
||||
OBSERVATORY_WORKER_WORK_ROOT_ENV: Final = "MISSIONCORE_OBSERVATORY_WORKER_WORK_ROOT"
|
||||
OBSERVATORY_WORKER_IDLE_POLL_SECONDS_ENV: Final = (
|
||||
"MISSIONCORE_OBSERVATORY_WORKER_IDLE_POLL_SECONDS"
|
||||
)
|
||||
OBSERVATORY_WORKER_IDLE_POLL_SECONDS_ENV: Final = "MISSIONCORE_OBSERVATORY_WORKER_IDLE_POLL_SECONDS"
|
||||
OBSERVATORY_WORKER_TRANSPORT_BACKOFF_SECONDS_ENV: Final = (
|
||||
"MISSIONCORE_OBSERVATORY_WORKER_TRANSPORT_BACKOFF_SECONDS"
|
||||
)
|
||||
@@ -53,12 +61,50 @@ DEFAULT_OBSERVATORY_WORKER_TRANSPORT_BACKOFF_SECONDS: Final = 5.0
|
||||
DEFAULT_OBSERVATORY_WORKER_MAX_TRANSPORT_FAILURES: Final = 12
|
||||
|
||||
_TOKEN = re.compile(r"^[A-Za-z0-9._:-]{32,512}$")
|
||||
_SETUP_ID = re.compile(r"^[a-z][a-z0-9-]{2,95}$")
|
||||
|
||||
|
||||
class ObservatoryWorkerServiceError(RuntimeError):
|
||||
"""Worker 006 cannot start without its exact local operational boundary."""
|
||||
|
||||
|
||||
class ObservatoryWorkerExecutorBuilder(Protocol):
|
||||
"""Installed code port that builds one reviewed executor before polling."""
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
context: ObservatoryWorkerExecutorBuildContext,
|
||||
) -> ObservatoryWorkerExecutorRegistration: ...
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ObservatoryWorkerExecutorBuildContext:
|
||||
"""Fixed local inputs shared with one install-time executor builder."""
|
||||
|
||||
definition: PortableRunDefinition
|
||||
candidate: PortableWorkerRuntimeCandidate
|
||||
source_transport: PortableWorkerSourceMaterializer
|
||||
result_transport: PortableWorkerResultPublisher
|
||||
work_root: Path
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_absolute_path(self.work_root, "Worker build work root")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ObservatoryWorkerExecutorBuilderRegistration:
|
||||
"""Local setup-to-builder binding; queued jobs cannot populate this map."""
|
||||
|
||||
setup_id: str
|
||||
builder: ObservatoryWorkerExecutorBuilder
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if _SETUP_ID.fullmatch(self.setup_id) is None:
|
||||
raise ValueError("Worker executor builder setup id is invalid")
|
||||
if not callable(self.builder):
|
||||
raise ValueError("Worker executor builder is not callable")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ObservatoryWorkerServiceConfiguration:
|
||||
"""Path-only service configuration; it never contains a plaintext secret."""
|
||||
@@ -67,12 +113,8 @@ class ObservatoryWorkerServiceConfiguration:
|
||||
bearer_token_file: Path
|
||||
work_root: Path
|
||||
idle_poll_seconds: float = DEFAULT_OBSERVATORY_WORKER_IDLE_POLL_SECONDS
|
||||
transport_backoff_seconds: float = (
|
||||
DEFAULT_OBSERVATORY_WORKER_TRANSPORT_BACKOFF_SECONDS
|
||||
)
|
||||
max_consecutive_transport_failures: int = (
|
||||
DEFAULT_OBSERVATORY_WORKER_MAX_TRANSPORT_FAILURES
|
||||
)
|
||||
transport_backoff_seconds: float = DEFAULT_OBSERVATORY_WORKER_TRANSPORT_BACKOFF_SECONDS
|
||||
max_consecutive_transport_failures: int = DEFAULT_OBSERVATORY_WORKER_MAX_TRANSPORT_FAILURES
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_worker_base_url(self.base_url)
|
||||
@@ -191,9 +233,7 @@ def compose_installed_observatory_worker_service(
|
||||
"""
|
||||
|
||||
require_ready_executor_coverage(definitions=definitions, executors=executors)
|
||||
bearer_token = load_observatory_worker_bearer_token(
|
||||
configuration.bearer_token_file
|
||||
)
|
||||
bearer_token = load_observatory_worker_bearer_token(configuration.bearer_token_file)
|
||||
try:
|
||||
gateway = ObservatoryWorkerHttpGateway(
|
||||
base_url=configuration.base_url,
|
||||
@@ -212,6 +252,119 @@ def compose_installed_observatory_worker_service(
|
||||
)
|
||||
|
||||
|
||||
def compose_installed_observatory_worker_service_from_builders(
|
||||
*,
|
||||
configuration: ObservatoryWorkerServiceConfiguration,
|
||||
definitions: PortableRunDefinitionRegistry,
|
||||
runtime_registry: PortableWorkerRuntimeRegistry,
|
||||
builders: Sequence[ObservatoryWorkerExecutorBuilderRegistration],
|
||||
http_transport: httpx.BaseTransport | None = None,
|
||||
) -> InstalledObservatoryWorkerService:
|
||||
"""Build complete ready-profile coverage before one agent can claim work.
|
||||
|
||||
Builders are reviewed local installation ports. Mission Core jobs supply
|
||||
neither builders nor executable locators. Every ready definition must have
|
||||
one builder, and that builder must return the exact executor identity sealed
|
||||
by the matching runtime candidate. Composition fails and closes transport
|
||||
before constructing an agent if any ready profile is missing or mismatched.
|
||||
"""
|
||||
|
||||
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(
|
||||
definitions=definitions,
|
||||
runtime_registry=runtime_registry,
|
||||
builders=builders,
|
||||
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(
|
||||
*,
|
||||
definitions: PortableRunDefinitionRegistry,
|
||||
runtime_registry: PortableWorkerRuntimeRegistry,
|
||||
builders: Sequence[ObservatoryWorkerExecutorBuilderRegistration],
|
||||
source_transport: PortableWorkerSourceMaterializer,
|
||||
result_transport: PortableWorkerResultPublisher,
|
||||
work_root: Path,
|
||||
) -> ObservatoryWorkerExecutorRegistry:
|
||||
"""Build one exact registry covering every ready definition, or reject it."""
|
||||
|
||||
_absolute_path(work_root, "Worker build work root")
|
||||
builders_by_setup: dict[str, ObservatoryWorkerExecutorBuilder] = {}
|
||||
for registration in builders:
|
||||
if registration.setup_id in builders_by_setup:
|
||||
raise ObservatoryWorkerServiceError("Worker executor builder setup ids must be unique")
|
||||
builders_by_setup[registration.setup_id] = registration.builder
|
||||
|
||||
ready_definitions = definitions.ready_recorded_definitions()
|
||||
ready_setup_ids = {definition.setup_id for definition in ready_definitions}
|
||||
builder_setup_ids = set(builders_by_setup)
|
||||
if ready_setup_ids - builder_setup_ids:
|
||||
raise ObservatoryWorkerServiceError(
|
||||
"a ready portable RunDefinition has no installed local builder"
|
||||
)
|
||||
if builder_setup_ids - ready_setup_ids:
|
||||
raise ObservatoryWorkerServiceError(
|
||||
"an installed local builder targets a non-ready RunDefinition"
|
||||
)
|
||||
|
||||
registrations: list[ObservatoryWorkerExecutorRegistration] = []
|
||||
for recorded_definition in ready_definitions:
|
||||
definition = definitions.resolve(
|
||||
recorded_definition.setup_id,
|
||||
recorded_definition.definition_sha256,
|
||||
)
|
||||
builder = builders_by_setup[definition.setup_id]
|
||||
candidate = runtime_registry.resolve(
|
||||
definition.setup_id,
|
||||
definition.definition_sha256,
|
||||
)
|
||||
if not candidate.ready:
|
||||
raise ObservatoryWorkerServiceError(
|
||||
"a ready portable RunDefinition has no ready local runtime candidate"
|
||||
)
|
||||
expected_identity = candidate.executor_identity()
|
||||
built = builder(
|
||||
ObservatoryWorkerExecutorBuildContext(
|
||||
definition=definition,
|
||||
candidate=candidate,
|
||||
source_transport=source_transport,
|
||||
result_transport=result_transport,
|
||||
work_root=work_root,
|
||||
)
|
||||
)
|
||||
if built.identity != expected_identity:
|
||||
raise ObservatoryWorkerServiceError(
|
||||
"installed executor builder returned another exact identity"
|
||||
)
|
||||
registrations.append(built)
|
||||
|
||||
executors = ObservatoryWorkerExecutorRegistry(tuple(registrations))
|
||||
require_ready_executor_coverage(definitions=definitions, executors=executors)
|
||||
return executors
|
||||
|
||||
|
||||
def require_ready_executor_coverage(
|
||||
*,
|
||||
definitions: PortableRunDefinitionRegistry,
|
||||
@@ -221,9 +374,7 @@ def require_ready_executor_coverage(
|
||||
|
||||
ready = definitions.ready_recorded_definitions()
|
||||
if not ready:
|
||||
raise ObservatoryWorkerServiceError(
|
||||
"no portable RunDefinition has a sealed ready executor"
|
||||
)
|
||||
raise ObservatoryWorkerServiceError("no portable RunDefinition has a sealed ready executor")
|
||||
identities: list[ObservatoryWorkerExecutorIdentity] = []
|
||||
for definition in ready:
|
||||
identity = ObservatoryWorkerExecutorIdentity(
|
||||
@@ -254,9 +405,7 @@ def load_observatory_worker_bearer_token(path: Path) -> str:
|
||||
)
|
||||
metadata = os.fstat(descriptor)
|
||||
if not stat.S_ISREG(metadata.st_mode):
|
||||
raise ObservatoryWorkerServiceError(
|
||||
"Worker bearer credential must be a regular file"
|
||||
)
|
||||
raise ObservatoryWorkerServiceError("Worker bearer credential must be a regular file")
|
||||
if os.name != "posix":
|
||||
raise ObservatoryWorkerServiceError(
|
||||
"native Worker credential ACL verification is not available; "
|
||||
@@ -267,31 +416,23 @@ def load_observatory_worker_bearer_token(path: Path) -> str:
|
||||
"Worker bearer credential permissions are too broad"
|
||||
)
|
||||
if not 32 <= metadata.st_size <= 512:
|
||||
raise ObservatoryWorkerServiceError(
|
||||
"Worker bearer credential format is invalid"
|
||||
)
|
||||
raise ObservatoryWorkerServiceError("Worker bearer credential format is invalid")
|
||||
with os.fdopen(descriptor, "rb") as stream:
|
||||
descriptor = None
|
||||
payload = stream.read(513)
|
||||
except ObservatoryWorkerServiceError:
|
||||
raise
|
||||
except OSError as exc:
|
||||
raise ObservatoryWorkerServiceError(
|
||||
"Worker bearer credential is unavailable"
|
||||
) from exc
|
||||
raise ObservatoryWorkerServiceError("Worker bearer credential is unavailable") from exc
|
||||
finally:
|
||||
if descriptor is not None:
|
||||
os.close(descriptor)
|
||||
try:
|
||||
token = payload.decode("ascii")
|
||||
except UnicodeDecodeError as exc:
|
||||
raise ObservatoryWorkerServiceError(
|
||||
"Worker bearer credential is not ASCII"
|
||||
) from exc
|
||||
raise ObservatoryWorkerServiceError("Worker bearer credential is not ASCII") from exc
|
||||
if _TOKEN.fullmatch(token) is None:
|
||||
raise ObservatoryWorkerServiceError(
|
||||
"Worker bearer credential format is invalid"
|
||||
)
|
||||
raise ObservatoryWorkerServiceError("Worker bearer credential format is invalid")
|
||||
return token
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user