# deputyOS — full site content > deputyOS is a batteries-included Linux appliance image for running open-source personal AI assistants on hardware you own. It manages a fleet of machines from one console, with signed updates, encrypted backup, tunnelled remote access and an air-gapped tier. Apache-2.0, no telemetry. Canonical site: https://www.deputyos.com/ License: Apache-2.0 --- # Documentation ## Air-gapped image builds Section: build URL: https://www.deputyos.com/docs/build/airgap-builds/ ```bash make build TARGET= PROFILE= TIER= AIRGAP=1 ``` The `AIRGAP=1` flag composes with the existing `TIER` and `TARGET` knobs. Every supported (target × tier) tuple gets an airgap variant. ## Pre-bake host requirements The Packer host needs the LFM2 GGUFs cached locally. `scripts/build.sh` fetches and verifies SHAs once into `build/staging/llm/` before invoking Ansible. | Tier | GGUF | SHA256 (pinned in `roles/deputyos/vars/llm-airgap.yml`) | |----------|-------------------------------------------------------|--------------------------------------------------------| | lean | `LFM2-350M.Q4_K_M.gguf` | (pinned) | | standard | `LFM2-1.2B.Q4_K_M.gguf` | (pinned) | | rich | `LFM2-2.6B.Q4_K_M.gguf` + `Qwen2.5-Coder-1.5B-Instruct.Q4_K_M.gguf` | (pinned, both) | `vars/llm-airgap.yml` ships with `REPLACE_ME_*` placeholders. The first real release fills them in once the source mirror's bytes are reproducible. The role refuses to bake an image whose model SHA does not match. ## What `AIRGAP=1` adds vs. a normal build | Layer | Normal build | `AIRGAP=1` | |----------|-------------------------------------------|---------------------------------------------------------------------------------------| | apt | `https://deb.debian.org/` | `file:///opt/deputyos/airgap/apt-mirror/` | | egress | `ufw default deny`, allow-list per channel| nftables policy `mode=airgap`; only RFC1918 + mDNS + local DNS escape | | LLM | optional Ollama / cloud API | LFM2 GGUF baked + `deputyos-llamacpp@.service` enabled | | `deputyctl model list` | catalog from network | catalog from `/opt/deputyos/airgap/models/catalog.json`; no network calls allowed | | `/etc/deputyos/limits.json` | `airgap_supported: false` | `airgap_supported: true` | ## Verifying ```bash qemu-system-x86_64 \ -nodefaults -nographic \ -drive file=build/qemu-x86_64-openclaw.qcow2,if=virtio \ -m 4G -smp 2 \ -netdev user,id=u0 -device virtio-net,netdev=u0 \ -net none # <-- the smoking gun ``` Wizard at `http://deputyos.local:8088/` should still come up; built-in chat at `/chat` should still answer (slowly on lean; happily on rich). ## Architecture Section: concepts URL: https://www.deputyos.com/docs/concepts/architecture/ deputyOS is an *appliance image* for personal AI assistants. This page explains what that phrase means in practice, what runs on a booted device, how a device gets from "freshly flashed" to "operational", and how it is updated over time. The goal is a working mental model of the system — not a tour of source files. For source-grounded detail, follow the cross-links into [Reference](/docs/reference/deputyctl/). ## The appliance model A traditional install puts a Linux distribution on a device and then layers an application on top. The application brings its own runtime (`apt install nodejs`, `pip install …`, `npm install …`), pulls native modules at install time, writes service units, and hopes the host's package manager and the application's package manager agree about everything in between. Every step talks to the network. Every step can fail. deputyOS inverts that. The image is built once, in a controlled environment, with every runtime, every native module, every signature database, every configuration template baked in. The booted device runs a Linux kernel and systemd, but it does not run a package manager *for the assistant*. The assistant — and the deputyOS management surface — are already on disk. The single load-bearing invariant is therefore: > **Zero first-boot network installs** > No `apt`, `npm`, `pip`, `cargo`, or `git clone` runs after the image is > flashed. The only outbound network traffic on first boot is DHCP, DNS, > NTP, and the model provider the user chose. Optional additions > (Tailscale, Cloudflare Tunnel) are user-driven and explicitly opt-in. > Everything else in this document is a consequence of that invariant. ## Lifecycle from a user's perspective ```mermaid sequenceDiagram autonumber participant U as User participant CDN as CDN
(signed manifest + image) participant Dev as Device
(Pi / mini-PC / VM) participant Wiz as deputywizard participant GW as Active profile
gateway participant PWA as deputypwa U->>CDN: Pick target on deputyos.com CDN-->>U: Signed image + minisig U->>Dev: Flash / launch image Dev->>Dev: Boot, mount data partition,
start systemd units Dev->>Wiz: deputywizard.service starts on :8088 Wiz->>U: mDNS deputyos.local + QR on TTY U->>Wiz: Provider key, channels, allowlist Wiz->>Dev: Persist /etc/deputyos/secrets.env (0600) Wiz->>GW: Start active profile gateway GW->>PWA: Send hooks via message relay U->>PWA: Open /chat — talk to assistant Note over Dev,CDN: Later: deputyctl update --check Dev->>CDN: Fetch signed manifest CDN-->>Dev: manifest-v1.json + .minisig Dev->>Dev: Verify, download to inactive slot,
set boot pointer, reboot Dev->>Dev: Watchdog: rollback if unhealthy ``` The phases are: 1. **Pick.** The user visits the picker page, which reads the latest signed manifest and surfaces the right artefact for their hardware. See [Reference → Schemas → release manifest](/docs/reference/release-manifest/). 2. **Flash / launch.** For real hardware, an image is written to an SD card or NVMe. For a desktop, the [`deputyos-desktop`](/docs/reference/deputyos-desktop/) launcher boots the image inside the platform's native hypervisor (WSL2, UTM, qemu+KVM). For a cloud VM, a 1-Click marketplace listing or a `cloud-init` recipe applies. 3. **Boot.** The kernel comes up with sysctls already tuned, AppArmor already enforcing, ufw already default-deny. systemd starts the wizard, the QR-on-TTY service, mDNS via Avahi, and (if a profile is already selected) the active profile's gateway service. 4. **Wizard.** The user opens `http://deputyos.local` (mDNS) or scans the QR shown on the HDMI output. [`deputywizard`](/docs/reference/deputywizard/) collects the model provider key, the chosen channels, and the gateway allowlist; it validates the key against the provider with a real round-trip; it writes `/etc/deputyos/secrets.env` at mode 0600 and restarts the gateway. 5. **Run.** [`deputypwa`](/docs/reference/deputypwa/) is the always-on dashboard at `deputyos.local/chat`. The active profile's gateway speaks to it through the [message relay](/docs/reference/message-relay/). Day-to-day operations happen via the PWA or via [`deputyctl`](/docs/reference/deputyctl/). 6. **Update.** When upstream cuts a new release, the [`deputyos-track`](/docs/reference/deputyos-track/) bot proposes a manifest bump; CI bakes new images; the signed manifest is published to the CDN. `deputyctl update --check` polls; `deputyctl update --apply` writes the new image into the inactive A/B slot, sets a one-shot boot pointer, and reboots. 7. **Rollback.** If the new slot fails to come up healthy within five minutes, the watchdog rolls back to the previous slot. The data partition is never overwritten — user state survives both the update and the rollback. See [Operations → Update and rollback](/docs/operations/update-and-rollback/). ## The image as the unit of release The image is the unit of trust, the unit of distribution, and the unit of rollback. Every image carries: - **Two read-only system slots** (`slotA`, `slotB`). One is active; the other is the rollback target. - **A read-write data partition** mounted at `/home/agent/` (and `/var/log/deputyos/`, `/etc/deputyos/secrets.env`, the cost ledger). Persists across image swaps. - **A FAT firmware partition** carrying the bootloader configuration and any pre-flash user config (`deputyos.yaml` on Pi). - **A baked-in SBOM** at `/etc/deputyos/sbom.json` (CycloneDX) and a build-provenance file at `/etc/deputyos/build.json` (commit, builder, profile versions, ClamAV DB date, Magika model version). - **The active profile's bake** under `/opt/deputyos/profiles//`. - **Pre-resolved offline caches** for npm, pip/uv, and skill assets under `/var/cache/deputyos/`. - **Packed ClamAV signatures** at `/var/lib/clamav/` and [Magika](https://google.github.io/magika/) model weights at `/opt/deputyos/magika/`. The full filesystem inventory is in [Reference → System → Filesystem layout](/docs/reference/filesystem-layout/). > **A/B is hardware-dependent** > Not every target supports A/B. Pi, mini-PC, and qcow2 images do. > DigitalOcean uses platform snapshots; Hetzner / Vultr / Linode / > Fly.io use re-deploy with the new manifest. WSL2 is a tarball > re-import. Each strategy is documented in > [Distribution → Hardware matrix](/docs/distribution/hardware-matrix/) > and [Operations → Update and rollback](/docs/operations/update-and-rollback/). > ## Process model on a running device Five long-running processes (six if you count the message relay's helper) make up the runtime surface: ```mermaid flowchart LR subgraph systemd["systemd (AppArmor enforced, ufw default-deny)"] wizard["deputywizard.service
:8088 (mDNS deputyos.local)"] pwa["deputypwa.service
:8088 /chat (post-wizard)"] gateway["<profile>-gateway.service
OpenClaw / Hermes / Khoj"] relay["agent-relay.service
Unix socket"] qr["deputyos-qr-on-tty.service
HDMI / serial"] avahi["avahi-daemon
mDNS"] end user[/User on LAN/] pwa <--> user wizard <--> user gateway -- hooks --> relay relay --> pwa relay --> gateway qr -. prints URL on boot .- user avahi -. deputyos.local .- user ``` What each one does: - **[`deputyctl`](/docs/reference/deputyctl/)** is not a daemon. It is the operator CLI — invoked over SSH or by the wizard, the PWA, or systemd one-shots. It reads the active profile's manifest (see [Reference → Schemas → profile.toml](/docs/reference/profile-toml/)), drives systemd, validates model keys, performs updates, rotates secrets, runs backups, and prints capability limits. - **[`deputywizard`](/docs/reference/deputywizard/)** is a small axum HTTP server on `:8088` that implements the first-boot UX. It is profile-aware (it asks the questions each profile's manifest declares it needs) but profile-agnostic in code. It writes `/etc/deputyos/secrets.env` and hands off to the gateway. Routes are documented in [Reference → APIs → wizard HTTP](/docs/reference/wizard-http/). - **[`deputypwa`](/docs/reference/deputypwa/)** is the always-on dashboard + private chat client. It replaces the wizard on `:8088` once first-boot setup completes. Routes are documented in [Reference → APIs → PWA HTTP](/docs/reference/pwa-http/). It surfaces the [`deputyctl limits`](/docs/reference/deputyctl/) view, the cost ledger (see [Operations → Cost guardrails](/docs/operations/cost-guardrails/)), the channel toggles, and the chat UI. - **The active profile's gateway** is the upstream agent itself — `openclaw onboard --daemon`, `hermes gateway start`, or `khoj --no-gui --host 127.0.0.1 --port 42110` — running under its own systemd unit and its own AppArmor profile. deputyOS adds nothing inside the agent process; it provides the host environment, the secrets, the channel allowlist, and the relay. - **The message relay** is a tiny Unix-socket service that brokers hook events between the gateway and the rest of deputyOS (the PWA, the cost ledger, the doctor). Its wire format is in [Reference → APIs → message relay](/docs/reference/message-relay/); its hook payload schemas are in [Reference → Schemas → hook payloads](/docs/reference/hook-payloads/). Hooks are how a profile signals "channel inbound", "tool call started", "cost incurred", etc., without each profile having to learn deputyOS-specific IPC. - **The QR-on-TTY service** prints the wizard URL and a QR code to the HDMI output and the serial console at boot, so a headless device is reachable without SSH. - **Avahi** advertises `deputyos.local` on mDNS for trivial LAN discovery. All long-running services run under systemd with AppArmor in enforce mode, ufw default-deny on the inbound chain, and a non-root `agent` user owning the data partition. See [Reference → System → systemd units](/docs/reference/systemd-units/) and [Reference → System → AppArmor profiles](/docs/reference/apparmor-profiles/) for the per-unit and per-profile details. ## How profiles plug in A profile is data, not code. It contributes: - A TOML manifest at `profiles/.toml` (schema in [Reference → Schemas → profile.toml](/docs/reference/profile-toml/)). - An Ansible bake recipe under `roles/deputyos/tasks/profile-.yml` that populates `/opt/deputyos/profiles//` *at build time*. - A systemd unit template, an AppArmor profile, and an offline-bake fallback stub. deputyOS itself — `deputyctl`, the wizard, the PWA, the tracker, the desktop launcher — is profile-agnostic. The third profile (Khoj) landed without a single Rust change; the cargo test count stayed at 221. See [Concepts → Plugin model](/docs/concepts/plugin-model/) for the full contract and [How-to → Add a profile](/docs/how-to/add-a-profile/) for the recipe. The rule for what counts as a profile is in [Concepts → Profile class](/docs/concepts/profile-class/). ## The shared role and variant gates The Ansible role at `roles/deputyos/` is the single source of truth for what goes into an image. Each Packer template (one per hardware target) just picks a builder; the provisioning is the same role with a *variant gate* that selects target-specific tasks. ```mermaid flowchart TB role["roles/deputyos/
shared baseline
(runtimes, security, deputyctl, profiles)"] role --> rpi5["variant-rpi5.yml
+ packer-arm rpi5"] role --> rpi4["variant-rpi4.yml
+ packer-arm rpi4"] role --> arm64["variant-arm64-generic.yml
+ packer-arm generic"] role --> x86["variant-x86_64-mini-pc.yml
+ packer qemu"] role --> wsl["variant-wsl2.yml
+ tarball"] role --> macq["variant-macos-qemu.yml
+ qcow2"] role --> cloud["variant-{do,hetzner,vultr,...}.yml
+ cloud-init / OCI"] rpi5 --> images[("CDN
signed manifest +
per-target images")] rpi4 --> images arm64 --> images x86 --> images wsl --> images macq --> images cloud --> images ``` The variant gate is `when: hw == ""` on each target-specific task file. A task that is not target-aware lives in the shared baseline. The build pipeline is documented in [Build → Image bake internals](/docs/build/image-bake-internals/); adding a new target is a five-file change documented in [How-to → Add a hardware target](/docs/how-to/add-a-hardware-target/). > **One role, many images** > The same Ansible role that runs in CI runs on a contributor's laptop > via `make build TARGET=`. macOS, Windows-WSL2, and Linux are all > first-class build hosts. There is no privileged build environment. > ## The release loop ```mermaid sequenceDiagram autonumber participant Up as Upstream
(openclaw / hermes / khoj) participant Track as deputyos-track
(GH Action) participant Repo as deputyos repo participant CI as CI matrix
(per-target build) participant Smoke as QEMU smoke
harness participant Sign as Signer
(minisign) participant CDN as CDN
(B2 + Cloudflare) participant Dev as Device
(deputyctl) loop every 30 min Track->>Up: poll latest tag end Up-->>Track: new tag observed Track->>Repo: open PR bumping profiles/.toml Repo->>CI: merge triggers build CI->>Smoke: each target boots in QEMU,
wizard answers a smoke script Smoke-->>CI: green CI->>Sign: hand artefacts to signer Sign->>CDN: upload images + manifest-v1.json + .minisig Dev->>CDN: deputyctl update --check CDN-->>Dev: signed manifest Dev->>Dev: verify minisig, download to inactive slot Dev->>Dev: deputyctl update --apply → reboot Dev->>Dev: watchdog: rollback if unhealthy ``` The signing key is offline; the minisign public key is baked into every image. Signature verification is non-skippable on the update path. The trust chain is described in detail in [Security → Update trust chain](/docs/security/update-trust-chain/). The wire format of the manifest is in [Reference → Schemas → release manifest](/docs/reference/release-manifest/). ## What's deliberately *not* in this picture - **Containers on the device.** deputyOS uses systemd, AppArmor, and the `agent` user — not Docker — to confine the gateway. Containers add indirection (data-volume bugs, networking complexity, image-pull at boot) that conflict with the zero-first-boot-network-installs invariant. - **Package upgrades on the device.** deputyOS never `apt upgrade`s itself. Upgrades are full image swaps. See [Operations → Update and rollback](/docs/operations/update-and-rollback/). - **Delta updates.** A new image is 1.5–4 GB; we accept that for the invariant. Bandwidth-conscious users can pin to LTS image revs. - **OAuth flows in the wizard.** Every supported model provider takes an API key. OAuth on a headless device is hostile; the wizard collects a key and validates it with a real round-trip. The exception is storage-bucket provisioning (Cloudflare R2), which is a storage flow, not a model-access flow. - **Multi-tenant isolation.** A device runs one active profile at a time for one user. deputyOS is not a hosting platform. ## Where to go next - [Concepts → Profile class](/docs/concepts/profile-class/) — the rule for what is and is not a profile. - [Concepts → Plugin model](/docs/concepts/plugin-model/) — how a third profile lands with zero Rust changes. - [Concepts → Threat model overview](/docs/concepts/threat-model-overview/) — the adversaries, the trust boundaries, and the layered defenses, with forward links into [Security](/docs/security/default-on-controls/). - [Reference → CLI → deputyctl](/docs/reference/deputyctl/) — the authoritative inventory of operator commands. - [Reference → APIs → message relay](/docs/reference/message-relay/) — the IPC protocol that wires the gateway to the rest of the appliance. - [Build → Image bake internals](/docs/build/image-bake-internals/) — what actually happens when CI (or your laptop) builds an image. ## Mount drives so the agent can read your files Section: how-to URL: https://www.deputyos.com/docs/how-to/mount-drives/ deputyOS gives the agent access to user files via three surfaces — all gated through a single policy file at `/etc/deputyos/mounts-policy.json`. Every mount lives under `/mnt/deputyos/` so AppArmor's per-profile rules can confine it. Every mount is reviewable + revocable from `deputyctl mounts` and from the PWA "Mounts" card. ## Surface 1: host-FS passthrough (desktop launcher / WSL2) When deputyOS runs as a VM on your laptop (via the desktop launcher → WSL2 / UTM / qemu), share a host folder into the appliance. ### Linux / macOS host (qemu / UTM, virtiofs) ```bash make try TARGET=qemu-aarch64 VIRTIOFS_SHARE=/home/me/Documents deputyctl mounts add \ --id documents \ --host-path /home/me/Documents \ --guest-path /mnt/deputyos/documents \ --mode rw ``` ### Windows host (WSL2, DrvFs) The desktop launcher detects `/mnt/c` and `/mnt/wslg` automatically and offers a per-folder picker. Behind the scenes it generates the same `deputyctl mounts add` invocation. ## Surface 2: removable drives on bare-metal Plug a USB stick or external SSD into a Pi 5 / NUC / mini-PC. With the default policy, nothing happens — auto-mount is opt-in: ```bash sudo jq '.removable.enabled = true | .removable.auto_mount = true' \ /etc/deputyos/mounts-policy.json | sudo tee /etc/deputyos/mounts-policy.json ``` Now plugging a USB stick auto-creates `/mnt/deputyos/`. The helper enforces: - `nosuid,nodev,noexec` for unknown filesystems. - `ro` by default; flip to `rw` per-device in the policy. - LUKS-encrypted volumes are refused until you `cryptsetup luksOpen` them manually. `deputyctl limits` adds a "Connected drives" section listing detected/mounted/refused devices with reasons. ## Surface 3: SMB / NFS network shares For NAS users (TrueNAS, Synology, Unraid). ```bash deputyctl mounts add \ --id photos \ --host-path "//nas.lan/photos" \ --guest-path /mnt/deputyos/photos \ --mode ro ``` The wizard's "Network shares (advanced)" tile collects credentials and writes them to `/etc/deputyos/secrets.env` (mode 0600); they are *never* recorded in the policy file. `deputyctl doctor mounts-health` pings each share and reports failures (DNS, auth, kernel-module-missing). ## Revoking access ```bash deputyctl mounts list # see what's configured deputyctl mounts remove documents # by id sudo systemctl restart deputyos-mounts.service # re-materialise ``` The PWA "Mounts" card has a one-click revoke button per mount. ## Why this is safe - Every mount is under `/mnt/deputyos/`, which AppArmor per-profile rules confine. - Mounts are explicit: an empty policy means the agent sees nothing. - Credentials live in `secrets.env`, never in the policy file (so the policy is safe to commit to backups). - Removable auto-mount is opt-in and applies safe options for unknown filesystems. ## Reporting vulnerabilities Section: security URL: https://www.deputyos.com/docs/security/reporting-vulnerabilities/ ## What this page does Tell a security researcher how to report a vulnerability to the deputyOS project, what the project promises in return, and what is out-of-scope. ## How to report Email **`security@deputyos.com`**. Include: - A clear description of the vulnerability. - Reproduction steps (an exploit script if available). - Affected deputyOS release versions, target(s), and profile(s). - Your preferred credit name (or "anonymous"). - Whether you intend to publish; if so, your intended date. PGP encryption is welcome but not required. The project's PGP key is published at `https://www.deputyos.com/.well-known/pgp-key.asc` (the `well-known` path is not yet published). > **Not yet published** > The `security@deputyos.com` inbox plus the `well-known` PGP path > are not yet published. Until then, file an issue > against the GitHub repository at `deputyos/deputyos` marked > "[security]" in the title and an explicit note that you are > requesting private disclosure. A maintainer will respond within > 72 hours with a private channel. > ## What the project promises - **Acknowledgment within 72 hours** of receiving your report. - **Status updates every 7 days** while the report is being triaged or fixed. - **Named credit** (or anonymous, your choice) in the release notes for the fixing release. - **CVE assignment** for confirmed issues affecting deputyOS code. - **Stable backport** to every supported release channel for high-severity fixes (today: only `dev` is supported; `beta` and `stable` channels are not yet published). - **Coordinated disclosure timeline** — deputyOS targets a 90-day disclosure window from acknowledgment. We will not blow past 90 days without your explicit consent. ## What the project does not promise - **No bug bounty.** deputyOS does not run a paid bounty program. - **No coverage of upstream agent vulnerabilities.** Bugs in OpenClaw / Hermes / Khoj themselves go to those projects' upstream security contacts. deputyOS is the appliance layer; the upstream agent's security model is a property of that project. We will, however, ship configuration mitigations (e.g. tightening an AppArmor profile to neutralize an upstream issue) where appropriate. - **No coverage of self-hosted infrastructure issues.** Misconfiguring a Cloudflare Tunnel, a Tailscale ACL, or your DNS is on you. - **No coverage of provider issues.** OpenAI / Anthropic / OpenRouter / Bedrock outages, key leaks at those providers, or data-handling incidents at those providers go to those providers. ## Severity guidance deputyOS uses CVSS v3.1 scoring with the following rough buckets, used for prioritization not for SLA: | Severity | Examples | | --- | --- | | Critical | Remote unauthenticated code execution; AppArmor confinement bypass leading to root | | High | Authenticated path to read `/etc/deputyos/secrets.env`; signature-bypass in update path | | Medium | Information leak from a single profile's logs; cost-cap bypass | | Low | Hardening drift in a non-default configuration | A "scaffold-only" issue (M-deferred functionality known to be incomplete and visibly labelled so) is not a vulnerability — see the roadmap. A "future-dated" issue (a vulnerability that only manifests once an M-deferred feature lands) is welcome and we'll track it toward that milestone. ## Public-disclosure timeline expectations ```text Day 0 report received Day <=3 acknowledgment + initial assessment Day <=14 fix in development OR out-of-scope decision communicated Day <=30 fix landed in `dev` channel Day <=60 fix backported to `beta` / `stable` (once those channels exist) Day 90 public disclosure (or sooner with consent) ``` If you need to disclose before day 90 because of a third-party constraint (CVE pre-disclosure, coordinated industry advisory), tell us in the report. We will work with you. ## Hall of fame Researchers who reported confirmed vulnerabilities are credited in `SECURITY.md` (published alongside the `security@deputyos.com` inbox). If you'd prefer to remain anonymous, say so in your report. ## Related - [Concepts → Threat model overview](/docs/concepts/threat-model-overview/) - [Security → Default-on controls](/docs/security/default-on-controls/) - [Security → Update trust chain](/docs/security/update-trust-chain/) - [Contributing → Overview](/docs/contributing/overview/) ## Profile class Section: concepts URL: https://www.deputyos.com/docs/concepts/profile-class/ A *profile* in deputyOS is a specific kind of thing — not any agent-shaped piece of software. This page states the rule, gives the fitting and non-fitting examples, and explains why the rule exists. ## The rule A profile in deputyOS **must** be a personal AI assistant of the OpenClaw / Hermes shape: - **Multi-channel gateway.** The assistant accepts inbound messages from multiple text-message-driven channels — Telegram, Slack, Discord, Matrix, WhatsApp, Signal, IRC, email, web chat, and so on. The agent's primary surface is a conversation, not an editor or a dashboard. - **Persistent memory across conversations.** The agent retains state beyond a single chat turn. SQLite, FTS5, embedded vector stores, plain files in a data directory — the storage choice is up to the profile; the *property* is required. - **Skill / tool system the agent can invoke.** The agent calls tools or loads skills to take actions, search the web, run code, query documents, etc. A pure passthrough to an LLM API is not a profile. A profile **must not** be: - An IDE coding agent — Aider, Continue.dev, Cursor and the like belong as IDE plugins. The shape is wrong: the surface is the editor, not a conversation; persistence is the codebase, not a memory store; tools are language-server actions, not channel handlers. - An agent framework — AutoGen, LangGraph, LangChain, CrewAI and the like are libraries for building agents, not appliances. They have no persistent identity, no first-boot UX, no channels of their own. - An ecosystem integration shim — Home Assistant, OpenHAB, MISP, n8n, and the like are platforms in their own right. They should *consume* deputyOS (e.g. a Home Assistant integration that talks to an deputyOS device on the LAN), not be a profile inside it. The rule is restated in [`CONTRIBUTING.md`](https://github.com/deputyos/deputyos/blob/main/CONTRIBUTING.md#profile-class) in the repo. PRs that propose profiles outside this class get a polite redirection rather than a merge. ## Flagship profiles OpenClaw and Hermes Agent are the two profiles deputyOS is built around. They are first-class supported, smoke-tested on every release, featured in the picker, and the default options offered in the wizard. ### OpenClaw [OpenClaw](https://github.com/openclaw/openclaw) is a Node.js personal assistant with broad messaging-channel support — Telegram, Slack, Discord, WhatsApp, Signal, iMessage / BlueBubbles, IRC, Matrix, Feishu, Line, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, WeChat, QQ, Google Chat, Microsoft Teams, and a built-in web chat. It keeps session state in SQLite under `~/.openclaw/`. It exposes a skill API the agent uses for actions. Manifest: [`profiles/openclaw.toml`](https://github.com/deputyos/deputyos/blob/main/profiles/openclaw.toml). Rationale (from the manifest): multi-channel gateway, persistent memory, skill system. Three for three. ### Hermes [Hermes Agent](https://github.com/NousResearch/hermes-agent) is a Python self-improving agent. It serves Telegram, Discord, Slack, WhatsApp, Signal, DingTalk, Twilio SMS, Mattermost, Matrix, generic webhook, IMAP/SMTP email, Home Assistant, Feishu, WeCom, and iMessage. Memory is an FTS5 SQLite session store. The skill system is self-modifying — Hermes can write new skills at runtime under an unprivileged-userns sandbox, which is why [`profiles/hermes.toml`](https://github.com/deputyos/deputyos/blob/main/profiles/hermes.toml) declares `kernel.unprivileged_userns_clone = "1"` as a required sysctl. Three for three. ## Community profile (validates the plugin model) ### Khoj [Khoj](https://github.com/khoj-ai/khoj) is a Python assistant with agent personas, document Q&A, online search, code, and image tools. Channels: web, Telegram, WhatsApp (via Twilio), Obsidian, Emacs, desktop client. Memory is SQLite + an embedded vector store under `~/.khoj/`. The skill system is agent-personas-with-tools. Manifest: [`profiles/khoj.toml`](https://github.com/deputyos/deputyos/blob/main/profiles/khoj.toml). Three for three. Khoj is **not** a flagship — it ships to prove the plugin model works. It landed as a manifest plus six bake artefacts and zero Rust changes (see [Plugin model](/docs/concepts/plugin-model/) for the acceptance test). Future community profiles follow the same pattern: a manifest, a bake recipe, an AppArmor profile, a systemd unit, a stub fallback, and one line in `tasks/main.yml`. Community profiles get smoke-tested when they land but are not gated on every release the way flagships are. ## Examples that don't fit (and why) ### Aider, Continue.dev, Cursor — IDE coding agents These tools belong inside an editor. The natural interaction surface is the codebase, not a conversation. Their state is the diff they're producing; they don't want a multi-channel gateway because the editor *is* the channel. Putting them in deputyOS would require shoehorning them into a shape they were not designed for — and would still not give the user what they want, which is "an AI in my IDE." ### AutoGen, LangGraph, CrewAI, LangChain — agent frameworks These are libraries you build with, not products you install. They have no first-boot UX of their own, no channels of their own, no persistent identity that survives a process restart. Profiles built *with* these frameworks are fine — Hermes is in fact a library-built agent — but the framework itself is not a profile. ### Home Assistant, OpenHAB, n8n — ecosystem platforms These are platforms in their own right with rich UIs, plugin ecosystems, and operational expectations. The right relationship is "Home Assistant talks to an deputyOS-hosted assistant over the network," not "Home Assistant runs as a profile on deputyOS." If we made Home Assistant a profile we would inherit its scope creep — automations, dashboards, add-ons, HACS — none of which is a personal AI assistant. ### Pure LLM clients — Ollama Web UI, OpenWebUI, LibreChat These are chat clients that talk to an LLM endpoint. They don't have multi-channel gateways (browser only), they don't have a persistent memory beyond chat history, and they don't have a tool system the agent calls — the user calls the tools by typing. They are perfectly good software in their category; that category isn't "personal AI assistant appliance." ### Coding sandboxes, RPA tools, scrapers If the primary user interaction is "click a button, watch the agent do a thing in a browser," it's a different category. deputyOS profiles are conversational — the user talks, the agent answers and acts. ## Why the rule Three reasons: 1. **Coherence of the appliance shape.** deputyOS is a single-purpose appliance: "a personal AI assistant on a Pi / mini-PC / cloud VM." That sentence sets every operational expectation — the wizard, the PWA, the cost guardrails, the channel allowlist, the message relay, the limits surface, the AppArmor profile shape. Stretching the definition stretches all of those, and the result is a worse experience for everything. 2. **Reviewability.** The profile-class rule lets a reviewer say "yes / no" to a profile PR in minutes. Without it every proposal becomes an architecture argument. With it, the question is concrete and answerable: does this thing have a multi-channel gateway, does it persist memory, does it have a skill system? 3. **Trust at the picker.** A user looking at deputyos.com sees three profiles today and might see ten next year. Every one of them is the same kind of thing. The user can choose by upstream and by feature set, not by "wait, is *this* one even an assistant?" The rule is intentionally narrow. It is not "any agent." It is "the OpenClaw / Hermes shape." If your project does not fit and you think we got it wrong, open a discussion issue first — the [CONTRIBUTING guide](/docs/contributing/overview/) has the path. ## What you can do with a fitting profile Once a profile fits, the [plugin model](/docs/concepts/plugin-model/) gives it: - A baked place under `/opt/deputyos/profiles//` (no install at boot). - A first-boot wizard step driven by the manifest's `[wizard].prompts`. - An AppArmor profile baked at `/etc/apparmor.d/deputyos.`. - A systemd unit started by [`deputyctl up`](/docs/reference/deputyctl/). - Channel toggles surfaced through the [PWA](/docs/reference/pwa-http/). - Hooks routed through the [message relay](/docs/reference/message-relay/). - Cost ledger entries tied to model calls (see [Operations → Cost guardrails](/docs/operations/cost-guardrails/)). - Backup/restore of the data directory via [`deputyctl backup`](/docs/reference/deputyctl/). - Update / rollback / factory-reset semantics inherited automatically. A profile gets all of this without writing any deputyOS-specific code — just a manifest, a bake recipe, an AppArmor profile, a systemd unit template, and an offline-bake fallback stub. The practical recipe lives in [How-to → Add a profile](/docs/how-to/add-a-profile/). ## Where to go next - [Concepts → Plugin model](/docs/concepts/plugin-model/) — the contract that makes a profile a drop-in. - [How-to → Add a profile](/docs/how-to/add-a-profile/) — the worked recipe, six files, no Rust changes. - [Reference → Schemas → profile.toml](/docs/reference/profile-toml/) — every field of the manifest. - [Contributing → Overview](/docs/contributing/overview/) — how a profile PR moves through review. ## Air-gapped tier Section: concepts URL: https://www.deputyos.com/docs/concepts/airgap/ The air-gapped tier is the truest expression of the *batteries-included* rule. The image works without ever reaching the public internet: - `apt` sources point at `/opt/deputyos/airgap/apt-mirror/`, a baked-in Debian/Raspbian snapshot frozen at image-bake time. - `nftables` denies all egress except RFC1918 + mDNS + the local DNS resolver. Outbound chats with `api.openai.com` / `api.anthropic.com` /etc. are blocked. - A tier-appropriate **LFM2** GGUF is baked under `/var/lib/deputyos/models/` and served by `llama.cpp` on `127.0.0.1:8090`. Wizard + chat default to it. ## Tier sizes | Tier | Default LLM | Compressed image | |----------|--------------------------------------------|------------------| | lean | `LFM2-350M.Q4_K_M.gguf` (~250 MB) | ~1.3 GB | | standard | `LFM2-1.2B.Q4_K_M.gguf` (~750 MB) | ~2.8 GB | | rich | `LFM2-2.6B.Q4_K_M.gguf` (~1.6 GB) + `Qwen2.5-Coder-1.5B-Instruct.Q4_K_M.gguf` (~1.0 GB) | ~6.5 GB | Exact numbers in `docs/12-bundled-software.md` §5. ## Building ```bash make build TARGET=qemu-x86_64 PROFILE=openclaw TIER=rich AIRGAP=1 ``` `AIRGAP=1` plumbs through Packer (`-var airgap=1`) into the role (`deputyos_airgap: true`). When false (the default), every airgap-related task is a no-op so non-airgap targets pay zero baking cost. ## Unlocking the network post-boot Air-gapped is the *posture*, not the *prison*. From the device: ```bash deputyctl network unlock # flip mode=airgap → mode=open, reload nftables ``` The wizard surfaces the same toggle. Going back the other way (`deputyctl network lock --airgap`) is also supported. A middle mode — `whitelist` — is reserved for a future release and shares the same `/etc/deputyos/network-policy.json` schema. ## Updates over sneakernet `deputyctl update --from /mnt/deputyos//manifest.json` consumes a manifest dropped onto a USB stick. The signature path is unchanged: minisign + cosign + SLSA verification still gate the apply step. The device just doesn't fetch the bytes — the user does. ## Caveats - The baked apt mirror is frozen at image bake. Security patches arrive via signed image rebuilds, not in-place package upgrades. - Cloud-API LLM providers are unreachable in airgap mode by design. The baked LFM2 (and any GGUF you `deputyctl model register` later) is the only path. - Pi 4 + airgap is supported only in the `lean` tier — the SoC can't run 1.2B at usable t/s. ## Threat Model — Overview Section: concepts URL: https://www.deputyos.com/docs/concepts/threat-model-overview/ This page is the short orientation. The depth is in the [Security](/docs/security/default-on-controls/) section, which enumerates each default-on control, the secrets-storage contract, and the update trust chain. Read this page first, then follow the cross-links. ## Adversaries we model deputyOS images run in a few well-defined places — a Pi on a home LAN, a mini-PC in a closet, a $5 cloud VM behind a single public IP, a desktop launcher on a developer laptop. The threats we design against, and that the [default-on controls](/docs/security/default-on-controls/) are sized for, are: - **Opportunistic LAN-side attackers.** Anyone else on the same Wi-Fi or VLAN as the device — a hostile guest, a compromised IoT camera, a laptop on a co-working network. They can probe ports, attempt brute force on SSH, and try to reach the wizard or the PWA. - **Opportunistic internet-side attackers**, when the device is exposed by user choice (a tunnel, a public IP, a Fly.io machine). They cannot reach the device at all by default; if the user has set up a tunnel they can reach exactly the surface the user opened. - **Supply-chain attacks on upstream agents.** A compromised `openclaw`, `hermes`, or `khoj` release would make its way through the [release loop](/docs/concepts/architecture/#the-release-loop) unless we catch it. We catch it by pinning upstream tags, by ClamAV / Magika scans during bake, by signing what we ship, and by giving the user a third-party way to verify (`make verify VERSION=`). - **Malicious skills and agent-generated code.** Hermes can write skills at runtime; agents can be talked into running tools the user did not intend. The defenses are the AppArmor profile, the unprivileged-userns sandbox Hermes uses for command execution, and the cost ledger / quiet hours that bound runaway tool calls. - **Compromised model-provider credentials.** A leaked OpenRouter or Anthropic key is the most common real-world incident. The defenses are mode 0600 on `secrets.env`, key rotation via `deputyctl model set`, cost caps that blunt blast radius, and CostAlert hooks that surface unusual spend immediately. ## Trust boundaries The system has a small number of distinct trust layers; understanding where they sit makes the rest of the security documentation easier to read. | Layer | What it protects | Who enforces it | |---|---|---| | **Signed manifest (cryptographic root)** | The integrity of every image and the legitimacy of every update. | minisign verification in `deputyctl update --check` and the desktop launcher. Public key baked into every image. See [Security → Update trust chain](/docs/security/update-trust-chain/). | | **Image bake (build-time integrity)** | What ends up at `/opt/deputyos/profiles//`, `/var/lib/clamav/`, `/var/cache/deputyos/`. | The CI pipeline; reproducibility via `make verify VERSION=`. SBOM at `/etc/deputyos/sbom.json`. | | **AppArmor (process)** | What each gateway process can read, write, exec, and connect to. | The kernel, with profiles at `/etc/apparmor.d/deputyos.`. See [Reference → System → AppArmor profiles](/docs/reference/apparmor-profiles/). | | **ufw (network)** | What inbound traffic the device accepts. Default-deny; allow rules per channel and for the LAN. | The kernel netfilter chain. See [Security → Default-on controls](/docs/security/default-on-controls/). | | **`agent` user (filesystem)** | The data partition is owned by `agent`, not root. The gateway runs as `agent`. | POSIX permissions; systemd `User=agent`. | | **`secrets.env` (mode 0600)** | Provider keys, tunnel tokens, backup credentials. Readable only by root and (via systemd credential forwarding) the gateway. | POSIX permissions, systemd. See [Security → Secrets storage](/docs/security/secrets-storage/). | | **`deputyctl` operator surface** | Every command is non-destructive by default; destructive ones (`factory-reset`, `rollback`, `update --apply`) prompt unless `--yes`. | The CLI itself; documented in [Reference → CLI → deputyctl](/docs/reference/deputyctl/). | Each layer assumes the layers below it are intact. A kernel-level compromise breaks AppArmor and the user-id boundary; a manifest-signing-key compromise breaks every layer below it. ## What's not in scope Documenting limits explicitly is part of the [awareness-of-limitations principle](/glossary/limits-awareness-principle/). The following are *not* threats deputyOS attempts to defend against, and the user should know that: - **Physical attacks.** A person with the device in their hands can swap the SD card or NVMe, attach a JTAG, or read the data partition. deputyOS does not enable full-disk encryption by default; opt-in encryption is a deferred milestone. - **Kernel zero-days.** AppArmor is a confinement, not a hypervisor. A kernel bug that lets a process escape AppArmor breaks our process boundary. We patch via image revs as upstream patches land. - **Side-channel attacks on shared cloud hosts.** A neighbour on a shared Hetzner or Vultr host running Spectre-class attacks is out of scope; the cloud provider owns that boundary. - **Compromise of the user's model-provider account by other means.** If your OpenRouter account is breached upstream, your key on the device is irrelevant. - **Covert tracking by the user's ISP.** DNS, NTP, and provider HTTPS traffic are visible to the network. deputyOS does not route through Tor or a VPN by default. - **Targeted nation-state adversaries.** This system is sized for the threats a self-hosting hobbyist faces, not for high-stakes high-resource attackers. ## Layered defenses summary The full enumeration is in [Security → Default-on controls](/docs/security/default-on-controls/); the table below is the orientation map. | Layer | What it protects | How to verify | |---|---|---| | Signed manifest | Every download and update | `deputyctl version` shows the manifest signature and the public key fingerprint; `deputyctl update --check` re-verifies on every update. See [Security → Update trust chain](/docs/security/update-trust-chain/). | | Reproducible bake | What's actually in the image | `make verify VERSION=` rebuilds the image and asserts the SHA256 matches. SBOM at `/etc/deputyos/sbom.json`. | | AppArmor enforce | Process-level confinement | `aa-status` shows every profile in enforce mode. Doctor verifies on boot. See [Reference → System → AppArmor profiles](/docs/reference/apparmor-profiles/). | | ufw default-deny | Inbound network surface | `ufw status verbose` lists the deny-by-default chain and per-channel allows. | | fail2ban | Brute-force resistance on SSH and the wizard | `fail2ban-client status` shows the active jails. | | Hardened sysctls | Kernel attack surface | `/etc/sysctl.d/90-deputyos.conf` is the source of truth. Doctor verifies the values are applied. | | ClamAV + Magika | Inbound file scanning | `clamdscan --version` (or `clamscan` on RAM-constrained targets); Magika is invoked by the gateway pre-write. | | `agent` user | Filesystem isolation | `id agent`, `ps -u agent`. The gateway is never run as root. | | `secrets.env` mode 0600 | Provider key isolation | `stat -c '%a %U' /etc/deputyos/secrets.env` returns `600 root`. | | Key-only SSH | Remote-access posture | `sshd -T \| grep -E '^(passwordauthentication\|permitrootlogin)'`. | | ZRAM + cgroup limits + systemd-oomd | Graceful pressure response | `swapon`, `oomctl status`, journal entries. See [Operations → Monitoring and logs](/docs/operations/monitoring-and-logs/). | | Cost guardrails | Bounded financial blast radius | `deputyctl cost` shows the ledger; CostAlert hooks fire on threshold breaches. See [Operations → Cost guardrails](/docs/operations/cost-guardrails/). | ## Where to go next - [Security → Default-on controls](/docs/security/default-on-controls/) — every default-on control, in detail. - [Security → Secrets storage](/docs/security/secrets-storage/) — the `/etc/deputyos/secrets.env` contract, how it survives updates, what factory-reset does to it. - [Security → Update trust chain](/docs/security/update-trust-chain/) — the minisign trust root, the CDN URL contract, and the third-party verification path. - [Security → Reporting vulnerabilities](/docs/security/reporting-vulnerabilities/) — the private-disclosure path and the SLA we promise. - [Concepts → Architecture](/docs/concepts/architecture/) — the system view that this threat model is layered onto. ## Hardware matrix Section: distribution URL: https://www.deputyos.com/docs/distribution/hardware-matrix/ ## What this page does Enumerate every distribution channel deputyOS ships — what gets built locally, what gets baked as a cloud snapshot, what gets distributed as a community template, and what gets driven from your laptop via the desktop launcher. For each row: what the artefact is, the acceleration shape, the RAM tier, whether you can build it locally, whether CI smoke-tests it, and the relevant per-row notes. The matrix is the surface answer to "where can I run deputyOS?" Today the answer is **17 targets** — nine via local Packer build (`qemu-aarch64`, `qemu-x86_64`, `rpi5`, `rpi4`, `arm64-generic`, `x86_64-mini-pc`, `digitalocean`, `oracle-arm-free`, `wsl2`), three via cloud-init recipe (`hetzner-cloud`, `vultr`, `linode`), one OCI artefact (`fly-machines`), three community templates (`proxmox`, `unraid`, `truenas`), and one desktop-launcher wrapper (`macos-qemu`). Two of those — `qemu-aarch64` and `qemu-x86_64` — are CI smoke-test targets rather than production ones, so **15 are production-ready**. That is the number to quote externally. The desktop launcher separately builds for three host platforms (Linux, Windows, macOS); those are launcher binaries, not appliance targets, and are not counted above. ## The full matrix | Target | Format | Build kind | Acceleration | RAM tier | Local-buildable | Smoke in CI | Notes | | --- | --- | --- | --- | --- | --- | --- | --- | | `qemu-aarch64` | qcow2 | Packer / qemu | KVM (host arm64) or TCG | standard | yes | yes | smoke-test target; not for production | | `qemu-x86_64` | qcow2 | Packer / qemu | KVM (host x86) or TCG | standard | yes | yes | smoke-test target; not for production | | `rpi5` | img.xz | Packer / pi-gen | native | low / standard / high | yes (chroot) | no | recommended Pi target; needs binfmt_misc | | `rpi4` | img.xz | Packer / pi-gen | native | low | yes (chroot) | no | constrained; on-demand ClamAV; no voice/local-LLM | | `arm64-generic` | img.xz | Packer | native | varies | yes (chroot) | no | Orange Pi / Rock Pi / generic arm64 SBCs | | `x86_64-mini-pc` | img.xz | Packer / qemu | KVM | standard / high | yes | no | mini-PCs, NUCs; same template as qemu-x86_64 with raw+xz post-process | | `digitalocean` | DO snapshot | Packer / DO API | provider-managed | standard | yes (needs `DIGITALOCEAN_TOKEN`) | no | $DIGITALOCEAN_TOKEN required | | `oracle-arm-free` | qcow2 | Packer | TCG | standard | yes (manual upload to OCI) | no | matches Oracle's free-tier Ampere shape | | `wsl2` | tar.gz | Packer | WSL2 | standard | yes (Linux/WSL2 host) | no | Windows users import via `wsl --import` | | `hetzner-cloud` | cloud-init recipe | recipe-only | provider | standard | n/a | no | paste YAML into Hetzner's User-Data | | `vultr` | cloud-init recipe | recipe-only | provider | standard | n/a | no | paste YAML into Vultr's startup script | | `linode` | cloud-init recipe | recipe-only | provider | standard | n/a | no | paste YAML into Linode's StackScript | | `fly-machines` | OCI image | buildah / docker | container | standard | yes (buildah/docker on PATH) | no | `flyctl deploy` to push | | `proxmox` | template + qcow2 | community template | KVM (Proxmox host) | standard | n/a (wraps qemu-x86_64) | no | follow `templates/proxmox/README.md` | | `unraid` | template + qcow2 | community template | KVM | standard | n/a | no | follow `templates/unraid/README.md` | | `truenas` | template + qcow2 | community template | bhyve | standard | n/a | no | follow `templates/truenas/README.md` | | `macos-qemu` | qcow2 | wrapper around qemu-aarch64 | TCG (host x86_64) or HVF (host arm64) | standard | yes | no | run via `macos/run-utm.sh` or `macos/run-orbstack.sh` | | desktop launcher / Linux | bin | local cargo | native | host | yes | no | downloads + drives qemu+KVM | | desktop launcher / Windows | bin (cross) | cross-compile | host WSL2 | host | yes | no | mandates WSL2 | | desktop launcher / macOS | bin (cross) | cross-compile | host UTM | host | yes (on Mac) | no | mandates UTM (Apple Silicon recommended) | ## Categories ### Build targets — real Packer `qemu-aarch64`, `qemu-x86_64`, `rpi5`, `rpi4`, `arm64-generic`, `x86_64-mini-pc`, `digitalocean`, `oracle-arm-free`, `wsl2`. `make build TARGET=` produces a local artefact. Lint and `packer validate` work without provider creds; real builds for cloud targets need creds (see [Build → Make targets](/docs/build/make-targets/)). ### Cloud-init recipes — no build `hetzner-cloud`, `vultr`, `linode`. These are not local builds. `make build TARGET=hetzner-cloud` prints the YAML recipe and exits 0. The user pastes the YAML into the provider's User-Data field at instance creation. Until the CDN goes live, the recipes contain `<<>>` placeholders — the recipe is a contract, not a runnable first-boot script. The CDN rollout fills the placeholder. ### Container — OCI `fly-machines`. `make build TARGET=fly-machines` produces an OCI image via buildah (preferred — rootless, daemonless) or Docker. The user runs `flyctl deploy` to push to Fly's registry. The Containerfile is at `fly/Containerfile`. ### Community templates — manual import `proxmox`, `unraid`, `truenas`. These wrap an existing qcow2 (built from `qemu-x86_64` or `qemu-aarch64`). The template files at `templates//` plus that provider's README walk through the import. deputyOS does not build these directly; the user follows the README. ### Desktop launcher — download + run locally The `deputyos-desktop` crate produces a Rust binary per platform that consumes existing image artefacts. See [Desktop launcher internals](/docs/distribution/desktop-launcher-internals/). `macos-qemu` (a special-case wrap of `qemu-aarch64`) is the qcow2 the macOS launcher boots; the launcher itself is `deputyos-desktop` built for macOS. ## Picking a target | If you have… | Pick | | --- | --- | | A Pi 5 with 8GB+ RAM | `rpi5` | | A Pi 5 with 4GB RAM | `rpi5` (low tier; voice off) | | A Pi 4 (any RAM) | `rpi4` | | An Orange Pi / Rock Pi / generic arm64 SBC | `arm64-generic` | | An Intel NUC / x86 mini-PC | `x86_64-mini-pc` | | A laptop and you want to evaluate quickly | desktop launcher | | A DigitalOcean droplet | `digitalocean` | | An Oracle free-tier Ampere VM | `oracle-arm-free` | | A Hetzner / Vultr / Linode VPS | the matching cloud-init recipe | | A Proxmox / Unraid / TrueNAS host | the matching community template | | Fly.io | `fly-machines` | | WSL2 on Windows | `wsl2` | ## Per-row notes ### `rpi5` The flagship Pi target. Three RAM tiers (4 GB, 8 GB, 16 GB); 8 GB+ is required for voice and local-LLM features. tryboot is the planned update mechanism (planned). ### `rpi4` Conservative single-tier (`low`). 4 GB RAM, Cortex-A72, no daemon ClamAV (on-demand timer instead), no voice, no local LLM, U-Boot bootcount A/B (planned). USB3 SSD recommended over SD for sustained I/O. ### `arm64-generic` Generic arm64 SBCs (Orange Pi 5, Rock Pi 4/5, etc.). Inherits rpi4-class limitations except where the board has ≥8 GB RAM. Stock Debian arm64 kernel; no per-board tuning baked in (the user picks the right device-tree post-flash). ### `x86_64-mini-pc` Same Packer template as `qemu-x86_64` with a `qcow2 → raw → xz` post-processor. Targets passively-cooled or mid-range mini-PCs (Intel N100, AMD Ryzen embedded). ### `wsl2` The Windows path. The output is a `.tar.gz` the user imports with `wsl --import`. systemd works under WSL2 (with `[boot] systemd=true`); ufw is more limited because the WSL2 kernel doesn't expose every nf_conntrack tunable. The PowerShell installer at `wsl/Install-DeputyOS.ps1` automates the import. ### `macos-qemu` A wrapper. The qcow2 the macOS launcher boots is the `qemu-aarch64` qcow2 — there's no separate Packer template. `scripts/build.sh TARGET=macos-qemu` recursively dispatches to `TARGET=qemu-aarch64` and copies the output. Run via `macos/run-utm.sh` (recommended) or `macos/run-orbstack.sh`. ### `fly-machines` Single-VM persistent app on Fly.io. The Containerfile expects `DEPUTYOS_PROFILE` and `DEPUTYOS_CHANNEL` build args. State persistence needs a Fly volume (see `fly/fly.toml.example`). No A/B updates — `fly deploy` is the update mechanism. ### `proxmox` / `unraid` / `truenas` Community-contributed templates. The qcow2 ships from the QEMU build; the per-provider XML / JSON / VM-config defines the import shape. The deputyOS team maintains the templates but does not run a Proxmox / Unraid / TrueNAS host for end-to-end smoke testing — community testing is the source of truth. ## Per-target capability map (the limits) For the per-target capability flags (`local_llm`, `voice_wake_word`, `voice_tts`, `clamav_daemon`, …), see [Reference → Schemas → limits.json](/docs/reference/limits-json/) and the per-target `roles/deputyos/files/limits..json`. The wizard reads `/etc/deputyos/limits.json` at first boot and filters every relevant choice list. ## Future targets Tracked in `docs/11-roadmap.md`: - **`pi3-zero-2w`** — explicitly out of scope. Pi 3 / Pi Zero 2W cannot run AppArmor cleanly with our gateway profiles. - **`riscv64`** — speculative; depends on stable Debian RISC-V release. - **`bare-metal-arm64-laptop`** — Lenovo X13s and friends. Possible with mainline-only kernel; tracked but not committed. ## Related - [Build → Make targets](/docs/build/make-targets/) - [Build → Image bake internals](/docs/build/image-bake-internals/) - [Distribution → Desktop launcher internals](/docs/distribution/desktop-launcher-internals/) - [Reference → Schemas → limits.json](/docs/reference/limits-json/) - [How-to → Add a hardware target](/docs/how-to/add-a-hardware-target/) ## Contributing — Overview Section: contributing URL: https://www.deputyos.com/docs/contributing/overview/ This page is the contributor's tour. It folds the repo's [`CONTRIBUTING.md`](https://github.com/deputyos/deputyos/blob/main/CONTRIBUTING.md) into a fuller guide for someone arriving from outside the project. If you have not opened a PR against deputyOS before, read this end-to-end once before you start writing code. ## Before you start - **Read the architecture.** [Concepts → Architecture](/docs/concepts/architecture/) is the shortest path to a working mental model. The rest of this site is much easier to navigate once that page has been read. - **Read the profile-class rule.** Whether your change is a profile or not, [Concepts → Profile class](/docs/concepts/profile-class/) explains the kind of system this is — and isn't. A surprising number of proposals get a polite redirection because they are asking deputyOS to be a category of thing it is not. - **Look at recent issues and PRs.** If your topic is already in motion, save yourself the duplicate effort. If a similar PR was recently closed without merge, the closing comment usually has the reason and what would unblock the same idea. - **Open an issue first for non-trivial changes.** Architectural changes, new dependencies, new hardware targets, new milestones, and new ADRs all benefit from agreement on the approach before code lands. A typo fix or a clearly scoped bug fix can go straight to PR. > **The load-bearing invariant** > The single non-negotiable design rule is **zero first-boot network > installs**. Any change that introduces an `apt`, `npm`, `pip`, > `cargo`, or `git clone` step on the booted device — even an > optional one, even gated behind a flag — will be rejected. See > [Concepts → Architecture](/docs/concepts/architecture/) for the > rationale and [`docs/adr/0002-zero-first-boot-network-installs.md`](https://github.com/deputyos/deputyos/blob/main/docs/adr/0002-zero-first-boot-network-installs.md) > in the repo for the design history. > ## The development loop You don't need a Pi. You need a laptop: ```sh git clone https://github.com/deputyos/deputyos.git cd deputyos make doctor # checks tools; tells you what to install cargo test --all # 221 tests; the baseline make try TARGET=qemu-aarch64 # ~12 min on a fresh checkout # wizard at http://localhost:8088 ``` The same Ansible role CI uses runs on your laptop — macOS (Apple Silicon and Intel), Windows (WSL2), and Linux (x86_64 and arm64) are all first-class build hosts. There is no privileged build environment. Per-host specifics are in [Build → Image bake internals](/docs/build/image-bake-internals/), and [Build → Make targets](/docs/build/make-targets/) is the inventory of every Make target. The iteration loop: 1. Edit (Rust, Ansible, Markdown — whatever your change touches). 2. `cargo test --all` if Rust changed. 3. `make build TARGET=qemu-aarch64` if the image needs to change. 4. `make try TARGET=qemu-aarch64` to boot the new image and exercise it. 5. `make smoke` to run the QEMU smoke harness. 6. `make ci SCAFFOLD_PHASE=1` to re-run the same checks CI runs. 7. Commit, push, open a PR. ## Workspace structure deputyOS is a five-crate Cargo workspace plus a shared Ansible role plus per-target Packer / cloud-init artefacts. The crates are: | Crate | Purpose | Reference | |---|---|---| | `deputyctl` | The operator CLI. Reads profile manifests, drives systemd, validates keys, performs updates, runs backups, prints capability limits. The single binary that owns the management surface. | [Reference → CLI → deputyctl](/docs/reference/deputyctl/) | | `deputywizard` | First-boot HTTP server on `:8088`. Writes `/etc/deputyos/secrets.env`. Profile-aware, profile-agnostic. | [Reference → CLI → deputywizard](/docs/reference/deputywizard/), [Reference → APIs → wizard HTTP](/docs/reference/wizard-http/) | | `deputypwa` | Always-on web dashboard and built-in private chat client. Replaces the wizard on `:8088` after first-boot. | [Reference → CLI → deputypwa](/docs/reference/deputypwa/), [Reference → APIs → PWA HTTP](/docs/reference/pwa-http/) | | `deputyos-track` | Release-tracker bot. Polls upstreams every 30 minutes, opens PRs that bump pinned profile versions. | [Reference → CLI → deputyos-track](/docs/reference/deputyos-track/) | | `deputyos-desktop` | Desktop launcher (~5 MB). Mandates the platform-native hypervisor — WSL2 on Windows, UTM on macOS, qemu+KVM on Linux. Downloads the latest signed image and boots it. | [Reference → CLI → deputyos-desktop](/docs/reference/deputyos-desktop/), [Distribution → Desktop launcher internals](/docs/distribution/desktop-launcher-internals/) | Outside the Rust workspace: - `roles/deputyos/` — the shared Ansible role. The single source of truth for what goes into an image. Variant gates per hardware target. See [Build → Image bake internals](/docs/build/image-bake-internals/). - `packer/` — Packer templates, one per buildable hardware target. - `cloud-init/` — `cloud-init` recipes for cloud targets that don't get a baked image (Hetzner, Vultr, Linode). - `wsl/`, `macos/` — host-specific tarball / qcow2 packagers. - `templates/` — community templates (Proxmox, Unraid, TrueNAS). - `fly/` — Fly.io OCI artefact. - `test/` — QEMU smoke harness fixtures. - `profiles/` — the three profile manifests. - `dist/` — release-pipeline outputs, gitignored. ## Adding things The recipes for the most common contributions are in How-to: - [How-to → Add a profile](/docs/how-to/add-a-profile/) — six files, zero Rust changes. The reference is the Khoj PR. - [How-to → Add a hardware target](/docs/how-to/add-a-hardware-target/) — five files: variant task, Packer template, smoke fixture, `limits.json` row, `main.yml` import. - [How-to → Add a hook](/docs/how-to/add-a-hook/) — drop a script in `/etc/deputyos/hooks.d//`; payload schemas are in [Reference → Schemas → hook payloads](/docs/reference/hook-payloads/). - [How-to → Add a model provider](/docs/how-to/add-a-model-provider/) — extend `deputyctl/etc/providers.json`. - [How-to → Add a channel](/docs/how-to/add-a-channel/) — channel registry, ufw rule, AppArmor mediation. What we accept (the short list): new profiles in the [profile class](/docs/concepts/profile-class/), new hardware variants, bug fixes to the workspace crates or the build pipeline, doc fixes, new ADRs documenting architectural decisions we missed. What we don't accept: profiles outside the class, changes that re-introduce first-boot network installs, runtime dependencies without arm64 + x86_64 prebuilt binaries, features that expose the agent to the public internet by default (opt-in is fine), and code that bypasses signature verification on the update path. ## Code style | Language | Tool | Settings | |---|---|---| | Rust | `rustfmt` | Defaults. Run with `cargo fmt --all`. | | Rust | `clippy` | `cargo clippy --all-targets --all-features -- -D warnings`. Warnings are errors in CI. | | Ansible | `ansible-lint` | At the production profile. Run with `make lint`. | | YAML | `yamllint` | 2-space indent. Configured via `.yamllint`. | | Shell | `shellcheck` | All scripts in `roles/deputyos/files/` and `scripts/`. | | Markdown | (no auto-formatter) | No trailing whitespace; hard-wrap at 100 cols inside paragraphs of prose; use mkdocs-material admonitions where useful. | | Commit messages | (no enforced format) | Imperative mood. Short, specific PR titles. Conventional-commit prefixes are welcome but not required. | The single rule: no commits land without `make ci` green. CI is the contract; if CI is wrong, fix CI in the same PR. ## Testing | Surface | What to run | Where to add tests | |---|---|---| | Rust units / integration | `cargo test --all` (221-test baseline) | Adjacent `mod tests` blocks; `tests/` for cross-crate integration; `deputyctl/tests/` for CLI shape tests. | | Rust public API | `cargo doc --all` builds clean | Doc-comment examples are run; keep them minimal and meaningful. | | Ansible role | `ansible-lint` and `make lint` | If you add a task, add an `assert` or a `command` check that proves the post-condition. | | QEMU smoke | `make smoke` | `test/qemu/.cloudinit.yaml` per target; `test/smoke/.sh` for behaviour. | | Provider library | `cargo test -p deputyctl model::test_provider_key` | Stub-server integration test for every new provider. | | Schema validation | The schema files live in `deputyctl/etc/`. | Add a fixture under `deputyctl/tests/fixtures/` that exercises the new field. | A profile PR must include a smoke fixture that boots an image with the profile selected, runs the wizard, validates that the gateway comes up healthy, and tears down. A hardware-target PR must include both a smoke fixture and a `limits.json` entry; the latter feeds the picker page and the `deputyctl limits` output. The 221-test baseline is the floor — adding a feature without adding tests is a signal something is wrong, not a sign the feature is small. ## ADRs Architecture Decision Records live at `docs/adr/` in the repo. There are eight today (numbered `0001` through `0008`). The numbering is sequential; the next ADR is `0009`. Write an ADR when: - You are making an *architectural* change — something that ripples across more than one crate, or that future contributors will want to understand the *why* of, not just the *what*. - You are making a security decision — adding a default-on control, changing a trust boundary, picking a crypto primitive, opting out of a hardening feature for a reason. - You are *changing direction* — replacing a previously-decided approach with a new one. The new ADR supersedes the old one and should reference it explicitly. You do not need an ADR for: a routine bug fix, a refactor that preserves behaviour, a new test, a doc improvement, a profile addition inside the existing class, or a hardware-target addition that uses the existing variant-gate pattern. The format is short — a problem statement, the alternatives considered, the decision, and the consequences. Pattern-match on the existing eight ADRs. ## The PR flow 1. **Open an issue first** for anything non-trivial. State the goal, the alternatives you considered, and the rough shape of the change. Get a thumbs-up from a maintainer before you spend a weekend on it. 2. **Branch.** No enforced naming; `feature/` or `fix/-` is conventional. 3. **Implement.** Keep PRs focused. A 2,000-line PR that touches `deputyctl`, the wizard, and the Ansible role is harder to review than three 700-line PRs each focused on one surface. 4. **Test locally.** `make ci SCAFFOLD_PHASE=1` is the same set of checks CI runs. If it's red on your laptop, it's red in CI. 5. **Open the PR.** Reference the issue. State what you did and what you didn't do. If the change touches a documented surface, update this site (the page you're touching most likely lives under [Reference](/docs/reference/deputyctl/) or [Concepts](/docs/concepts/architecture/)). 6. **Review.** Reviewers will land on the PR within a few days. Expect feedback; expect to iterate. 7. **Merge.** Squash-merge is the default. The PR title becomes the commit message; keep it specific. > **Don't forget the docs site** > If your PR adds a CLI flag, a TOML field, an HTTP route, or a > systemd unit, the matching page in this site needs to be updated > in the same PR. `mkdocs build --strict` is part of `make ci`, so a > broken cross-link will fail your build. > ## Cross-references - [Concepts → Architecture](/docs/concepts/architecture/) — the system view. - [Concepts → Profile class](/docs/concepts/profile-class/) — what qualifies as a profile. - [Concepts → Plugin model](/docs/concepts/plugin-model/) — the contract that makes a profile a drop-in. - [Build → Make targets](/docs/build/make-targets/) — every Make target, what it does, what it produces. - [Build → Image bake internals](/docs/build/image-bake-internals/) — pi-gen, Packer, the Ansible role, variant gates. - [Reference → CLI → deputyctl](/docs/reference/deputyctl/) — the authoritative command surface. - [Security → Reporting vulnerabilities](/docs/security/reporting-vulnerabilities/) — the private-disclosure path. **Vulnerability reports do not go through public issues.** ## Code of conduct Be kind, especially to people newer than you. deputyOS is built for semi-technical users — and that includes contributors, sometimes. Patient review and clear writing are first-class contributions on their own. ## Plugin Model Section: concepts URL: https://www.deputyos.com/docs/concepts/plugin-model/ deputyOS's plugin model is a contract: a third profile lands in the appliance as **a manifest plus a bake recipe plus a systemd unit template plus an AppArmor profile plus an offline-bake fallback** — and zero changes to the Rust code. This page explains the contract and what makes it possible. ## The promise A new profile (in the [profile class](/docs/concepts/profile-class/)) needs the following six artefacts in the repo, and *nothing else*: 1. **`profiles/.toml`** — the manifest. 2. **`roles/deputyos/tasks/profile-.yml`** — the Ansible bake recipe. 3. **`roles/deputyos/templates/-gateway.service.j2`** — the systemd unit template. 4. **`roles/deputyos/files/apparmor/deputyos.`** — the AppArmor confinement profile. 5. **`roles/deputyos/files/-stub.sh`** — the offline-bake fallback stub (used in offline / sandboxed builds where the upstream cannot be fetched). 6. **One line in `roles/deputyos/tasks/main.yml`** — `import_tasks: profile-.yml` to make the build pipeline pick the new recipe up. That is the entire contract. No Rust changes. No schema migrations. No new wizard step types. No new HookKind variants. > **If you find yourself editing Rust** > If a profile PR needs to change `deputyctl`, the wizard, the PWA, > the tracker, or the desktop launcher, that is a signal that either > (a) the profile does not fit the [profile class](/docs/concepts/profile-class/) > and is asking for a special case, or (b) deputyOS itself needs an > extension that *every* profile would benefit from. The right move > is to split the PR — propose the deputyOS extension separately, > with its own ADR. See [Contributing → Overview](/docs/contributing/overview/) > for the ADR process. > ## What makes this work Three properties of deputyOS combine to keep the surface profile-agnostic: ### Profile-agnostic operator surface [`deputyctl`](/docs/reference/deputyctl/), [`deputywizard`](/docs/reference/deputywizard/), [`deputypwa`](/docs/reference/deputypwa/), [`deputyos-track`](/docs/reference/deputyos-track/), and [`deputyos-desktop`](/docs/reference/deputyos-desktop/) — every Rust crate — read profile data, never profile-specific logic. Switching the active profile is reading a different TOML file at `/etc/deputyos/profiles/.toml` and starting a different systemd unit; no command in the operator surface has an `if profile == "openclaw"` branch. The manifest schema is documented in [Reference → Schemas → profile.toml](/docs/reference/profile-toml/). The wizard reads `[wizard].prompts` to decide which questions to ask; the doctor reads `[health]` and `[runtime]`; the up/down commands read `[service]`; the AppArmor verifier reads `[apparmor]`. Everything is data-driven. ### Profile-agnostic IPC The [message relay](/docs/reference/message-relay/) is a line-delimited JSON socket protocol with four hook kinds — and that's the entire IPC vocabulary between a profile and deputyOS. A profile that emits hooks in the [documented payload schemas](/docs/reference/hook-payloads/) is a first-class citizen of the appliance. The PWA, the cost ledger, the doctor, and the channel-toggle surface all read from the relay and do not care which profile produced the hook. ### Profile-agnostic build The Ansible role at `roles/deputyos/` is the single source of truth for what goes into an image. A new profile adds *one* `import_tasks` line to `roles/deputyos/tasks/main.yml` and contributes its own task file. Everything else — the runtime baseline, the security baseline, the A/B partitioning, the systemd hardening, the offline cache — is shared across every profile. The variant gate (`when: hw == ""`) is per-hardware-target, not per-profile. A profile is built into every image; what differs across hardware is the limits the profile runs under. ## The 6-file recipe at a glance This is the inventory; the [How-to → Add a profile](/docs/how-to/add-a-profile/) page walks each file step-by-step with a worked example (Khoj). ### 1. `profiles/.toml` — the manifest The schema is in [Reference → Schemas → profile.toml](/docs/reference/profile-toml/). Required sections: `[profile]`, `[paths]`, `[runtime]`, `[service]`, `[health]`, `[apparmor]`, `[wizard]`, `[channels]`, `[upgrade]`. The `[memory]` and `[kernel]` sections are optional. The manifest declares what is *already in the image*. It does not cause anything to be installed — see [the architecture page](/docs/concepts/architecture/) for the zero-first-boot-network-installs invariant. ### 2. `roles/deputyos/tasks/profile-.yml` — the bake recipe This is where the agent actually lands on disk. The recipe runs at build time inside the Ansible role. It must: - Lay the agent down at `[paths].install_root` on the slot partition. - Pre-resolve all native modules and Python wheels into the offline cache under `/var/cache/deputyos/`. - Be reproducible — same inputs, same outputs. - Be idempotent — running it twice produces the same image. - Honour the offline-bake mode (use the stub fallback when the network is unavailable; details below). The recipe is full Ansible — `pip`, `npm`, `git`, `unarchive`, `get_url`, `command` — but everything happens at *build* time, never at boot. ### 3. `roles/deputyos/templates/-gateway.service.j2` — the systemd unit A Jinja2 template rendered into `/etc/systemd/system/-gateway.service` at bake time. Required hardening directives: - `User=agent`, `Group=agent` — never root. - `ExecStart=` references `[paths].binary` from the manifest. - `EnvironmentFile=/etc/deputyos/secrets.env` — provider keys live there at mode 0600, owned by root, readable by the gateway via supplementary group or systemd `LoadCredential`. - `Restart=always`, `RestartSec=` matching the manifest's `[service].restart_policy`. - `ProtectSystem=strict`, `ProtectHome=true`, `PrivateTmp=true`, `NoNewPrivileges=true`, `CapabilityBoundingSet=` (drop everything not needed), `SystemCallFilter=` (deny obvious bad classes). - `ConditionPathExists=/etc/deputyos/profiles/.toml` — the unit refuses to start if the profile manifest is not present. The full hardening list is in [Reference → System → systemd units](/docs/reference/systemd-units/). ### 4. `roles/deputyos/files/apparmor/deputyos.` — the AppArmor profile The profile lives at `/etc/apparmor.d/deputyos.` after bake and is loaded into enforce mode at boot. It must allow: - Read on `[paths].install_root` (the agent's read-only bake). - Read/write on `[paths].data_dir` (the agent's persistent state). - Read on `/etc/deputyos/secrets.env` (or via systemd credential forwarding). - Network egress on the channels the profile actually uses. - Whatever runtime-specific syscalls the agent's language stack needs. It must deny everything else. AppArmor profiles are [documented in detail](/docs/reference/apparmor-profiles/) with per-rule rationale. ### 5. `roles/deputyos/files/-stub.sh` — the offline-bake fallback When CI builds in offline mode (no network, sandboxed runners), the real bake recipe cannot fetch upstream. The stub is a small shell script that is laid down at `[paths].binary` instead and prints a clear message: "this profile was offline-baked; the agent binary is not present; you cannot start this profile." That keeps the image testable end-to-end (every other deputyOS surface still works) while making the missing payload obvious. The stub matters for the local-build-as-first-class workstream — a contributor on a flaky cafe Wi-Fi can still reproduce an image and exercise the appliance shape. ### 6. One-line addition to `roles/deputyos/tasks/main.yml` ```yaml - name: Bake the profile import_tasks: profile-.yml ``` That's the only edit to a shared file. Everything else is new, profile-owned artefacts. ## What the acceptance test proved Khoj was added to deputyOS as the acceptance test of the plugin model. The acceptance criteria were: 1. Khoj fits the [profile class](/docs/concepts/profile-class/) (multi-channel gateway, persistent memory, skill/tool system) — yes. 2. The PR adds exactly the six artefacts above and modifies *only* `roles/deputyos/tasks/main.yml` (one new `import_tasks` line) and `README.md` / supporting docs. No Rust changes — confirmed. 3. `cargo test --all` is still green at 221 tests — confirmed; the number didn't move because no Rust code was touched. 4. The Khoj image boots in QEMU, the wizard runs, the gateway starts on port 42110, the PWA shows Khoj-branded chat — confirmed by the smoke harness. 5. `deputyctl profile switch khoj` and `deputyctl profile switch openclaw` both work on a multi-profile image — confirmed. The Khoj manifest, recipe, AppArmor profile, systemd unit, and stub remain in the repo as the reference example for any subsequent profile. ## What plugins *cannot* do The plugin model is wide — a profile can pick its language, its package manager, its channels, its memory backend, its required sysctls — but it has hard edges. The following changes are *not* plugin-extendable; they require Rust changes to deputyOS itself, generally with an ADR: - **New HookKind variants.** The relay's hook vocabulary is fixed; a profile that wants a fundamentally new event type is asking for an deputyOS-wide extension. The four current kinds are documented in [Reference → Schemas → hook payloads](/docs/reference/hook-payloads/). - **New manifest schema fields.** Adding a field to `profile.toml` changes a Rust struct in `deputyctl/src/manifest.rs` and ripples to the wizard, the doctor, and (potentially) the tracker. That is an deputyOS extension, not a profile contribution. - **New wizard step types.** The wizard renders steps from a fixed set of step kinds (`model_provider`, `channels`, `gateway_allowlist`, `backup_destination`). A profile that needs a brand-new step type needs an deputyOS extension. - **New systemd hardening defaults, new AppArmor abstractions, new ufw chains.** These are appliance-wide invariants; per-profile loosening is fine inside an existing profile's AppArmor file, but changing the *defaults* is a security review, not a profile review. - **New CLI commands on `deputyctl`.** Per-profile CLI helpers are not a thing — the operator surface is shared. If a profile needs to do something new, it does so via its own gateway's CLI (which [`deputyctl shell`](/docs/reference/deputyctl/) drops into). The bright line is: a profile contributes *what runs* and *how it is confined*, not *how the appliance is operated*. ## Where to go next - [How-to → Add a profile](/docs/how-to/add-a-profile/) — the worked step-by-step for the six files, using Khoj as the reference example. - [Concepts → Profile class](/docs/concepts/profile-class/) — the rule for what qualifies before you start. - [Reference → Schemas → profile.toml](/docs/reference/profile-toml/) — every field of the manifest. - [Reference → System → systemd units](/docs/reference/systemd-units/) — the hardening directives a unit template must include. - [Reference → System → AppArmor profiles](/docs/reference/apparmor-profiles/) — the structure of the per-profile confinement file. - [Reference → Schemas → hook payloads](/docs/reference/hook-payloads/) — the events a profile emits to integrate with the rest of the appliance. ## Desktop launcher internals Section: distribution URL: https://www.deputyos.com/docs/distribution/desktop-launcher-internals/ ## What this page does Architecture walkthrough of the `deputyos-desktop` crate — the "download + double-click → wizard in browser" launcher for Linux, Windows, and macOS. The launcher is a Rust binary per platform that consumes existing deputyOS image artefacts (the same qcow2 / img.xz files that ship for the QEMU and Pi targets) and orchestrates the host's native virtualization to boot them. The user-facing surface is documented in [Reference → CLI → deputyos-desktop](/docs/reference/deputyos-desktop/); this page is the implementation-side reference for contributors and auditors. ## Crate architecture ```text deputyos-desktop/ ├── src/ │ ├── main.rs ← CLI dispatch (clap) │ ├── lib.rs ← public modules │ ├── config.rs ← cache locations, version pinning, prefs │ ├── manifest.rs ← reuses deputyctl::release types for verify │ ├── download.rs ← image fetch + sha256 + minisig verify │ ├── browser.rs ← cross-platform "open URL" shim │ ├── driver.rs ← Driver trait + `current_driver()` factory │ └── drivers/ │ ├── mod.rs │ ├── linux.rs ← qemu+KVM driver │ ├── windows.rs ← WSL2-mandate driver │ └── macos.rs ← UTM-mandate driver └── Cargo.toml ``` ## The user-facing CLI surface ```text deputyos-desktop install # prereq + download + cache + import deputyos-desktop start # boot VM + open browser. Default action. deputyos-desktop stop # graceful shutdown deputyos-desktop status # running/stopped + URL deputyos-desktop update # check manifest + (not yet implemented) apply deputyos-desktop uninstall [--data] # remove cache; --data also wipes data dir ``` Invoking with **no subcommand** mimics double-click: install (if needed), start, then open the wizard URL in the default browser. That's the "non-technical user double-clicks the icon" path. ## Driver model Each platform implements the `Driver` trait: ```rust pub trait Driver { fn check_prereqs(&self) -> Result<()>; fn start(&self, image_path: &Path) -> Result; fn stop(&self) -> Result<()>; fn status(&self) -> Result; } ``` Where `VmStatus` reports running/stopped + the wizard URL (always `http://localhost:8088/wizard` per the loopback-only design). ### Linux driver - **Mandate**: qemu+KVM. The driver checks `/dev/kvm` is accessible and `qemu-system-x86_64` (or `qemu-system-aarch64`) is on PATH. - **No bundled hypervisor.** Linux distros all ship qemu+KVM; we don't re-bundle. - **Boot command**: roughly `qemu-system- -m 4G -accel kvm -drive file=,if=virtio -netdev user,id=net0, hostfwd=tcp:127.0.0.1:8088-:8088 -device virtio-net,netdev=net0 -nographic -daemonize -pidfile /vm.pid`. - **Stop**: SIGTERM to the pid in the pidfile. - **Status**: `kill -0 ` plus `curl -fsS http://localhost:8088/healthz`. ### Windows driver - **Mandate**: WSL2. The driver checks for the WSL2 distro that `deputyos-desktop install` imports (or that `wsl/Install-DeputyOS.ps1` imported earlier). - **No bundled hypervisor.** WSL2 is the right shape on Windows; we don't run nested qemu in WSL. - **Boot command**: `wsl --distribution deputyos --user agent /opt/deputyos/start.sh`. - **Stop**: `wsl --terminate deputyos`. - **Status**: `wsl --list --running` plus the healthz probe. ### macOS driver - **Mandate**: UTM. The driver checks for UTM at `/Applications/UTM.app` and the `utmctl` CLI it ships. - **No bundled hypervisor.** Apple's restrictions on third-party hypervisors plus UTM's existing ecosystem make UTM the right shape. - **Apple Silicon (aarch64)**: HVF acceleration; near-native speed. - **Intel x86_64 macOS**: TCG (slow) — supported but not recommended. - **Boot**: `utmctl start deputyos`. - **Stop**: `utmctl stop deputyos`. - **Status**: `utmctl status deputyos`. ## Cross-compile per host triple The `make desktop-launcher` target builds for the host triple by default; pass `DESKTOP_TARGET=` to cross-compile: | Host | Target triple | Notes | | --- | --- | --- | | Linux x86_64 | `x86_64-unknown-linux-gnu` | native | | Linux arm64 | `aarch64-unknown-linux-gnu` | native | | Windows | `x86_64-pc-windows-msvc` | from Windows host | | Windows (cross) | `x86_64-pc-windows-gnu` | from Linux via `cross` | | macOS Intel | `x86_64-apple-darwin` | must build on a Mac | | macOS Apple Silicon | `aarch64-apple-darwin` | must build on a Mac | macOS builds need Apple's toolchain blobs we cannot legally redistribute. macOS contributors build on a Mac. ## Manifest fetch + sha + minisig verify The launcher reuses `deputyctl::release` types — the same code path the on-device update verifier uses. Steps in `download.rs`: 1. Fetch `manifest.json` and `manifest.json.minisig` from the deputyOS CDN URL (`DEPUTYOS_DESKTOP_MANIFEST_URL` env override; or the baked default). 2. Verify the minisign signature against the pubkey baked into the binary at build time. 3. Find the artefact for the host's `(target, profile)` — for Linux this is `qemu-x86_64` or `qemu-aarch64`, for Windows it's `wsl2`, for macOS it's `macos-qemu`. 4. Download to the cache. Verify sha256. Verify per-artefact minisig. 5. Cache hit → use cached image. ## Cache locations | Platform | Cache root | | --- | --- | | Linux | `${XDG_CACHE_HOME:-~/.cache}/deputyos-desktop/` | | Windows | `%LOCALAPPDATA%\deputyos-desktop\` | | macOS | `~/Library/Caches/deputyos-desktop/` | Cache contents: - `images//` — downloaded and verified images. - `vm.pid` (Linux) — running VM PID. - `state.json` — current install state. `deputyos-desktop uninstall` removes the cache. `--data` also wipes the persistent data dir at: | Platform | Data dir | | --- | --- | | Linux | `${XDG_DATA_HOME:-~/.local/share}/deputyos-desktop/` | | Windows | `%APPDATA%\deputyos-desktop\` | | macOS | `~/Library/Application Support/deputyos-desktop/` | ## Code-signing per platform Distributing the binary needs platform-specific signing — without it, Gatekeeper / SmartScreen will block the launch. | Platform | Signing requirement | Status | | --- | --- | --- | | Linux | gpg-signed AppImage | not yet built | | Windows | Authenticode EV cert | not yet built | | macOS | Apple Developer ID + notarization | not yet built | The CI signing infrastructure for these is not yet built. Today the launcher binaries are unsigned — distribution is "build it yourself with `make desktop-launcher`" pending the signing path. ## Browser open `browser.rs` is a thin shim over the platform default. Linux: `xdg-open`. Windows: `start` (via cmd). macOS: `open`. The launcher always opens `http://localhost:8088/wizard`; the wizard's first-boot state is persistent in the data dir, so successive `start` invocations go straight to whatever step the user left off. ## Update flow `deputyos-desktop update`: 1. Fetch the latest manifest. 2. Compare to the cached manifest's release_version. 3. If newer: download + verify the new image. (This step is in the not yet implemented; today's `update` is check-only.) 4. On next `start`, boot the new image. The launcher does not yet support A/B for desktop the way the appliance images do (planned). Replacement is in-place; the previous image is removed only after the new one verifies. ## Verification ```sh # Build for host cargo build --release -p deputyos-desktop # Run install + start (interactive) ./target/release/deputyos-desktop install ./target/release/deputyos-desktop start # Status / logs ./target/release/deputyos-desktop status ``` ## Related - [Reference → CLI → deputyos-desktop](/docs/reference/deputyos-desktop/) - [Distribution → Hardware matrix](/docs/distribution/hardware-matrix/) - [Build → Make targets](/docs/build/make-targets/) (the `desktop-launcher` target) - [Security → Update trust chain](/docs/security/update-trust-chain/) - [Operations → Update and rollback](/docs/operations/update-and-rollback/) ## Image bake internals Section: build URL: https://www.deputyos.com/docs/build/image-bake-internals/ ## What this page does Walk through how an deputyOS image is **baked** end to end — the shared Ansible role, the variant gates, the per-Packer-template invocation, the pre-build staging in `scripts/build.sh`, and the hermetic-build invariants. This page is for contributors who want to understand the build system enough to extend it (new hardware target, new profile, new policy baseline). ## The shared Ansible role + variant gates pattern Every shipped image is the same Debian 12 (bookworm) base plus the deputyOS Ansible role at `roles/deputyos/`. The role applies in a fixed order: 1. **`security-baseline.yml`** — sysctls, ufw, fail2ban, SSH hardening, AppArmor framework, zram, ClamAV, Magika, the agent user, `/etc/deputyos/` layout, `deputyctl` binary install. 2. **`networking-baseline.yml`** — mDNS via avahi (LAN targets only), Cloudflare Tunnel + Tailscale binaries (installed but not started). 3. **`wizard-baseline.yml`** — `deputywizard` binary install + first-boot systemd unit + qr-on-tty unit (LAN targets). 4. **`hooks-baseline.yml`** — `/etc/deputyos/hooks.d//` directory tree creation. 5. **`voice-baseline.yml`** (gated) — whisper.cpp + Piper + voice-relay shell bridge + `deputyos-voice-relay.service` + AppArmor profile. Self-gates on `deputyos_voice_enabled` and the `deputyos_no_voice_hw` list. 6. **`variant-.yml`** — per-target tuning (kernel package, bootloader cmdline, governor, on-demand vs daemon ClamAV, voice gates, …). 7. **`profile-.yml`** — agent code install, gateway systemd unit, AppArmor profile, `.bake-meta`. Each step is idempotent. The role never starts gateways at bake time — first boot of the image is what runs the first-boot unit, which in turn runs the wizard, which on apply enables and starts the gateway. ## Layout ``` roles/deputyos/ ├── defaults/ │ └── main.yml ← role-wide facts (no_voice_hw, cloudish_hw, ...) ├── meta/ │ └── main.yml ← role metadata (Ansible compatibility) ├── handlers/ │ └── main.yml ← Reload sysctl / ufw / Avahi / AppArmor / systemd ├── tasks/ │ ├── main.yml ← entry point; dispatches to the rest │ ├── security-baseline.yml │ ├── networking-baseline.yml │ ├── wizard-baseline.yml │ ├── hooks-baseline.yml │ ├── voice-baseline.yml ← gated │ ├── variant-rpi5.yml │ ├── variant-rpi4.yml │ ├── variant-arm64-generic.yml │ ├── variant-qemu-aarch64.yml │ ├── variant-qemu-x86_64.yml │ ├── variant-x86_64-mini-pc.yml │ ├── variant-wsl2.yml │ ├── variant-macos-qemu.yml │ ├── variant-digitalocean.yml │ ├── variant-oracle-arm-free.yml │ ├── variant-hetzner-cloud.yml │ ├── variant-vultr.yml │ ├── variant-linode.yml │ ├── variant-fly-machines.yml │ ├── profile-openclaw.yml │ ├── profile-hermes.yml │ └── profile-khoj.yml ├── templates/ ← *.j2 — systemd units + per-profile config └── files/ ← AppArmor profiles, stub binaries, limits..json ``` The shared invocation pattern: every Packer template runs the role with `deputyos_hw=` and `deputyos_profile=`, both injected via Packer `var` blocks. The shared playbook lives at `packer/playbook.yml`. ## Entry point: `tasks/main.yml` The `main.yml` is a flat `import_tasks` chain: ```yaml - name: Assert required role variables are set ansible.builtin.assert: that: - deputyos_hw | length > 0 - deputyos_profile | length > 0 - name: Apply security baseline ansible.builtin.import_tasks: security-baseline.yml - name: Apply networking baseline ansible.builtin.import_tasks: networking-baseline.yml - name: Apply wizard baseline ansible.builtin.import_tasks: wizard-baseline.yml - name: Apply hooks baseline ansible.builtin.import_tasks: hooks-baseline.yml - name: Apply voice baseline (gated) ansible.builtin.import_tasks: voice-baseline.yml # 14 variant dispatches, when: deputyos_hw == "" - name: Apply rpi5 variant tuning ansible.builtin.import_tasks: variant-rpi5.yml when: deputyos_hw == "rpi5" # ... rpi4, arm64-generic, qemu-aarch64, qemu-x86_64, # x86_64-mini-pc, wsl2, macos-qemu, # digitalocean, oracle-arm-free, hetzner-cloud, vultr, # linode, fly-machines # 3 profile dispatches, when: deputyos_profile == "" - name: Apply OpenClaw profile bake ansible.builtin.import_tasks: profile-openclaw.yml when: deputyos_profile == "openclaw" # ... hermes, khoj ``` ## Pre-build staging: `scripts/build.sh` Before invoking Packer, `scripts/build.sh` populates `build/staging/` with the host-built artefacts the role copies into the guest. Pre-stage steps: 1. **`deputyctl` release binary** — `cargo build --release --bin deputyctl`; copied to `build/staging/deputyctl`. 2. **`deputywizard` release binary** — same; `build/staging/deputywizard`. 3. **Profile manifest** — `cp profiles/.toml build/staging/profiles/.toml`. 4. **`limits.json`** — per-target. Falls back to a synthesized inline default if neither `deputyctl/etc/limits..json` nor `roles/deputyos/files/limits..json` exists. 5. **Voice assets** — `whisper.cpp` model + Piper binary + Piper voice. Skipped if `DEPUTYOS_VOICE_OFFLINE=1` or the target is in the no-voice set. Failures are non-fatal — the role tolerates missing assets and refuses to start the unit. 6. **Cloud-init seed** — for QEMU-style targets that need an SSH session for Packer. A throwaway Ed25519 keypair is generated under `build/staging/ssh-key`. Then Packer is invoked: ```sh packer init