Skip to content

Runtime adapter

The RuntimeAdapter is the one place in Hostwright that talks to the runtime. Nothing else — not the reconciler, not the CLI, not the state store — calls Apple container directly. This single boundary is the most important architectural decision in the project.

Hostwright core talks only through the RuntimeAdapter interface to a CLI adapter today and a planned native adapter later.

Apple container is young and evolving. Its command surface, structured output, and native APIs will change. If assumptions about it were scattered across the reconciler, the state store, and the CLI, every runtime change would ripple through the whole codebase.

By forcing every runtime operation through one typed protocol, Hostwright keeps those assumptions contained. The reconciler works against an abstract contract; it does not know or care whether the call is served by shelling out to the Apple container CLI or by a future Swift-native client.

The adapter exposes:

  • metadata() — adapter and runtime identity;
  • capabilities() — supported capability discovery;
  • observe(desiredState:) — read-only runtime snapshots;
  • plan(desiredState:observedState:) — adapter-aware planning hooks;
  • logs(for:tail:) — bounded read-only log output;
  • execute(_:confirmation:) — the mutation hook. It may perform exactly one supported mutation action, and only after confirmation, plan hash, policy validation, and state persistence gates have all passed.

Typed inputs, typed outputs, typed errors. The reconciler never parses a human-readable table.

Every process the adapter might run is represented as a typed command spec and classified as readOnly, mutating, forbidden, or unknown. The process runner is not a general shell-out path:

  • the executable must resolve through a controlled resolver;
  • read-only commands must pass read-only policy;
  • mutating commands must carry one of exactly four supported kinds: createMissingService, startManagedService, restartManagedService, deleteManagedContainer;
  • forbidden, unknown, and unsupported specs are rejected before execution;
  • every command has a timeout (default 30 s, maximum 300 s) and captured, redacted output.

Redaction applies to command arguments, environment values, stdout, stderr, parser errors, and runtime error messages — sensitive key fragments such as token, password, secret, credential, auth, and key are masked.

Hostwright-created resources carry collision-resistant versioned runtime identifiers and exact ownership labels. Observation binds current-project labels to the exact identifier, keeps owned orphans visible, ignores unrelated labeled projects, and fails closed on malformed current-project ownership. Managed start, managed restart, and cleanup operate on exact identifiers only — never on recomputed names. This is what lets two projects with identical legacy names remain distinct and safely cleanable.

The observation parser accepts only reviewed shapes: the fixture-defined observation schema, the verified real empty container list --all --format json output, Apple builder output (ignored as non-Hostwright state), and exact labeled Apple container 1.0.0 rows with network metadata (hostname, IPv4/IPv6, gateway, MAC, network name, MTU). Anything else is reported as a parse failure instead of being guessed at — unverified CLI output is never turned into product truth.

The concrete implementations today drive the Apple container CLI: AppleContainerReadOnlyAdapter for observation and AppleContainerApplyAdapter for the four narrow mutations. A Swift-native adapter built on Apple’s Containerization APIs is a later option — adopted only when that surface is stable enough to depend on. Both would implement the same protocol, so the rest of the system is unaffected by the switch.