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
27 changes: 27 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import mimetypes
import os
import re
import shlex
import time
from contextlib import asynccontextmanager
from datetime import timezone
Expand Down Expand Up @@ -41,6 +42,7 @@
)
from app.http_caching import strip_range
from app.memory_observability import record_memory_checkpoint
from app.storage_io import atomic_write
from app import activity, models
# providers and push are on the agent's write surface; deferred into
# lifespan with try/except so a SyntaxError in either doesn't prevent
Expand All @@ -60,6 +62,23 @@
_BOOT_ID = os.environ.get("MOBIUS_BOOT_ID") or f"{os.getpid()}-{time.time_ns()}"


def _install_pm_commit_launcher(source: Path, target: Path) -> bool:
"""Point the stable command path at the helper in the served checkout."""
if not source.is_file():
raise FileNotFoundError(source)
launcher = (
f"#!/bin/sh\nexec {shlex.quote(str(source))} \"$@\"\n"
).encode()
try:
if target.read_bytes() == launcher and target.stat().st_mode & 0o111:
return False
except FileNotFoundError:
pass
atomic_write(target, launcher)
target.chmod(0o755)
return True


def _init_db():
"""Run migrations and create tables, retrying on transient failures."""
for attempt in range(10):
Expand Down Expand Up @@ -142,6 +161,14 @@ async def lifespan(app):
import asyncio as _asyncio
import logging as _logging
_log = _logging.getLogger(__name__)
try:
_install_pm_commit_launcher(
Path(__file__).resolve().parents[1] / "scripts" / "pm-commit",
Path(get_settings().data_dir) / ".pm-commit",
)
except Exception as exc:
# Agent commit tooling must never make the owner-facing service unbootable.
_log.error("pm-commit launcher refresh failed: %s", exc, exc_info=True)
record_memory_checkpoint("lifespan_start")
# Wrapped: providers.py is on the agent's write surface. A broken
# providers.py shouldn't take down the server — log and skip the
Expand Down
6 changes: 3 additions & 3 deletions backend/scripts/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1268,9 +1268,9 @@ su -s /bin/sh mobius -c '
git config --global credential.helper "!gh auth git-credential"
' 2>/dev/null || true

# Only copy the pm-commit helper if missing or if the image version
# differs from the on-disk copy. Blindly overwriting on every boot wipes
# any instance-local edits the agent or operator may have made.
# Seed a usable image-floor helper before the app starts. FastAPI lifespan
# replaces this target with a launcher to the served platform copy, so later
# source updates remain authoritative without another container rebuild.
if [ ! -f /data/.pm-commit ] || ! cmp -s /app/scripts/pm-commit /data/.pm-commit; then
cp /app/scripts/pm-commit /data/.pm-commit
chmod +x /data/.pm-commit
Expand Down
37 changes: 37 additions & 0 deletions backend/tests/test_pm_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
from pathlib import Path
import subprocess

import pytest

from app.main import _install_pm_commit_launcher


SCRIPT = Path(__file__).parents[1] / "scripts" / "pm-commit"

Expand Down Expand Up @@ -84,3 +88,36 @@ def test_broad_snapshot_mode_does_not_exist(tmp_path):

assert result.returncode == 2
assert git(work, "status", "--short") == "M owned.txt"


def test_launcher_follows_live_helper_updates_without_reinstall(tmp_path):
source = tmp_path / "platform" / "pm-commit"
target = tmp_path / "data" / ".pm-commit"
source.parent.mkdir()
target.parent.mkdir()
target.write_text("stale image copy\n")

source.write_text("#!/bin/sh\nprintf 'first:%s\\n' \"$1\"\n")
source.chmod(0o755)
assert _install_pm_commit_launcher(source, target)
assert subprocess.run(
[str(target), "one argument"], check=True, capture_output=True, text=True,
).stdout == "first:one argument\n"

source.write_text("#!/bin/sh\nprintf 'second:%s\\n' \"$1\"\n")
assert subprocess.run(
[str(target), "same launcher"], check=True, capture_output=True, text=True,
).stdout == "second:same launcher\n"
assert not _install_pm_commit_launcher(source, target)


def test_launcher_preserves_seed_when_live_helper_is_missing(tmp_path):
source = tmp_path / "platform" / "pm-commit"
target = tmp_path / "data" / ".pm-commit"
target.parent.mkdir()
target.write_text("usable image copy\n")

with pytest.raises(FileNotFoundError):
_install_pm_commit_launcher(source, target)

assert target.read_text() == "usable image copy\n"