292 lines
12 KiB
Python
292 lines
12 KiB
Python
"""Versioned launchd declaration for the canonical local Mission Core service."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import os
|
|
import plistlib
|
|
import stat
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Final, cast
|
|
|
|
MISSION_CORE_LAUNCH_AGENT_LABEL: Final = "com.nodedc.mission-core.local"
|
|
MISSION_CORE_LAUNCH_AGENT_SCHEMA: Final = "missioncore.local-launch-agent-plan/v1"
|
|
OBSERVATORY_LOCAL_WORKER_ENABLED_ENV: Final = (
|
|
"MISSIONCORE_OBSERVATORY_WORKER_LOCAL_ENABLED"
|
|
)
|
|
OBSERVATORY_SOURCE_CAS_ROOT_ENV: Final = (
|
|
"MISSIONCORE_OBSERVATORY_WORKER_SOURCE_CAS_ROOT"
|
|
)
|
|
OBSERVATORY_RESULT_STAGING_ROOT_ENV: Final = (
|
|
"MISSIONCORE_OBSERVATORY_WORKER_RESULT_STAGING_ROOT"
|
|
)
|
|
ARTIFACT_STORE_ROOT_ENV: Final = "MISSIONCORE_ARTIFACT_STORE_ROOT"
|
|
|
|
|
|
class MissionCoreLaunchAgentError(RuntimeError):
|
|
"""The local launch agent cannot be planned without weakening its boundary."""
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class MissionCoreLaunchAgentPlan:
|
|
agent_path: Path
|
|
current_sha256: str
|
|
desired_sha256: str
|
|
current_working_directory: Path
|
|
desired_working_directory: Path
|
|
preserved_data_directory: Path | None
|
|
current_program_arguments: tuple[str, ...]
|
|
desired_program_arguments: tuple[str, ...]
|
|
local_observatory_worker_enabled: bool
|
|
desired_payload: bytes
|
|
|
|
def to_dict(self) -> dict[str, object]:
|
|
return {
|
|
"schema_version": MISSION_CORE_LAUNCH_AGENT_SCHEMA,
|
|
"label": MISSION_CORE_LAUNCH_AGENT_LABEL,
|
|
"agent_path": str(self.agent_path),
|
|
"current_sha256": self.current_sha256,
|
|
"desired_sha256": self.desired_sha256,
|
|
"current_working_directory": str(self.current_working_directory),
|
|
"desired_working_directory": str(self.desired_working_directory),
|
|
"preserved_data_directory": (
|
|
str(self.preserved_data_directory)
|
|
if self.preserved_data_directory is not None
|
|
else None
|
|
),
|
|
"current_program_arguments": list(self.current_program_arguments),
|
|
"desired_program_arguments": list(self.desired_program_arguments),
|
|
"changes": {
|
|
"repository_migration": self.current_working_directory
|
|
!= self.desired_working_directory,
|
|
"data_directory_preserved": self.preserved_data_directory is not None,
|
|
"preserved_data_directory": (
|
|
str(self.preserved_data_directory)
|
|
if self.preserved_data_directory is not None
|
|
else None
|
|
),
|
|
"dependency_sync_disabled": "--no-sync"
|
|
in self.desired_program_arguments,
|
|
"self_health_watchdog": True,
|
|
"bounded_launchd_exit_timeout_seconds": 20,
|
|
"keep_alive": True,
|
|
"process_group_owned": True,
|
|
"local_observatory_worker_enabled": (
|
|
self.local_observatory_worker_enabled
|
|
),
|
|
},
|
|
}
|
|
|
|
|
|
def plan_mission_core_launch_agent(
|
|
*,
|
|
repository_root: Path,
|
|
agent_path: Path,
|
|
expected_current_repository_root: Path | None = None,
|
|
enable_local_observatory_worker: bool = False,
|
|
) -> MissionCoreLaunchAgentPlan:
|
|
repository = repository_root.expanduser().resolve(strict=True)
|
|
expected_current_repository = (
|
|
expected_current_repository_root.expanduser().resolve(strict=True)
|
|
if expected_current_repository_root is not None
|
|
else None
|
|
)
|
|
path = agent_path.expanduser().absolute()
|
|
current_payload = _read_private_regular_file(path)
|
|
try:
|
|
current = plistlib.loads(current_payload)
|
|
except plistlib.InvalidFileException as exc:
|
|
raise MissionCoreLaunchAgentError("current Mission Core launch agent is invalid") from exc
|
|
if not isinstance(current, dict) or current.get("Label") != MISSION_CORE_LAUNCH_AGENT_LABEL:
|
|
raise MissionCoreLaunchAgentError("current launch agent identity changed")
|
|
current_arguments = _program_arguments(current)
|
|
current_working_directory = current.get("WorkingDirectory")
|
|
if not isinstance(current_working_directory, str):
|
|
raise MissionCoreLaunchAgentError("current launch agent working directory is invalid")
|
|
if expected_current_repository is None and current_working_directory != str(repository):
|
|
raise MissionCoreLaunchAgentError("current launch agent targets another repository")
|
|
if (
|
|
expected_current_repository is not None
|
|
and current_working_directory != str(expected_current_repository)
|
|
):
|
|
raise MissionCoreLaunchAgentError(
|
|
"current launch agent does not target the expected current repository"
|
|
)
|
|
environment_document = current.get("EnvironmentVariables")
|
|
if not isinstance(environment_document, dict) or any(
|
|
not isinstance(key, str) or not isinstance(value, str)
|
|
for key, value in environment_document.items()
|
|
):
|
|
raise MissionCoreLaunchAgentError("current launch agent environment is invalid")
|
|
environment = cast(dict[str, str], environment_document)
|
|
# A LaunchAgent started directly from this repository's venv is denied
|
|
# access to ``.venv/pyvenv.cfg`` by macOS privacy controls because the
|
|
# checkout is below Downloads. The Homebrew uv launcher is already the
|
|
# accepted local execution boundary. ``--no-sync`` keeps launch startup
|
|
# deterministic and prevents dependency mutation during recovery.
|
|
uv_entrypoint = Path(current_arguments[0])
|
|
if (
|
|
not uv_entrypoint.is_absolute()
|
|
or uv_entrypoint.name != "uv"
|
|
or not uv_entrypoint.exists()
|
|
):
|
|
raise MissionCoreLaunchAgentError("Mission Core uv entrypoint is unavailable")
|
|
current_repository = Path(current_working_directory)
|
|
repository_migration = current_repository != repository
|
|
preserved_data_directory = (
|
|
_preserved_migration_data_directory(
|
|
current_repository=current_repository,
|
|
environment=environment,
|
|
)
|
|
if repository_migration
|
|
else None
|
|
)
|
|
desired_environment = dict(environment)
|
|
desired_environment["MISSIONCORE_SERVICE_WATCHDOG"] = "1"
|
|
if preserved_data_directory is not None:
|
|
desired_environment["MISSIONCORE_DATA_DIR"] = str(preserved_data_directory)
|
|
if enable_local_observatory_worker:
|
|
data_directory = _local_observatory_data_directory(
|
|
repository=repository,
|
|
environment=desired_environment,
|
|
)
|
|
artifact_store = _private_local_worker_directory(
|
|
data_directory / "observatory-artifact-store",
|
|
"local Observatory artifact store",
|
|
)
|
|
source_cas = _private_local_worker_directory(
|
|
data_directory / "observatory-worker-source-cas",
|
|
"local Observatory source CAS",
|
|
)
|
|
result_staging = _private_local_worker_directory(
|
|
data_directory / "observatory-worker-result-staging",
|
|
"local Observatory result staging",
|
|
)
|
|
desired_environment["MISSIONCORE_DATA_DIR"] = str(data_directory)
|
|
desired_environment[ARTIFACT_STORE_ROOT_ENV] = str(artifact_store)
|
|
desired_environment[OBSERVATORY_SOURCE_CAS_ROOT_ENV] = str(source_cas)
|
|
desired_environment[OBSERVATORY_RESULT_STAGING_ROOT_ENV] = str(result_staging)
|
|
desired_environment[OBSERVATORY_LOCAL_WORKER_ENABLED_ENV] = "1"
|
|
log_path = repository / ".runtime/mission-core/k1link-serve-launchd.log"
|
|
desired_program_arguments = (
|
|
str(uv_entrypoint),
|
|
"run",
|
|
"--no-sync",
|
|
"k1link",
|
|
"serve",
|
|
)
|
|
desired: dict[str, object] = {
|
|
"Label": MISSION_CORE_LAUNCH_AGENT_LABEL,
|
|
"ProgramArguments": list(desired_program_arguments),
|
|
"WorkingDirectory": str(repository),
|
|
"EnvironmentVariables": desired_environment,
|
|
"KeepAlive": True,
|
|
"RunAtLoad": True,
|
|
"AbandonProcessGroup": False,
|
|
"ProcessType": "Background",
|
|
"ThrottleInterval": 5,
|
|
"ExitTimeOut": 20,
|
|
"StandardOutPath": str(log_path),
|
|
"StandardErrorPath": str(log_path),
|
|
}
|
|
desired_payload = plistlib.dumps(desired, fmt=plistlib.FMT_XML, sort_keys=True)
|
|
return MissionCoreLaunchAgentPlan(
|
|
agent_path=path,
|
|
current_sha256=_sha256(current_payload),
|
|
desired_sha256=_sha256(desired_payload),
|
|
current_working_directory=current_repository,
|
|
desired_working_directory=repository,
|
|
preserved_data_directory=preserved_data_directory,
|
|
current_program_arguments=current_arguments,
|
|
desired_program_arguments=desired_program_arguments,
|
|
local_observatory_worker_enabled=enable_local_observatory_worker,
|
|
desired_payload=desired_payload,
|
|
)
|
|
|
|
|
|
def _local_observatory_data_directory(
|
|
*,
|
|
repository: Path,
|
|
environment: dict[str, str],
|
|
) -> Path:
|
|
configured = environment.get("MISSIONCORE_DATA_DIR", "").strip()
|
|
candidate = Path(configured) if configured else repository / ".runtime" / "mission-core"
|
|
return _private_local_worker_directory(
|
|
candidate,
|
|
"Mission Core data directory",
|
|
)
|
|
|
|
|
|
def _private_local_worker_directory(path: Path, label: str) -> Path:
|
|
if not path.is_absolute():
|
|
raise MissionCoreLaunchAgentError(f"{label} is not an absolute directory")
|
|
try:
|
|
metadata = path.lstat()
|
|
resolved = path.resolve(strict=True)
|
|
except OSError as exc:
|
|
raise MissionCoreLaunchAgentError(f"{label} is unavailable") from exc
|
|
if (
|
|
resolved != path
|
|
or not stat.S_ISDIR(metadata.st_mode)
|
|
or stat.S_IMODE(metadata.st_mode) != 0o700
|
|
or metadata.st_uid != os.getuid()
|
|
):
|
|
raise MissionCoreLaunchAgentError(
|
|
f"{label} is not a private canonical directory"
|
|
)
|
|
return resolved
|
|
|
|
|
|
def _program_arguments(document: dict[str, object]) -> tuple[str, ...]:
|
|
value = document.get("ProgramArguments")
|
|
if not isinstance(value, list) or not value or any(not isinstance(item, str) for item in value):
|
|
raise MissionCoreLaunchAgentError("launch agent program arguments are invalid")
|
|
return tuple(value)
|
|
|
|
|
|
def _preserved_migration_data_directory(
|
|
*,
|
|
current_repository: Path,
|
|
environment: dict[str, str],
|
|
) -> Path:
|
|
configured = environment.get("MISSIONCORE_DATA_DIR", "")
|
|
if configured.strip():
|
|
if configured != configured.strip():
|
|
raise MissionCoreLaunchAgentError(
|
|
"current Mission Core data directory is not a canonical absolute path"
|
|
)
|
|
candidate = Path(configured)
|
|
else:
|
|
candidate = current_repository / ".runtime" / "mission-core"
|
|
if not candidate.is_absolute():
|
|
raise MissionCoreLaunchAgentError(
|
|
"current Mission Core data directory is not a canonical absolute path"
|
|
)
|
|
try:
|
|
resolved = candidate.resolve(strict=True)
|
|
metadata = candidate.lstat()
|
|
except OSError as exc:
|
|
raise MissionCoreLaunchAgentError(
|
|
"current Mission Core data directory is unavailable"
|
|
) from exc
|
|
if resolved != candidate or (
|
|
not stat.S_ISDIR(metadata.st_mode)
|
|
or stat.S_IMODE(metadata.st_mode) != 0o700
|
|
or metadata.st_uid != os.getuid()
|
|
):
|
|
raise MissionCoreLaunchAgentError(
|
|
"current Mission Core data directory is not a private canonical directory"
|
|
)
|
|
return resolved
|
|
|
|
|
|
def _read_private_regular_file(path: Path) -> bytes:
|
|
if path.is_symlink() or not path.is_file():
|
|
raise MissionCoreLaunchAgentError("Mission Core launch agent is unavailable")
|
|
return path.read_bytes()
|
|
|
|
|
|
def _sha256(payload: bytes) -> str:
|
|
return hashlib.sha256(payload).hexdigest()
|