Hub
The full-stack primary node. Runs the Mac Mini host with the Ubuntu VM, all AI agents, Home Assistant, inference servers, and the complete service catalog. There is exactly one hub per Sanctum instance.
Sanctum is designed to run across multiple physical locations. A primary hub at your main haus coordinates with satellite nodes at secondary properties, mobile nodes on laptops, and sensor nodes for lightweight IoT devices (the RuView door cameras are the first). All nodes share a single instance.yaml configuration and communicate over Tailscale.

What you’re looking at is a multi-site distributed system. The kind of thing a mid-sized company might run across three data centers. Except the data centers are hauses, the ops team is one person, and the primary node is next to a coffee maker.
Hub
The full-stack primary node. Runs the Mac Mini host with the Ubuntu VM, all AI agents, Home Assistant, inference servers, and the complete service catalog. There is exactly one hub per Sanctum instance.
Satellite
A lighter deployment at a secondary haus. Runs a subset of services (typically a gateway, Home Assistant, and a small local model). Syncs configuration and state with the hub over Tailscale.
Mobile
A MacBook Pro or similar portable device. Connects to the hub remotely via Tailscale for agent access, SSH, and API calls. Runs a small set of persistent guardrail daemons (see “Persistent Services Across Hosts” below) but no user-facing primary services.
Sensor
Dedicated IoT or monitoring hardware that reports to the hub without running the full Sanctum stack. The first ones in service are the RuView door cameras (ruview_front, ruview_garage, and friends) — low-power eyes, not brains.
Each node knows who it is through a single-line file at ~/.sanctum/.node_id:
# On the hub:cat ~/.sanctum/.node_id# manoir
# On the satellite:cat ~/.sanctum/.node_id# chaletOne file. One word. The machine’s entire sense of self lives in a text file smaller than a tweet. And yet if you delete it, everything stops knowing where it is. Identity is fragile — even for computers.
The identity string must match a key in the nodes section of instance.yaml. Scripts and services use this to determine which configuration block applies to the current machine.
source ~/.sanctum/lib/config.sh
NODE=$(sanctum_whoami) # "manoir"TYPE=$(sanctum_node_get "$NODE" type) # "hub"IP=$(sanctum_node_get "$NODE" tailscale_ip) # "100.0.0.20"The node key (manoir) is the machine’s name; its type (hub) is its role. Two different words for the same box — a distinction that earns its keep the day you have two hubs.
Every node is declared under the nodes key with its network addresses, SSH user, node type, and the list of services it runs:
nodes: manoir: type: hub host: 192.0.2.10 tailscale_ip: 100.0.0.20 ssh_user: neo vm: host: 10.0.0.10 # agent VM over the host bridge ssh_user: ubuntu
chalet: type: satellite host: "" # Set during on-site install tailscale_ip: 100.0.0.30 ssh_user: neo services: home_assistant: { enabled: true, port: 8123 } agent: { enabled: true, name: ahsoka, model: qwen2.5-coder-14b-instruct } local_model: { enabled: true, port: 1234 } # Qwen2.5-Coder-14B via LM Studio
mbp: type: mobile host: 192.0.2.111 # DHCP on the haus LAN; varies elsewhere tailscale_ip: 100.0.0.40 ssh_user: neo services: autoresearch: { enabled: true }| Field | Required | Description |
|---|---|---|
type | Yes | One of hub, satellite, mobile, sensor |
host | No | LAN IP address. Empty string ("") if off the haus network or DHCP-assigned. |
tailscale_ip | Yes | Stable Tailscale IP for cross-network access |
ssh_user | Yes | Username for SSH connections to this node |
services | No | Map of services this node runs (name: { enabled, port, ... }). The hub omits it — by definition it runs the full catalog. |
The hub is the authoritative node. It runs every service, hosts the VM with the agent cluster, and is the source that satellites sync from. In organizational terms, this is the head office, the server room, and the IT department — all running on a machine the size of a hardcover book.
sanctum-mlx, sanctum-mlx-codestral, Sanctum TTS) and the sanctum-server router that fronts theminstance.yamlThese services only run on the hub and are not deployed to satellites:
| Service | Reason |
|---|---|
Council MLX (sanctum-mlx) | Qwen3.6-35B-A3B-4bit needs ≥ 40 GB unified memory + a Metal-class GPU |
Codestral MLX (sanctum-mlx-codestral) | Codestral-22B-v0.1-4bit on :3301. (Superseded the Qwen2.5-Coder-14B coder seat on :1338, retired 2026-06-07 along with the LM Studio :1234 bridge.) |
| Firewalla Bridge | Direct LAN access to the primary router |
| Cloudflare Tunnel | Single ingress point for the instance |
| Orbi Bridge | Direct LAN access to the access point |
| Sonos Bridge | Native SoCo control of LAN Sonos speakers |
| Voice Agent | Tied to local Sonos Bridge and Sanctum TTS |
The Mac Mini hub runs one virtual machine — the Ubuntu agent VM on Lima vmType=vz — plus an OrbStack container runtime for the few persistent containers (Home Assistant, Outline). The bridge from the Mac host to the VM is bridge100 on a private /24: the Mac is 10.0.0.1, the VM is 10.0.0.10. OrbStack runs on its own private network and does not contest bridge100.
The agent VM was migrated from QEMU/HVF to Lima vmType=vz on 2026-05-10 to end the 370% idle-spin the QEMU/HVF bind was burning (field note). The container runtime was migrated from Colima to OrbStack on 2026-04-27 (Colima→OrbStack field note) — OrbStack uses native networking with no vmnet contention, so the dual-bridge race that haunted the QEMU+Colima era is structurally impossible now. The Colima LaunchAgent was retired in the same cleanup on 2026-05-16.
The Mac Mini starts every service — including the agent VM — without a GUI login. No auto-login, no Touch ID removal, no compromises. The boot graph runs through a small set of system LaunchDaemons in /Library/LaunchDaemons/ and the usual user LaunchAgents:
sanctum-mlx (:1337, mTLS, Qwen3.6-35B-A3B-4bit) and sanctum-mlx-codestral (:3301, Codestral-22B-v0.1-4bit, the coder seat since the :1338 Qwen coder + :1234 LM Studio bridge were retired 2026-06-07) load. proxyd on :4040 and sanctum-server on :8900 come up to front them. Both MLX seats load as user LaunchAgents (com.sanctum.mlx, com.sanctum.mlx-codestral), not system daemons.vmnet bridge is set up via socket_vmnet; the host IP is pinned to 10.0.0.1; the boot path waits for VM SSH before declaring success.apple-post-boot.sh loads SSH keys from Keychain via ssh-add --apple-load-keychain; socat proxies stitch VM-side requests to Mac-side services (Firewalla, MLX coder, SanctumBridge).Five P0 services (force-flow, proxyd, watchdog-rust, ha-gateway, firewalla) were promoted from user LaunchAgents to system LaunchDaemons in 2026-05-07/08 (field note). They survive any reboot pattern, including a missing /etc/kcpassword. The MLX seats stayed user-domain on purpose: a model that can’t reach the user’s Keychain boots into a brick, so they wait for login state rather than racing it.
# The whole boot graph is one script, run by the bootstrap LaunchDaemon at boot.# Run it by hand to (re)start every service without a reboot:bash ~/.sanctum/boot/sanctum-bootstrap.shmacOS Sequoia+ does not auto-load SSH keys into the agent at boot. The apple-post-boot.sh script runs ssh-add --apple-load-keychain to load all Keychain-stored SSH passphrases into the agent. Without this, non-interactive LaunchAgents (SSH tunnels, skill sync, VM health checks) cannot authenticate to the VM.
The SSH config for VM hosts uses IdentityAgent SSH_AUTH_SOCK to ensure they use the system agent, not the 1Password SSH agent (which is the Host * default).
A satellite is a smaller deployment at a secondary location. It runs a gateway with a lightweight local model and its own Home Assistant instance for location-specific devices.
Think of it as a field office. It can operate independently, make local decisions, and keep the lights on — but the real horsepower stays at headquarters. The satellite doesn’t need five AI agents. It needs to control the heat and not die when the internet goes out.
.node_id file to the satellite’s name (e.g., satellite).:1234, which backs the on-site agent (ahsoka). Or route to the hub’s :3301 Codestral coder seat when the link is reliable.host field in instance.yaml on the hub.Satellites pull updates from the hub over Tailscale:
Hub (manoir) Satellite (chalet) | | +-- instance.yaml ---- Tailscale ----> instance.yaml (subset) +-- skills repo ---- Tailscale ----> skills repo (rsync) +-- agent config ---- Tailscale ----> agent configMobile nodes are laptops that connect to the Sanctum instance remotely. They run a small set of persistent guardrail daemons (see below) but no user-facing primary services. Otherwise they SSH into the hub, query agents, and access dashboards over Tailscale.
| Action | Command / URL |
|---|---|
| SSH to hub | ssh neo@100.0.0.20 |
| SSH to VM | ssh -J neo@100.0.0.20 ubuntu@10.0.0.10 |
| Dashboard | http://100.0.0.20:1111 |
| Home Assistant | https://ha.<your-domain> (via Cloudflare tunnel) |
| Agent query | Via gateway API at 100.0.0.20:1977 |
Two Sanctum daemons run on both the hub and the mobile node. They are deliberately symmetric: each host enforces its own RAM ceiling and exposes its own Claude Max subscription. There is no central authority — that was the point of the 2026-04-24 capacity doctrine. Each host is responsible for its own air supply.
| Daemon | Hub (Mac Mini) | Mobile (MacBook Pro) | Role |
|---|---|---|---|
com.sanctum.castellan (Gollum) | :2189 | :2189 | RAM-pool admission + capacity governor (Rust root LaunchDaemon). Superseded sanctum-admit 2026-05-29 and absorbed the old pressure-valve’s shed-and-freeze duty. Per-host doctrine. |
com.sanctum.claude-max-proxy | :3456 | :3456 | OpenAI-compatible HTTP wrapper around the local claude CLI. Each host has its own Claude Max OAuth session; no cross-machine routing required. |
com.sanctum.pressure-valve used to be the third entry here — a standalone daemon that shed and froze memory offenders before the kernel could. It was retired 2026-05-29 and its job folded into castellan; one governor that admits and sheds is harder to leave half-running than two that have to agree.
The Claude proxy was unified on 2026-04-27 — before that the hub ran a per-request CLI-spawn proxy on :2001 (com.sanctum.claude-cli-proxy), while the mobile ran the persistent claude-max-api-proxy npm package. Both hosts now run the same npm package via a tiny wrapper at ~/.sanctum/bin/claude-max-api-tailscale.js. Smart-router cloud-backend fallback is symmetric: either host can serve the other’s escalations if the primary is down.
Per-host symmetry by design: each machine answers for its own air supply (capacity doctrine, 2026-04-24). User-facing and stateful daemons (watchdog, chitti, livekit-server, sanctum-mlx, home-assistant, outline) remain hub-only — the mobile is a dev and validation surface and doesn’t carry production state.
Both the shell and TypeScript libraries provide functions for working with the node topology:
source ~/.sanctum/lib/config.sh
# Who am I?sanctum_whoami # "manoir"
# Get a field from any nodesanctum_node_get chalet tailscale_ip # "100.0.0.30"sanctum_node_get mbp ssh_user # "neo"import { whoami, nodeGet, getNodesByType } from './lib/config';
const me = whoami(); // "manoir"const satellites = getNodesByType('satellite'); // ["chalet"]const chaletIp = nodeGet('chalet', 'tailscale_ip'); // "100.0.0.30" Tailscale Mesh (your-tailnet.ts.net) _______________________________________________ / | \ Hub: manoir Satellite: chalet Mobile: mbp 100.0.0.20 100.0.0.30 100.0.0.40 | | | [Mac Mini M4 Pro] [Mac Mini M1] [MacBook Pro M4 Max] +-- Lima vz VM (Ubuntu) +-- Gateway (guardrail daemons) +-- 5 AI Agents +-- Home Assistant +-- castellan +-- Full service catalog +-- Coder-14B (LM Studio) +-- claude-max-proxy +-- Home Assistant +-- sanctum-mlx + codestral +-- sanctum-server routerThree machines. Two hauses. One tailnet. Zero regrets. Well. Few regrets.