Files
hermes-agent/hermes_cli/update_contract.py
T
Teknium 4860978115 feat(update): image/package-managed installs refuse in-place updates through one shared gate (#91277 Phase 3)
Every surface that can start an in-place mutation — hermes update
(apply), update --check, and the dashboard's update endpoint — now
routes through evaluate_update_admission(): the baked image-provenance
marker first (authoritative; a bind-mounted checkout inside a container
looks like git to the heuristics while the filesystem is an immutable
image), then the pre-existing docker/nix/apt heuristics verbatim.

A refusal prints the real update command for the deployment kind,
records a 'refused' receipt (fleet tooling sees 'not updatable in
place, use <cmd>' instead of a silent non-update), and exits 2 on CLI
surfaces — distinct from exit-1 errors. The dashboard response keeps
the per-kind error codes its UI already keys on. collect_runtime
inventory()'s updatable_in_place also honors the marker, so --plan and
receipts report image-managed truthfully even with a bind-mounted
checkout.

Live E2E (real hermes update subprocesses, real marker file): apply and
--check both refuse exit-2 with docker-pull guidance, receipts land as
refused/image-marker, an in-place corrupted marker still refuses
(fail-closed), removing the marker admits the git checkout.
2026-08-26 11:41:04 -07:00

139 lines
5.4 KiB
Python

"""Image-managed install refusal contract (#91277 Phase 3).
One shared admission gate for every surface that can start an in-place
``hermes update`` mutation (CLI apply, CLI --check, dashboard update
endpoint). The decision layers:
1. **Baked provenance marker** (``/etc/hermes/image-provenance.json``,
written by the image build — see :mod:`hermes_cli.image_provenance`):
authoritative ground truth that this filesystem came from an immutable
image. Fail-closed: a present-but-malformed marker still refuses.
2. **Filesystem heuristics** (``detect_install_method()``): the pre-existing
docker/nix/apt detection, kept as the fallback for images built before
the marker existed and for package-managed installs that have no image
marker at all.
A refusal prints the real update command for the deployment kind, records a
``refused`` receipt (so fleet tooling sees "this install cannot self-update,
use <command>" instead of a silent non-update), and exits 2 on CLI surfaces.
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class UpdateRefusal:
"""Why an in-place update is refused, and what to run instead."""
code: str # image-marker | image-marker-invalid | docker | nix | apt
message: str # full user-facing text (multi-line ok)
update_command: str # the one-line remediation command
def evaluate_update_admission(project_root: Path) -> Optional[UpdateRefusal]:
"""Return an :class:`UpdateRefusal` when in-place update must not run.
``None`` means the install is eligible for in-place update (git checkout
or unknown-but-mutable). Never raises; on any internal error it falls
back to the heuristic layer only.
"""
# Layer 1: baked provenance marker — authoritative when present.
try:
from hermes_cli.image_provenance import read_image_provenance
provenance = read_image_provenance()
if provenance is not None:
from hermes_cli.config import (
format_docker_update_message,
recommended_update_command_for_method,
)
if not provenance.valid:
# Present but malformed: still image-managed — an integrity
# defect is never permission to mutate the image in place.
command = recommended_update_command_for_method("docker")
return UpdateRefusal(
code="image-marker-invalid",
message=(
"✗ This install is image-managed, but its provenance "
f"marker is invalid ({provenance.error}).\n"
" In-place update is disabled. Update by pulling a "
f"new image:\n {command}"
),
update_command=command,
)
manager = provenance.manager
if manager == "docker":
return UpdateRefusal(
code="image-marker",
message=format_docker_update_message(),
update_command=recommended_update_command_for_method("docker"),
)
command = recommended_update_command_for_method(manager)
return UpdateRefusal(
code="image-marker",
message=command,
update_command=command,
)
except Exception as exc:
logger.debug("Image provenance check failed (using heuristics): %s", exc)
# Layer 2: pre-existing filesystem heuristics, verbatim semantics.
try:
from hermes_cli.config import (
detect_install_method,
format_docker_update_message,
is_nix_install_method,
recommended_update_command_for_method,
)
method = detect_install_method(project_root)
if method == "docker":
return UpdateRefusal(
code="docker",
message=format_docker_update_message(),
update_command=recommended_update_command_for_method("docker"),
)
if is_nix_install_method(method) or method == "apt":
command = recommended_update_command_for_method(method)
return UpdateRefusal(
code=method if method == "apt" else "nix",
message=command,
update_command=command,
)
except Exception as exc:
logger.debug("Install-method admission check failed: %s", exc)
return None
def record_refusal_receipt(refusal: UpdateRefusal) -> None:
"""Write a minimal ``refused`` receipt for a blocked update attempt.
Gives fleet tooling a durable record that an update was ATTEMPTED and
refused ("not updatable in place, use <command>") instead of a silent
nothing. Best-effort; never raises.
"""
try:
from hermes_cli.update_receipt import (
begin_update_receipt,
finalize_update_receipt,
record_step,
)
begin_update_receipt()
record_step(
"admission",
False,
f"not updatable in place ({refusal.code}); use: {refusal.update_command}",
)
finalize_update_receipt("refused", stop_reason=refusal.code)
except Exception as exc:
logger.debug("Could not record refusal receipt: %s", exc)