Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/engineering-platform-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ jobs:
measured_modules = {Path(path).resolve() for path in files}
unmeasured = [str(path) for path in production_modules if path not in measured_modules]
aggregate = report["totals"]["percent_covered"]
if len(production_modules) != 107:
failures.append(f"production module inventory changed: {len(production_modules)} (expected 107)")
if len(production_modules) != 108:
failures.append(f"production module inventory changed: {len(production_modules)} (expected 108)")
if unmeasured:
failures.append("unmeasured production modules: " + ", ".join(unmeasured))
if aggregate < 80.00:
Expand Down
8 changes: 8 additions & 0 deletions docs/engineering/STANDALONE_EP_SERVER_FOUNDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ engineering-platform-server init --data-root /secure/ep-server
engineering-platform-server start --data-root /secure/ep-server
engineering-platform-server health --data-root /secure/ep-server
engineering-platform-server stop --data-root /secure/ep-server
engineering-platform-server service-install --data-root /secure/ep-server
engineering-platform-server service-uninstall --data-root /secure/ep-server
engineering-platform-server relay-install --data-root /secure/ep-server
```

Expand All @@ -35,6 +37,12 @@ projection at `GET /v1/operations/projects`. See
[Standalone runtime surfaces](STANDALONE_RUNTIME_SURFACES.md) for the complete
installed-artifact authority and role contract.

`service-install` installs exactly one per-user macOS LaunchAgent for the
foreground `serve` entrypoint. Its fixed arguments use the installed Python
runtime and initialized data root; it never invokes the self-daemonizing
`start` helper or accepts arbitrary commands. `service-uninstall` removes only
that owned LaunchAgent and preserves CENTRAL and installation identity.

The existing Execution Host remains unchanged and retains its current execution
authority. The server does not read a source checkout, `.engineering`, or any
DJConnect state at runtime.
Expand Down
7 changes: 6 additions & 1 deletion src/engineering_platform/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from . import project_topology
from . import submission_service
from . import server_relay
from . import server_service
from . import storage
from . import managed_codex_runtime
from . import provider_readiness
Expand Down Expand Up @@ -3112,7 +3113,7 @@ def health(data_root: Path) -> dict[str, object]:

def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="engineering-platform-server", description="Manage the standalone Engineering Platform Server foundation")
parser.add_argument("command", choices=("init", "start", "serve", "stop", "status", "health", "relay-install", "relay-uninstall", "pairing-create", "agent-status", "agent-revoke", "agent-reset", "topology", "submission-diagnose", "bootstrap-topology", "register-topology", "provision-declaration", "issue-consumer-credential", "bind-repository", "rebind-repository", "unbind-repository", "resolve-repository", "register-producer-binding", "list-producer-bindings", "deactivate-producer-binding"))
parser.add_argument("command", choices=("init", "start", "serve", "stop", "status", "health", "service-install", "service-uninstall", "relay-install", "relay-uninstall", "pairing-create", "agent-status", "agent-revoke", "agent-reset", "topology", "submission-diagnose", "bootstrap-topology", "register-topology", "provision-declaration", "issue-consumer-credential", "bind-repository", "rebind-repository", "unbind-repository", "resolve-repository", "register-producer-binding", "list-producer-bindings", "deactivate-producer-binding"))
parser.add_argument("--data-root", type=Path, default=default_data_root())
parser.add_argument("--bind-host", default="127.0.0.1")
parser.add_argument("--bind-port", type=int, default=8765)
Expand Down Expand Up @@ -3150,6 +3151,10 @@ def main(argv: list[str] | None = None) -> int:
elif args.command == "stop": result = stop(args.data_root)
elif args.command == "status": result = status(args.data_root)
elif args.command == "health": result = health(args.data_root)
elif args.command == "service-install":
initialize(args.data_root)
result = {"result": "INSTALLED", **server_service.install(args.data_root)}
elif args.command == "service-uninstall": result = {"result": "UNINSTALLED", **server_service.uninstall(args.data_root)}
elif args.command == "relay-install":
initialize(args.data_root)
result = {"result": "INSTALLED", **server_relay.install(args.data_root)}
Expand Down
110 changes: 110 additions & 0 deletions src/engineering_platform/server_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""macOS LaunchAgent lifecycle for the installed EP Server.

The Server remains the lifecycle and CENTRAL authority. This module only
installs one fixed per-user supervisor for its foreground ``serve`` command;
it never starts a second daemon or accepts arbitrary commands.
"""
from __future__ import annotations

from dataclasses import dataclass
import os
from pathlib import Path
import plistlib
import platform
import subprocess
import sys
from typing import Callable, Mapping, Sequence


LABEL = "com.engineeringplatform.server"
DEFAULT_PATH = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"


class ServerServiceError(ValueError):
"""Raised when the bounded EP Server service cannot be managed safely."""


@dataclass(frozen=True)
class ServicePaths:
data_root: Path
launch_agents_dir: Path

@property
def plist_path(self) -> Path:
return self.launch_agents_dir / f"{LABEL}.plist"


def default_paths(data_root: Path, home: Path | None = None) -> ServicePaths:
return ServicePaths(data_root.resolve(), (home or Path.home()).expanduser() / "Library" / "LaunchAgents")


def _domain() -> str:
return f"gui/{os.getuid()}"


Runner = Callable[[Sequence[str]], subprocess.CompletedProcess[str]]


def _launchctl(arguments: Sequence[str], runner: Runner | None = None) -> subprocess.CompletedProcess[str]:
if platform.system() != "Darwin":
raise ServerServiceError("EP Server LaunchAgent lifecycle is supported only on macOS.")
return (runner or (lambda command: subprocess.run(command, capture_output=True, text=True, check=False)))(["launchctl", *arguments])


def _installed_interpreter(candidate: str | Path | None = None) -> Path:
executable = Path(candidate or sys.executable).expanduser().resolve()
if not executable.is_file() or not os.access(executable, os.X_OK):
raise ServerServiceError("The EP Server interpreter is not an executable installed runtime.")
return executable


def plist_payload(paths: ServicePaths, interpreter: Path) -> dict[str, object]:
return {
"Label": LABEL,
"ProgramArguments": [str(interpreter), "-m", "engineering_platform.server", "serve", "--data-root", str(paths.data_root)],
"WorkingDirectory": str(paths.data_root),
"RunAtLoad": True,
"KeepAlive": {"SuccessfulExit": False},
"ProcessType": "Background",
"EnvironmentVariables": {
"PATH": DEFAULT_PATH,
"PYTHONNOUSERSITE": "1",
"PYTHONSAFEPATH": "1",
"EP_SERVER_DATA_ROOT": str(paths.data_root),
},
"StandardOutPath": "/dev/null",
"StandardErrorPath": "/dev/null",
}


def write_plist(paths: ServicePaths, interpreter: Path) -> Path:
paths.launch_agents_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
content = plistlib.dumps(plist_payload(paths, interpreter), fmt=plistlib.FMT_XML, sort_keys=True)
temporary = paths.plist_path.with_suffix(".plist.tmp")
temporary.write_bytes(content)
temporary.chmod(0o644)
os.replace(temporary, paths.plist_path)
paths.plist_path.chmod(0o644)
return paths.plist_path


def install(data_root: Path, *, interpreter: str | Path | None = None, home: Path | None = None,
runner: Runner | None = None) -> Mapping[str, str]:
paths = default_paths(data_root, home)
if not paths.data_root.is_dir() or not (paths.data_root / "server.json").is_file():
raise ServerServiceError("EP Server must be initialized before its LaunchAgent is installed.")
plist = write_plist(paths, _installed_interpreter(interpreter))
result = _launchctl(("bootstrap", _domain(), str(plist)), runner)
if result.returncode and "service already loaded" not in (result.stderr or "").lower():
raise ServerServiceError("Unable to bootstrap EP Server LaunchAgent.")
return {"state": "installed", "label": LABEL, "plist": str(plist), "data_root": str(paths.data_root)}


def uninstall(data_root: Path, *, home: Path | None = None, runner: Runner | None = None) -> Mapping[str, str]:
paths = default_paths(data_root, home)
if paths.plist_path.exists():
result = _launchctl(("bootout", _domain(), str(paths.plist_path)), runner)
if result.returncode and not any(marker in (result.stderr or "").lower() for marker in ("could not find service", "no such process", "not found")):
raise ServerServiceError("Unable to unload EP Server LaunchAgent.")
paths.plist_path.unlink(missing_ok=True)
return {"state": "uninstalled", "label": LABEL, "plist": str(paths.plist_path), "data_root": str(paths.data_root)}
10 changes: 10 additions & 0 deletions tests/engineering/test_server_foundation.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,16 @@ def test_server_cli_installs_relay_through_server_owned_lifecycle(self) -> None:
self.assertEqual(result["result"], "INSTALLED")
self.assertEqual(result["component"], "dashboard_relay")

def test_server_cli_installs_server_launchagent_through_server_owned_lifecycle(self) -> None:
with patch("engineering_platform.server.server_service.install", return_value={
"state": "installed", "label": "com.engineeringplatform.server", "plist": "/Library/LaunchAgents/com.engineeringplatform.server.plist", "data_root": "/installation",
}) as install, redirect_stdout(io.StringIO()) as output:
self.assertEqual(server.main(["service-install", "--data-root", str(self.root)]), 0)
install.assert_called_once_with(self.root)
result = json.loads(output.getvalue())
self.assertEqual(result["result"], "INSTALLED")
self.assertEqual(result["label"], "com.engineeringplatform.server")

def test_live_file_inbox_with_quarantine_is_degraded_without_execution_state(self) -> None:
server.initialize(self.root)
inbox = self.root / server.FILE_INBOX_DIRECTORY
Expand Down
70 changes: 70 additions & 0 deletions tests/engineering/test_server_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from __future__ import annotations

from pathlib import Path
from tempfile import TemporaryDirectory
import subprocess
import sys
import unittest
from unittest.mock import patch

from engineering_platform import server_service


class ServerServiceTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = TemporaryDirectory()
self.root = Path(self.temporary.name) / "instance"
self.home = Path(self.temporary.name) / "home"
self.root.mkdir()
(self.root / "server.json").write_text("{}", encoding="utf-8")

def tearDown(self) -> None:
self.temporary.cleanup()

@staticmethod
def runner(arguments: list[str]) -> subprocess.CompletedProcess[str]:
return subprocess.CompletedProcess(arguments, 0, "", "")

def test_payload_runs_foreground_server_from_absolute_interpreter(self) -> None:
paths = server_service.default_paths(self.root, self.home)
payload = server_service.plist_payload(paths, Path("/runtime/bin/python"))
self.assertEqual(payload["Label"], server_service.LABEL)
self.assertEqual(payload["ProgramArguments"], ["/runtime/bin/python", "-m", "engineering_platform.server", "serve", "--data-root", str(self.root.resolve())])
self.assertEqual(payload["WorkingDirectory"], str(self.root.resolve()))
self.assertNotIn("PYTHONPATH", payload["EnvironmentVariables"])

def test_install_is_idempotent_and_writes_only_owned_plist(self) -> None:
with patch("engineering_platform.server_service.platform.system", return_value="Darwin"):
result = server_service.install(self.root, interpreter=Path(sys.executable), home=self.home, runner=self.runner)
self.assertEqual(result["label"], server_service.LABEL)
self.assertTrue((self.home / "Library" / "LaunchAgents" / f"{server_service.LABEL}.plist").is_file())

def test_uninitialized_data_root_fails_closed(self) -> None:
with self.assertRaisesRegex(server_service.ServerServiceError, "initialized"):
server_service.install(self.root.parent / "missing", interpreter=Path(__file__).resolve(), home=self.home, runner=self.runner)

def test_uninstall_boots_out_only_the_owned_agent(self) -> None:
paths = server_service.default_paths(self.root, self.home)
paths.launch_agents_dir.mkdir(parents=True)
paths.plist_path.write_text("owned", encoding="utf-8")
calls: list[list[str]] = []

def runner(arguments: list[str]) -> subprocess.CompletedProcess[str]:
calls.append(arguments)
return subprocess.CompletedProcess(arguments, 0, "", "")

with patch("engineering_platform.server_service.platform.system", return_value="Darwin"):
result = server_service.uninstall(self.root, home=self.home, runner=runner)
self.assertEqual(result["state"], "uninstalled")
self.assertFalse(paths.plist_path.exists())
self.assertEqual(calls, [["launchctl", "bootout", f"gui/{server_service.os.getuid()}", str(paths.plist_path)]])

def test_launchagent_failures_and_non_macos_fail_closed(self) -> None:
with patch("engineering_platform.server_service.platform.system", return_value="Darwin"):
def failed(arguments: list[str]) -> subprocess.CompletedProcess[str]:
return subprocess.CompletedProcess(arguments, 1, "", "permission denied")
with self.assertRaisesRegex(server_service.ServerServiceError, "Unable to bootstrap"):
server_service.install(self.root, interpreter=Path(sys.executable), home=self.home, runner=failed)
with patch("engineering_platform.server_service.platform.system", return_value="Linux"):
with self.assertRaisesRegex(server_service.ServerServiceError, "macOS"):
server_service.uninstall(self.root, home=self.home, runner=self.runner)
Loading