Skip to content

Host-supervised services

anvil-serving host services inventories and operates one explicitly declared service that is owned by the resolved topology host. It is the lifecycle surface for portable, supervisor-managed host services. It does not replace anvil-serving serves or a model recipe as the authority for model deployment.

The native Windows PowerShell CLI can inspect offline configuration and topology and dispatch declared typed operations to a remote owner. On a Windows host, run the owning service runtime in Linux containers with Docker Desktop's Linux engine or inside Linux in WSL. Native Windows service hosting is unsupported, and only verbs with a declared remote execution plan operate a remote owner.

Use Host & setup for the wider host command family and Configuration for the operator-home model.

Commands and confirmation

The commands are status, discover, capabilities, logs, adopt, install, up, down, restart, enable, and disable. status, discover, capabilities, and bounded logs are read operations. Every other command first returns a plan. A mutation applies only with both --no-dry-run and --confirm; --confirm by itself still returns the preview.

anvil-serving host services capabilities
anvil-serving host services discover
anvil-serving host services status --topology operator-topology.toml
anvil-serving host services logs voice-stt --tail 100 --topology operator-topology.toml

# Preview, then apply the exact same declared action.
anvil-serving host services up voice-stt --topology operator-topology.toml
anvil-serving host services up voice-stt --topology operator-topology.toml \
  --no-dry-run --confirm

The lifecycle action resolves the owning resource with the normal --topology, --command-host, --command-runtime, --target, and --transport options. The command runtime must match the binding resource's supervisor execution runtime. Windows Docker execution through WSL requires an explicit WSL owner. A local CLI may use --manifest PATH to inspect a deliberate local services.toml override. For Docker model adoption it may also name the owning declaration with --serve and a local --serve-manifest PATH override. Controller and MCP calls always use the owner’s configured services.toml; an MCP Docker-model adoption accepts serve but resolves its serve manifest from the owner config home. Neither remote surface accepts an arbitrary path.

A single manifest may contain several runtime owners. Unfiltered status reports other owners as requires_owner_runtime with unknown process state; select that owner's execution context to observe it. It never probes another owner's host-relative loopback URL or Docker context from the caller's runtime.

The MCP catalog exposes the same contract as host_services_status, host_services_discover, host_services_capabilities, host_services_logs, and host_services_manage. Inputs are typed service facts; they never include a command argv, an environment mapping, or a secret value. A controller cannot stop or restart itself through its own remote transport; use a declared recovery transport for that case.

Manager and engine are separate facts

The manager identifies the supervisor that owns the process. The engine says what the declared service runs. Neither field selects a route, downloads a model, or chooses a replacement model.

Host family Supported manager Native engine Docker engine Provider lifecycle
macOS launchd, docker mlx-lm, mlx-vlm, mlx-audio Docker-supported declared adapters; MLX is not a Docker engine TBD
Windows host (Linux containers or WSL) docker None Docker-supported declared adapters TBD
Linux docker None Docker-supported declared adapters TBD
NeoCloud: Vast.ai, Runpod No provider adapter yet N/A N/A TBD
Cloud: AWS, Azure No provider adapter yet N/A N/A TBD

This is the implemented platform contract. An isolated live macOS LaunchAgent smoke covers install, start, status, logs, restart, enable, disable, and stop. Docker adapters have simulated supervisor tests; live Docker lifecycle qualification on Linux, macOS, and Windows hosts using a Linux runtime remains pending.

An existing macOS LaunchAgent for Parakeet or Kokoro may be adopted only as support = "legacy". That records its supervised identity and bounded state; it does not make the legacy process a supported MLX engine. Adoption never migrates a model, moves weights, converts a definition, changes an endpoint, or replaces a running service. Plan a separate qualified migration through the owning recipe or serve manifest.

Service inventory

init installs a generic services.toml in the operator config home. The empty scaffold is intentional. Add a binding only after discovering or otherwise inspecting the exact supervisor-owned service.

schema = "anvil-services/v1"

[[service]]
id = "voice-stt"
resource = "voice"
manager = "launchd"
engine = "parakeet"
support = "legacy"
label = "org.example.voice-stt"
owner_uid = 1000
source_definition = "definitions/org.example.voice-stt.plist"
definition = "/absolute/operator/LaunchAgents/org.example.voice-stt.plist"
definition_sha256 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
memory_mib = 2048

The service id, topology resource, manager identity, engine, and support classification are required. A launchd binding pins its label, current user UID, absolute definition path, and definition_sha256. A Docker binding pins its container name, immutable image ID, and Anvil identity labels. Docker discovery returns only eligible Anvil-owned containers; it is not an arbitrary container-management tool.

Retained Docker incarnations

retained_container = true is a separate native host-service contract for an operator-reviewed container that already exists and must be stopped and started as the same Docker object. It is not adoption, recipe ownership, or external Compose ownership. A retained declaration pins the full 64-character container_id, name, immutable image_id, complete identity_labels, exact restart_count, restart_policy, restart_maximum_retry_count, and SHA-256 digests of the exact writable-mount set and a closed security projection. It also pins a separate, reviewed, nonsecret definition file with definition_sha256. shutdown_grace_seconds pins the deliberate Docker stop grace; the operation timeout must leave at least five more seconds for physical verification. The closed provenance JSON binds the service, container and image identities, reviewed nonsecret source and review digests, and the reviewed Docker healthcheck digest. healthcheck_required = true is mandatory. The definition is provenance only; the lifecycle never executes it, renders Compose, loads an environment file, pulls an image, or creates or removes a container.

Retained inspection asks Docker for a fixed projection. It never requests the container environment, command, or full inspect document. Added or removed labels, a changed container ID, name, image, writable mount, restart count or policy, or security projection all fail closed. A missing container is drift, including after a stop; it is not a successful stop. Status and exact up and down are the only supported actions. Dependencies, model/serve declarations, install, adoption, restart, enable, disable, and logs are refused.

The first command produces a reviewable retained_container.preview_sha256:

anvil-serving host services down SERVICE

Apply requires that digest and a protected owner authorization created after the operator's storage-custody, ingress-state, drain-state, and human-approval gates:

anvil-serving host services down SERVICE --expected-preview-sha256 SHA256 --operator-authorization-file PATH --no-dry-run --confirm

The authorization is an owner-only, non-symlink JSON file with schema anvil-retained-container-authorization/v1, scope retained-container-lifecycle, a five-minute-or-shorter expiry, and exact service, action, manifest, preview, container, custody, definition, and evidence digests. A stop requires storage_consumer_state = "clear" with its evidence digest. It records ingress_idle_state as quiesced or unknown and drain_state as complete or bounded, each with its evidence digest. An unknown ingress state or bounded drain is accepted only when the same protected authorization explicitly sets uncertain_interruption_risk_accepted = true and binds the fresh human-approval digest; it is never relabeled as idle or drained. The executor rechecks the manifest, definition, authorization, physical container, and custody projection under its operator lock immediately before issuing one exact docker stop --timeout SECONDS CID or docker start CID. The preview and pre-mutation comparison bind Docker's lifecycle timestamps as well as its PID and state, so a copied authorization cannot survive an intervening manual start/stop cycle. Immediately before the command it durably creates an exclusive consumption marker beside the authorization. Reusing the authorization is refused even after a manual state cycle, and a failed or uncertain command still consumes it. It never retries a mutation. A failed start may issue one rollback stop only while the original CID and full custody projection still match; replacement or configuration drift leaves an explicit hold. The preview reports a separate rollback_timeout_seconds of the declared shutdown grace plus five seconds for physical verification, along with maximum_total_seconds for the primary action and that failure-only reserve.

This contract is available only through the owning native Linux CLI. The MCP service tool does not accept authorization-file paths. A start receipt proves that the same physical Docker incarnation reached running with its declared Docker health status healthy; it does not claim application or router health. Operational activation still performs the separately reviewed native health readback. Starting an already running object or stopping an already stopped object is refused rather than reported as an applied no-op.

Ordinary Docker bindings require the first-party recipe ownership label. External Compose bindings keep their exact project, service, image, and read-only configuration-mount checks. Neither sibling mode can be used as a retained-container shortcut. Declaring or previewing retained custody does not authorize an actual pause, router stop, readmission, or any later recovery step.

startup_policy is the policy enabled by enable: it may be always or unless-stopped. disable is the separate operation that removes automatic start; there is no startup_policy = "no" adoption value.

source_definition is the staged reviewed source. definition is the final manager-owned destination. definition_sha256 pins the staged bytes and the installed launchd definition. install copies a hash-matching staged launchd definition into an existing safe destination directory without starting or enabling it. It refuses an existing registration or a changed source. For Docker, container creation remains the job of the owning serve recipe, so install only verifies that declared container is already present. No command downloads an engine or model.

If an up starts a previously registered but idle launchd job and a later step fails, the rollback explicitly bootouts that newly started job to prevent KeepAlive from starting it after the failed transaction. The error receipt reports registration_restored = false for that deliberate cleanup. A preexisting running service is never stopped by this rollback path.

An operator-config inventory and export treat services.toml, source_definition, definition, serve_manifest, and services_manifest as versionable dependency edges. The definition_sha256 field is a content pin, not a filesystem path. Selected exports include the required source/definition closure when those files are safely inside the selected operator home, and still refuse unsafe, missing, outside-home, or secret-bearing dependencies.

Model reservations and observable state

Host services may describe a model process, but they do not bypass model admission. A Docker model binding must name both serve and serve_manifest; the owning serves ledger checks its declared container and reservations before an up or restart. A native model binding needs a positive memory_mib budget. The owner refuses it when all resident native model budgets would leave less than 4 GiB for the host.

Native serves.toml entries use runtime = "native", service, and an optional services_manifest; their model and engine must match the selected binding. Native recipes use those same fields under recipe.serve, including an explicit engine and a model equal to recipe.model. Native entries reject Docker launch, GPU reservation, and exclusive-mode controls. Their up/down/status/logs actions delegate to this lifecycle. Voice STT/TTS and proxy declarations opt in with lifecycle = "service", service, and optional services_manifest.

Dependencies must share one host and supervisor execution runtime. up starts dependencies first; stopping a dependency with running consumers is refused. Stop consumers before dependencies. Cross-owner orchestration remains unsupported.

The status payload keeps these facts separate:

Fact Meaning
registered The supervisor has the pinned identity registered.
running The supervisor reports that process as running.
enabled Its automatic-start policy is enabled.
state Supervisor lifecycle detail such as absent, unloaded, exited, or unavailable.
engine.ready A declared loopback endpoint answered its bounded readiness probe.
identity and support The pinned manager identity and whether the binding is supported or legacy.

Running does not prove readiness, enabled does not start a process, and an adopted legacy identity does not prove model residency or route eligibility. Unknown, inaccessible, or changed supervisor state blocks a mutation until the owner can inspect it again. logs returns a bounded, redacted tail from only the declared service log sources.