fix(service): supervise canonical Mission Core lifecycle
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"""Versioned launchd declaration for the canonical local Mission Core service."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import plistlib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
MISSION_CORE_LAUNCH_AGENT_LABEL: Final = "com.nodedc.mission-core.local"
|
||||
MISSION_CORE_LAUNCH_AGENT_SCHEMA: Final = "missioncore.local-launch-agent-plan/v1"
|
||||
|
||||
|
||||
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_program_arguments: tuple[str, ...]
|
||||
desired_program_arguments: tuple[str, ...]
|
||||
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_program_arguments": list(self.current_program_arguments),
|
||||
"desired_program_arguments": list(self.desired_program_arguments),
|
||||
"changes": {
|
||||
"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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def plan_mission_core_launch_agent(
|
||||
*,
|
||||
repository_root: Path,
|
||||
agent_path: Path,
|
||||
) -> MissionCoreLaunchAgentPlan:
|
||||
repository = repository_root.expanduser().resolve(strict=True)
|
||||
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 current_working_directory != str(repository):
|
||||
raise MissionCoreLaunchAgentError("current launch agent targets another repository")
|
||||
environment = current.get("EnvironmentVariables")
|
||||
if not isinstance(environment, dict) or any(
|
||||
not isinstance(key, str) or not isinstance(value, str)
|
||||
for key, value in environment.items()
|
||||
):
|
||||
raise MissionCoreLaunchAgentError("current launch agent environment is invalid")
|
||||
# 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")
|
||||
desired_environment = dict(environment)
|
||||
desired_environment["MISSIONCORE_SERVICE_WATCHDOG"] = "1"
|
||||
log_path = repository / ".runtime/mission-core/k1link-serve-launchd.log"
|
||||
desired: dict[str, object] = {
|
||||
"Label": MISSION_CORE_LAUNCH_AGENT_LABEL,
|
||||
"ProgramArguments": [
|
||||
str(uv_entrypoint),
|
||||
"run",
|
||||
"--no-sync",
|
||||
"k1link",
|
||||
"serve",
|
||||
],
|
||||
"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_program_arguments=current_arguments,
|
||||
desired_program_arguments=tuple(desired["ProgramArguments"]),
|
||||
desired_payload=desired_payload,
|
||||
)
|
||||
|
||||
|
||||
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 _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()
|
||||
Reference in New Issue
Block a user