feat(local-service): allow exact worktree migration
This commit is contained in:
@@ -3,10 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import plistlib
|
||||
import stat
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
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"
|
||||
@@ -21,6 +23,9 @@ 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, ...]
|
||||
desired_payload: bytes
|
||||
@@ -32,9 +37,24 @@ class MissionCoreLaunchAgentPlan:
|
||||
"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,
|
||||
@@ -49,8 +69,14 @@ def plan_mission_core_launch_agent(
|
||||
*,
|
||||
repository_root: Path,
|
||||
agent_path: Path,
|
||||
expected_current_repository_root: Path | None = None,
|
||||
) -> 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:
|
||||
@@ -61,14 +87,24 @@ def plan_mission_core_launch_agent(
|
||||
raise MissionCoreLaunchAgentError("current launch agent identity changed")
|
||||
current_arguments = _program_arguments(current)
|
||||
current_working_directory = current.get("WorkingDirectory")
|
||||
if current_working_directory != str(repository):
|
||||
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")
|
||||
environment = current.get("EnvironmentVariables")
|
||||
if not isinstance(environment, dict) or any(
|
||||
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.items()
|
||||
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
|
||||
@@ -81,18 +117,31 @@ def plan_mission_core_launch_agent(
|
||||
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)
|
||||
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": [
|
||||
str(uv_entrypoint),
|
||||
"run",
|
||||
"--no-sync",
|
||||
"k1link",
|
||||
"serve",
|
||||
],
|
||||
"ProgramArguments": list(desired_program_arguments),
|
||||
"WorkingDirectory": str(repository),
|
||||
"EnvironmentVariables": desired_environment,
|
||||
"KeepAlive": True,
|
||||
@@ -109,8 +158,11 @@ def plan_mission_core_launch_agent(
|
||||
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=tuple(desired["ProgramArguments"]),
|
||||
desired_program_arguments=desired_program_arguments,
|
||||
desired_payload=desired_payload,
|
||||
)
|
||||
|
||||
@@ -122,6 +174,42 @@ def _program_arguments(document: dict[str, object]) -> tuple[str, ...]:
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user