feat(local-service): allow exact worktree migration

This commit is contained in:
DCCONSTRUCTIONS
2026-08-30 23:23:36 +03:00
parent 2a5763d3fb
commit 177be8869f
5 changed files with 421 additions and 25 deletions
@@ -72,6 +72,49 @@ waits until `bootout` has fully removed the old label, bootstraps the new
declaration and accepts only the exact Mission Core health document. Any
failure restores the previous plist and repeats the same health acceptance.
### Explicit worktree migration
A plan from a different checkout is rejected by default. To migrate the
canonical service between these two exact worktrees, authorize the installed
predecessor explicitly on both plan and apply:
```bash
cd /Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_MISSION_CORE_m5_observatory
uv run python scripts/manage_mission_core_launch_agent.py plan \
--repository-root /Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_MISSION_CORE_m5_observatory \
--expected-current-repository-root /Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_MISSION_CORE
```
The plan is admissible only when `current_working_directory` is exactly
`/Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_MISSION_CORE`,
`desired_working_directory` is exactly
`/Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_MISSION_CORE_m5_observatory`
and `changes.repository_migration` is `true`. The same plan must report
`preserved_data_directory` and `changes.preserved_data_directory` as exactly
`/Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_MISSION_CORE/.runtime/mission-core`,
with `changes.data_directory_preserved=true`. The desired plist then carries
that exact path as `MISSIONCORE_DATA_DIR`; an already configured nonblank,
private canonical `MISSIONCORE_DATA_DIR` is retained instead. Copy the two
exact hashes from that same output:
```bash
uv run python scripts/manage_mission_core_launch_agent.py apply \
--repository-root /Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_MISSION_CORE_m5_observatory \
--expected-current-repository-root /Users/dcconstructions/Downloads/mnt/NODEDC/NODEDC_MISSION_CORE \
--expected-current-sha256 <current-sha256> \
--expected-desired-sha256 <desired-sha256>
```
The migration option is not a general cross-repository override. The planner
resolves it to one exact path and rejects any installed `WorkingDirectory`
that differs. Apply recomputes the same path-bound plan and still requires both
plist hashes, so a changed predecessor or candidate must be planned again.
Do not symlink either worktree's `.runtime/mission-core` to the other. The
checkout-local singleton lock requires a real private directory; durable data
continuity is expressed only by the path-bound `MISSIONCORE_DATA_DIR` in this
migration plan.
Read-only status:
```bash
@@ -28,6 +28,14 @@ def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("action", choices=("plan", "apply", "status"))
parser.add_argument("--repository-root", type=Path, required=True)
parser.add_argument(
"--expected-current-repository-root",
type=Path,
help=(
"authorize migration only from this exact installed WorkingDirectory; "
"cross-repository plans remain denied when omitted"
),
)
parser.add_argument(
"--agent-path",
type=Path,
@@ -43,6 +51,7 @@ def main() -> int:
plan = plan_mission_core_launch_agent(
repository_root=arguments.repository_root,
agent_path=arguments.agent_path,
expected_current_repository_root=arguments.expected_current_repository_root,
)
if arguments.action == "plan":
print(json.dumps(plan.to_dict(), indent=2, sort_keys=True))
+101 -13
View File
@@ -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")
+214 -12
View File
@@ -3,7 +3,32 @@ from __future__ import annotations
import plistlib
from pathlib import Path
from k1link.local_service_launchd import plan_mission_core_launch_agent
import pytest
from k1link.local_service_launchd import (
MissionCoreLaunchAgentError,
plan_mission_core_launch_agent,
)
def _write_agent(
*,
path: Path,
repository: Path,
uv_entrypoint: Path,
environment: dict[str, str] | None = None,
) -> None:
path.write_bytes(
plistlib.dumps(
{
"Label": "com.nodedc.mission-core.local",
"ProgramArguments": [str(uv_entrypoint), "run", "k1link", "serve"],
"WorkingDirectory": str(repository),
"EnvironmentVariables": environment or {"PATH": "/usr/bin:/bin"},
}
)
)
path.chmod(0o600)
def test_launch_agent_plan_disables_sync_and_enables_watchdog(tmp_path: Path) -> None:
@@ -13,17 +38,7 @@ def test_launch_agent_plan_disables_sync_and_enables_watchdog(tmp_path: Path) ->
uv_entrypoint.write_text("#!/bin/sh\n")
uv_entrypoint.chmod(0o700)
agent = tmp_path / "agent.plist"
agent.write_bytes(
plistlib.dumps(
{
"Label": "com.nodedc.mission-core.local",
"ProgramArguments": [str(uv_entrypoint), "run", "k1link", "serve"],
"WorkingDirectory": str(repository),
"EnvironmentVariables": {"PATH": "/usr/bin:/bin"},
}
)
)
agent.chmod(0o600)
_write_agent(path=agent, repository=repository, uv_entrypoint=uv_entrypoint)
plan = plan_mission_core_launch_agent(
repository_root=repository,
@@ -44,3 +59,190 @@ def test_launch_agent_plan_disables_sync_and_enables_watchdog(tmp_path: Path) ->
assert desired["AbandonProcessGroup"] is False
assert desired["ExitTimeOut"] == 20
assert plan.current_sha256 != plan.desired_sha256
assert plan.current_working_directory == repository
assert plan.desired_working_directory == repository
assert plan.preserved_data_directory is None
assert "MISSIONCORE_DATA_DIR" not in desired["EnvironmentVariables"]
assert plan.to_dict()["changes"]["repository_migration"] is False
assert plan.to_dict()["changes"]["data_directory_preserved"] is False
def test_launch_agent_plan_rejects_cross_repository_migration_by_default(
tmp_path: Path,
) -> None:
current_repository = tmp_path / "current-repo"
current_repository.mkdir()
desired_repository = tmp_path / "desired-repo"
desired_repository.mkdir()
uv_entrypoint = tmp_path / "uv"
uv_entrypoint.write_text("#!/bin/sh\n")
uv_entrypoint.chmod(0o700)
agent = tmp_path / "agent.plist"
_write_agent(
path=agent,
repository=current_repository,
uv_entrypoint=uv_entrypoint,
)
with pytest.raises(
MissionCoreLaunchAgentError,
match="current launch agent targets another repository",
):
plan_mission_core_launch_agent(
repository_root=desired_repository,
agent_path=agent,
)
def test_launch_agent_plan_binds_explicit_cross_repository_migration(
tmp_path: Path,
) -> None:
current_repository = tmp_path / "current-repo"
current_repository.mkdir()
desired_repository = tmp_path / "desired-repo"
desired_repository.mkdir()
other_repository = tmp_path / "other-repo"
other_repository.mkdir()
data_directory = current_repository / ".runtime" / "mission-core"
data_directory.mkdir(parents=True, mode=0o700)
uv_entrypoint = tmp_path / "uv"
uv_entrypoint.write_text("#!/bin/sh\n")
uv_entrypoint.chmod(0o700)
agent = tmp_path / "agent.plist"
_write_agent(
path=agent,
repository=current_repository,
uv_entrypoint=uv_entrypoint,
)
with pytest.raises(
MissionCoreLaunchAgentError,
match="does not target the expected current repository",
):
plan_mission_core_launch_agent(
repository_root=desired_repository,
agent_path=agent,
expected_current_repository_root=other_repository,
)
plan = plan_mission_core_launch_agent(
repository_root=desired_repository,
agent_path=agent,
expected_current_repository_root=current_repository,
)
desired = plistlib.loads(plan.desired_payload)
document = plan.to_dict()
assert plan.current_working_directory == current_repository
assert plan.desired_working_directory == desired_repository
assert plan.preserved_data_directory == data_directory
assert desired["WorkingDirectory"] == str(desired_repository)
assert desired["EnvironmentVariables"]["MISSIONCORE_DATA_DIR"] == str(data_directory)
assert document["current_working_directory"] == str(current_repository)
assert document["desired_working_directory"] == str(desired_repository)
assert document["preserved_data_directory"] == str(data_directory)
assert document["changes"]["repository_migration"] is True
assert document["changes"]["data_directory_preserved"] is True
assert document["changes"]["preserved_data_directory"] == str(data_directory)
def test_launch_agent_migration_retains_existing_private_data_directory(
tmp_path: Path,
) -> None:
current_repository = tmp_path / "current-repo"
current_repository.mkdir()
desired_repository = tmp_path / "desired-repo"
desired_repository.mkdir()
configured_data_directory = tmp_path / "canonical-data"
configured_data_directory.mkdir(mode=0o700)
uv_entrypoint = tmp_path / "uv"
uv_entrypoint.write_text("#!/bin/sh\n")
uv_entrypoint.chmod(0o700)
agent = tmp_path / "agent.plist"
_write_agent(
path=agent,
repository=current_repository,
uv_entrypoint=uv_entrypoint,
environment={
"PATH": "/usr/bin:/bin",
"MISSIONCORE_DATA_DIR": str(configured_data_directory),
},
)
plan = plan_mission_core_launch_agent(
repository_root=desired_repository,
agent_path=agent,
expected_current_repository_root=current_repository,
)
desired = plistlib.loads(plan.desired_payload)
assert plan.preserved_data_directory == configured_data_directory
assert (
desired["EnvironmentVariables"]["MISSIONCORE_DATA_DIR"]
== str(configured_data_directory)
)
def test_launch_agent_migration_rejects_symlinked_data_directory(tmp_path: Path) -> None:
current_repository = tmp_path / "current-repo"
current_repository.mkdir()
desired_repository = tmp_path / "desired-repo"
desired_repository.mkdir()
private_directory = tmp_path / "private-data"
private_directory.mkdir(mode=0o700)
symlinked_directory = tmp_path / "data-link"
symlinked_directory.symlink_to(private_directory, target_is_directory=True)
uv_entrypoint = tmp_path / "uv"
uv_entrypoint.write_text("#!/bin/sh\n")
uv_entrypoint.chmod(0o700)
agent = tmp_path / "agent.plist"
_write_agent(
path=agent,
repository=current_repository,
uv_entrypoint=uv_entrypoint,
environment={
"PATH": "/usr/bin:/bin",
"MISSIONCORE_DATA_DIR": str(symlinked_directory),
},
)
with pytest.raises(
MissionCoreLaunchAgentError,
match="data directory is not a private canonical directory",
):
plan_mission_core_launch_agent(
repository_root=desired_repository,
agent_path=agent,
expected_current_repository_root=current_repository,
)
def test_launch_agent_migration_rejects_nonprivate_default_data_directory(
tmp_path: Path,
) -> None:
current_repository = tmp_path / "current-repo"
current_repository.mkdir()
desired_repository = tmp_path / "desired-repo"
desired_repository.mkdir()
data_directory = current_repository / ".runtime" / "mission-core"
data_directory.mkdir(parents=True, mode=0o755)
data_directory.chmod(0o755)
uv_entrypoint = tmp_path / "uv"
uv_entrypoint.write_text("#!/bin/sh\n")
uv_entrypoint.chmod(0o700)
agent = tmp_path / "agent.plist"
_write_agent(
path=agent,
repository=current_repository,
uv_entrypoint=uv_entrypoint,
)
with pytest.raises(
MissionCoreLaunchAgentError,
match="data directory is not a private canonical directory",
):
plan_mission_core_launch_agent(
repository_root=desired_repository,
agent_path=agent,
expected_current_repository_root=current_repository,
)
@@ -1,7 +1,10 @@
from __future__ import annotations
import importlib.util
import json
import plistlib
import subprocess
import sys
from pathlib import Path
from typing import Any
@@ -15,6 +18,57 @@ manager = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(manager)
def test_plan_cli_authorizes_only_the_declared_current_repository(
monkeypatch: Any,
capsys: Any,
tmp_path: Path,
) -> None:
current_repository = tmp_path / "current-repo"
current_repository.mkdir()
desired_repository = tmp_path / "desired-repo"
desired_repository.mkdir()
data_directory = current_repository / ".runtime" / "mission-core"
data_directory.mkdir(parents=True, mode=0o700)
uv_entrypoint = tmp_path / "uv"
uv_entrypoint.write_text("#!/bin/sh\n")
uv_entrypoint.chmod(0o700)
agent = tmp_path / "agent.plist"
agent.write_bytes(
plistlib.dumps(
{
"Label": "com.nodedc.mission-core.local",
"ProgramArguments": [str(uv_entrypoint), "run", "k1link", "serve"],
"WorkingDirectory": str(current_repository),
"EnvironmentVariables": {"PATH": "/usr/bin:/bin"},
}
)
)
agent.chmod(0o600)
monkeypatch.setattr(
sys,
"argv",
[
str(_SCRIPT),
"plan",
"--repository-root",
str(desired_repository),
"--expected-current-repository-root",
str(current_repository),
"--agent-path",
str(agent),
],
)
assert manager.main() == 0
document = json.loads(capsys.readouterr().out)
assert document["current_working_directory"] == str(current_repository)
assert document["desired_working_directory"] == str(desired_repository)
assert document["preserved_data_directory"] == str(data_directory)
assert document["changes"]["repository_migration"] is True
assert document["changes"]["data_directory_preserved"] is True
def test_reload_waits_for_launchd_transition_before_bootstrap(
monkeypatch: Any,
tmp_path: Path,