Host Runtime Supervisor
Host Runtime Supervisor
The host runtime runs an agent directly on the operator's machine. It is the lowest isolation tier in the host -> Docker -> VM spectrum and is intended for AIWG base-level local execution where a VM or container is unavailable or operator-selected away.
Host provisioning is supervisor-backed. The admin API must not launch an unmanaged local shell and then return; a durable host supervisor or daemon owns the process group, environment, PTY/session attachment, liveness, and cleanup for every host-backed instance.
Responsibilities
The supervisor boundary is responsible for:
- Keeping host-backed agents alive independently of one HTTP request handler.
- Starting, stopping, destroying, and reporting liveness for host instances.
- Owning launch cwd, environment, labels, loadout/profile selection, and
agentshare wiring.
- Managing PTY/session attachment and reattach through the executor contract.
- Supporting multiple watch agents on one host without ID, cwd, or PTY
collision.
- Cleaning up process trees so local shells are not orphaned after management
server restarts or operator stop/destroy actions.
- Reporting the session backend in use (`native`, `screen`, `zellij`, or
`tmux`) and whether it is a `direct` or `managed` session so #461 can layer multiplexer-backed session control on top of #460.
Isolation
Host runtime isolation is `host`. The launched process has ambient user-host access unless the operator separately configures OS controls such as cgroups, namespaces, AppArmor, SELinux, or a restricted service account. The runtime extension intentionally omits `image_ref` for host instances because there is no image boundary.
Current Wiring
The management server exposes a `HostRuntimeSupervisor` boundary. When no supervisor is configured, `POST /api/v2/admin/instances` with `"runtime": "host"` fails closed with `501 runtime.not_implemented`. The 501 detail points at this document and the `AGENTIC_HOST_RUNTIME_ENABLED` opt-in; it is the expected response on a server where the host tier was never enabled, not a missing feature.
Capability signal
Harnesses and orchestrators should not infer host-tier availability from a 501. `GET /healthz/deep` reports it directly:
{ "status": "healthy", "agent_count": 0, "active_tasks": 0, "host_runtime_enabled": false }
`host_runtime_enabled` is `true` exactly when a supervisor is wired (i.e. `AGENTIC_HOST_RUNTIME_ENABLED=1`). A conformance/UAT matrix that reads this field can treat the host tier as skipped when it is `false` rather than recording a failure, and only assert host provisioning when it is `true`.
The dedicated `Host Runtime` CI tier (`.gitea/workflows/host-runtime.yml`) boots the server with the local supervisor enabled and asserts both that `/healthz/deep` reports `host_runtime_enabled: true` and that `POST /api/v2/admin/instances {"runtime":"host"}` is accepted (202) rather than fail-closed (501).
When a supervisor implementation is configured, admin v2 submits a `HostProvisionRequest`, records the resulting operation, and registers a host `InstanceContext` in the v2 executor registry so `/agents/{instance_id}/*` routes resolve through the same contract as Docker and VM instances.
Admin v2 lifecycle operations also route host-backed instances through the configured supervisor. `POST /api/v2/admin/instances/{id}/stop` asks the supervisor to stop the recorded host process and marks the executor context unready while preserving both that context and per-instance state, so a subsequent destroy remains routed to the host supervisor after the agent stream disconnects. `POST /api/v2/admin/instances/{id}/destroy` asks the supervisor to stop and remove its per-instance state, then drains the executor context and signing key directory. VM instances continue to use the existing libvirt lifecycle path.
The built-in local supervisor is opt-in:
| Environment variable | Default | Meaning |
|---|---|---|
| `AGENTIC_HOST_RUNTIME_ENABLED` | unset / disabled | Set to `1`, `true`, or `yes` to enable host provisioning. |
| `AGENTIC_HOST_RUNTIME_MODE` | `local` | `local` uses the built-in process-backed supervisor; `daemon` delegates to a host-side supervisor service over Unix socket. |
| `AGENTIC_HOST_RUNTIME_ROOT` | Linux: `/var/lib/agentic-sandbox/host-runtime`; macOS: `~/Library/Application Support/io.aiwg.agentic-sandbox/host-runtime` | Root for private per-instance env, metadata, PID, log, and TLS files. |
| `AGENTIC_HOST_WORKSPACE_ROOT` | Linux: management cwd; macOS: `~/Library/Application Support/io.aiwg.agentic-sandbox/workspace` | Default working directory when the request omits `working_dir`. |
| `AGENTIC_HOST_AGENT_CLIENT` | `agent-client` | Agent client binary to spawn for each host instance. |
| `AGENTIC_HOST_GRPC_SERVER` | management gRPC bind address | Management gRPC endpoint passed to the local agent. |
| `AGENTIC_HOST_SUPERVISOR_ID` | `host-supervisor-local` | Identifier reported in provision results. |
| `AGENTIC_HOST_BOOTSTRAP_ENROLLMENT_URL` | `https://localhost:8124/api/v1/bootstrap-enrollment/consume` | HTTPS bootstrap enrollment URL passed to host agents. |
| `AGENTIC_HOST_BOOTSTRAP_CA` | unset | CA certificate used to pin the host bootstrap HTTPS endpoint. Required when secure bootstrap material is issued. |
| `AGENTIC_HOST_RUNTIME_DAEMON_SOCKET` | Linux: `/run/agentic-sandbox/host-runtime.sock`; macOS: `$TMPDIR/io.aiwg.agentic-sandbox/host-runtime.sock` | Unix socket used when `AGENTIC_HOST_RUNTIME_MODE=daemon`. The macOS parent is mode `0700`. |
| `AGENTIC_HOST_RUNTIME_DAEMON_TIMEOUT_SECS` | `10` | Per-request daemon RPC timeout. |
With the local supervisor enabled, host provisioning writes `<root>/instances/<instance_id>/agent.env`, starts a detached local `agent-client` when `start: true`, and records `<root>/instances/<instance_id>/metadata.json`. Each provisioned host instance gets a unique `host-<instance-prefix>` watch agent, allowing multiple host agents on the same machine without ID or cwd collisions.
Native-host instances do not run the VM/container guest-image setup pipeline. The supervisor therefore writes `AGENT_SETUP_COMPLETE=1`; the agent treats this explicit supervisor signal like the guest setup-complete marker and reports `Ready` on its first heartbeat after transport enrollment.
When the management server has a bootstrap token store and gRPC mTLS listener, host provisioning issues a one-time bootstrap enrollment token for the host agent. The local supervisor writes `AGENT_TRANSPORT=auto`, the bootstrap SPIFFE identity, the bootstrap TLS directory, and the short-lived token into `agent.env`; it does not expose the plaintext token in the admin operation result. The agent exchanges that token for mTLS client material, reconnects with transport identity, and scrubs the bootstrap token fields from `agent.env` after successful enrollment.
Local stop/destroy read the PID recorded in that metadata file. Stop sends SIGTERM to the instance's dedicated process group, waits for and reaps the owned agent process, and escalates to SIGKILL only after a bounded timeout. It retries interrupted child-status waits so platform signal delivery cannot turn a completed shutdown into a failed lifecycle response. It records the instance as stopped only after that process has exited; destroy then removes the supervisor-owned instance directory. This built-in supervisor is process-backed and opt-in; deploying it as a persistent host service remains an operator action.
When `AGENTIC_HOST_RUNTIME_MODE=daemon`, management does not spawn local agent processes directly. It connects to the configured Unix socket and sends one line-delimited JSON request per lifecycle operation. The daemon owns process groups, PTY/session hosts, liveness, reattach, and multi-watch-agent placement. If the socket is unavailable, times out, returns malformed JSON, or returns an error response, management fails the host operation closed instead of falling back to VM, Docker, or the process-backed supervisor.
The repository ships a first-party daemon binary:
cargo run --manifest-path management/Cargo.toml --bin agentic-host-runtime-daemon -- \
--socket /run/agentic-sandbox/host-runtime.sock \
--root-dir /var/lib/agentic-sandbox/host-runtime \
--agent-client agent-client \
--management-server 127.0.0.1:8120 \
--grpc-tls-server-name localhost \
--bootstrap-enrollment-url https://localhost:8124/api/v1/bootstrap-enrollment/consume \
--bootstrap-ca /etc/agentic-sandbox/grpc-mtls/ca.pem
The daemon binds the Unix socket, serves one JSON request per connection, and delegates provision/stop/destroy to the same local supervisor implementation used by process-backed mode. It removes only the socket it created during a clean shutdown; it refuses to start if the socket path already exists, which keeps stale-socket cleanup as an explicit operator action.
`management/systemd/agentic-host-runtime-daemon.service` is an example unit for operators who want the daemon to survive controller restarts. Enabling or starting that unit is a host action and is not performed by the management server. When management uses the daemon, set management-side variables such as:
AGENTIC_HOST_RUNTIME_ENABLED=1
AGENTIC_HOST_RUNTIME_MODE=daemon
AGENTIC_HOST_RUNTIME_DAEMON_SOCKET=/run/agentic-sandbox/host-runtime.sock
On Apple Silicon macOS, the equivalent user service is `deploy/launchd/io.aiwg.agentic-sandbox.host-runtime.plist`. It is deliberately not loaded by build or package construction. Render package or development binary paths with `scripts/render-macos-launch-agent.sh`, validate with `plutil -lint`, and explicitly opt in with `launchctl bootstrap`. It runs as the logged-in user and relies on the private per-user defaults above; it never uses root-owned runtime state. See the macOS Host Runtime and Local CA Keychain Runbook.
After management restarts, a reconnecting supervisor-owned agent whose id uses the reserved `host-` prefix is reclassified as `host` before its loadout is considered. This reconstructs the executor route truthfully instead of misclassifying the process as a VM or container, while the daemon retains lifecycle ownership and the agent reports its live PTY sessions for reattach.
Daemon request envelope:
{
"request_id": "019b23e3-9d41-7a31-a3aa-6e3d8f2b6f80",
"op": "provision",
"instance_id": "019b23e3-8f8b-7ad0-8bd3-13d9ac0f1db7",
"provision": {
"instance_id": "019b23e3-8f8b-7ad0-8bd3-13d9ac0f1db7",
"name": "agent-host-local",
"loadout": "agentic-dev",
"profile": null,
"image_ref": null,
"agentshare": true,
"start": true,
"working_dir": "/workspace",
"labels": {}
}
}
`op` is one of `provision`, `stop`, or `destroy`. `stop` and `destroy` omit the `provision` object. Successful responses return either `provisioned` or `lifecycle`:
{
"ok": true,
"provisioned": {
"instance_id": "019b23e3-8f8b-7ad0-8bd3-13d9ac0f1db7",
"name": "agent-host-local",
"supervisor_id": "host-supervisor-daemon",
"host_endpoint": "workstation-1",
"session_backend": "tmux",
"watch_agents": ["host-019b23e3-a", "host-019b23e3-b"]
}
}
Error responses use:
{
"ok": false,
"error": {
"code": "working_dir.not_found",
"message": "working_dir does not exist: /workspace"
}
}
`InstanceProvisionRequest.working_dir` is honored for host instances and must point at an existing directory. If omitted, the supervisor uses the management server's current directory. Docker and VM provisioning ignore this field.