Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting Started

This chapter takes you from a clean checkout to a running agent: prerequisites, per-OS setup, building, API keys, your first run, and the full CLI flag reference.

Fresh box in one command

Standing up a new machine end to end (clone, dependencies, build, launch, and a Connect discovery link) is one command — see Credential Custody for the trust story behind it. The canonical installer is a release asset: every v* release attaches install.sh / install.ps1 stamped with the release’s tag and commit, with their sha256 hashes committed to the public transparency log alongside the binaries’ — immutable once published, and outside any hosted serving path:

# macOS / Linux
curl -fsSL https://github.com/intendant-dev/Intendant/releases/latest/download/install.sh | sh
# Windows (PowerShell)
& ([scriptblock]::Create((irm https://github.com/intendant-dev/Intendant/releases/latest/download/install.ps1)))

To link the daemon to a rendezvous for discovery, say so explicitly — … | sh -s -- --connect https://intendant.dev (or your self-hosted rendezvous); the installer no longer learns a default from wherever it was fetched. Add --service / -Service on an unattended box: intendant service install registers the daemon with the platform’s native supervisor (systemd where present, launchd on macOS, Task Scheduler on Windows, cron @reboot plus a built-in restart supervisor on systemd-less Linux — no init system is a dependency) and prints where the one-time claim code lands. intendant service uninstall|status manage it afterwards.

The installer stands up its own prerequisites: when git is missing (stock Debian minimal cloud images ship without it), the pre-flight installs it through the platform package manager — apt/dnf/yum/zypper/pacman/apk/brew, winget or choco on Windows — announcing the exact command before it runs, escalating via sudo only where it exists and never silently, and otherwise stopping to name the command for you to run. Rust and the native build dependencies follow the same law via the per-OS setup scripts from the cloned tree.

For reproducibility, pin the tag instead of latest: …/releases/download/v0.1.0/install.sh. Release assets are immutable, and a stamped installer double-checks itself: it prints its release identity (tag @ commit) before doing anything else, installs exactly that released tree, and fails closed with RELEASE_PIN_MISMATCH when the checkout doesn’t match the commit its release recorded. Everything else it executes comes from that verified tree, and cargo build --locked extends the pinning to dependency hashes.

Pick your rung of the trust ladder honestly:

  1. The one-liner is the convenience rung: you are trusting GitHub’s release storage and the transparency log’s public evidence, sight unseen.
  2. Download, read, then run the same asset — it is a short script. Check it against the release’s install.sh.sha256 and the logged release manifest first.
  3. A signed native app outranks both once published. No Developer ID-signed/notarized release exists for this alpha, and an -unsigned-dev artifact never qualifies as an anchor.

https://intendant.dev/install.sh survives only as a non-canonical convenience: it answers with a redirect (HTTP 302) to the release asset and never a script body — script against the GitHub URL, not the pretty one. While no v* release has been published yet (the current state of this alpha), the asset URL 404s; until the first release, clone and build from source — the prerequisites and build steps below are exactly what the installer automates.

What happens next is the whole story in four steps:

  1. Link. The daemon prints a single-use twelve-word claim code in its log. Enter it at your rendezvous — intendant.dev/connect for the hosted service (invite-only during the alpha), or your self-hosted rendezvous — and the route appears under Your computers. Linking is discovery metadata only: it creates no IAM principal or grant and grants no daemon access.
  2. Establish a trusted owner. On the machine’s console or SSH session, run intendant access setup; alternatively use a direct mTLS connection. The packaged macOS app contains a local mTLS bridge, but no Developer ID-signed/notarized release has been published for this alpha, so its current -unsigned-dev artifact is not an anchor. The default build cannot mint or exercise root through hosted-origin code. The former --owner <browser-key> shortcut is retired in this alpha; a bare fingerprint is not remote authentication.
  3. Authorize on that trusted surface. Install/use the daemon’s browser mTLS client certificate or stay on loopback. Browser identity-key records are not an active login mechanism in this alpha. The hosted Connect tab remains discovery-only; there is no opt-in that turns it into a control client.
  4. Fuel and work. Open the trusted dashboard: until a credential lands, the Activity greeting offers Add API keys and Sign in an agent buttons — and when external coding agents are already signed in, it leads with a “…ready to work” headline instead of a blocker. Grant a time-boxed API-key lease (or relay provider calls through your browser), configure a local key, sign an external agent into its subscription from the Vault tab, or run intendant auth chatgpt login for Intendant Native. API-key and native ChatGPT leases are memory-only, but this is not a blanket disk guarantee: .env and the optional private native ChatGPT login store are durable, while a full-credential OAuth lease for an external CLI temporarily materializes a private auth home under <state-root>/leased-auth (~/.intendant/leased-auth by default) until cleanup. Credential Custody is the full story. Then send the first task from the composer, watch it in Activity, and dial autonomy and approvals to taste.

Upgrading an earlier alpha installed with --owner: remove the retired flag from the service command, upgrade and restart the daemon, close any old Connect dashboard tabs, then run intendant access setup locally and install the generated mTLS client credential. CLI parsing now rejects --owner with this migration direction. IAM schema v2 revokes both connect-bootstrap grants and legacy browser-key grants whose reason records a --owner bootstrap; it does not silently preserve browser-key root.

The rest of this chapter is the from-checkout path the installer automates — including the classic alternative to fueling from the vault: putting an API key in .env on a machine you fully trust.

Prerequisites

Intendant is a Rust workspace. At minimum you need:

  • Rust 1.96.1 — pinned by rust-toolchain.toml; rustup selects and installs it automatically
  • wasm-pack 0.14.0 — pinned by .wasm-pack-version; install it with cargo install wasm-pack --version 0.14.0 --locked (the build auto-rebuilds stale browser WASM, see WASM)
  • ffmpeg — display recording and software H.264 encoding
  • Provider credentials — an API key in .env for at least one of OpenAI, Anthropic, or Gemini; an Intendant-owned ChatGPT login; or nothing on disk at all, because an authorized daemon can run on vault leases

Platform-specific runtime dependencies (display capture, input injection, audio routing) are best installed with the setup script for your OS.

Per-OS setup scripts

The scripts in scripts/ install everything a fresh machine needs and verify what is already present. Each accepts --check to report status without changing anything.

# macOS — installs cliclick, ffmpeg, sox, SwitchAudioSource, wasm-pack,
# Vortex Audio HAL plugin (or BlackHole fallback), and builds the app bundle.
./scripts/setup-macos.sh
./scripts/setup-macos.sh --check     # report only

# Linux (Debian/Ubuntu) — installs the APT_PACKAGES set: libvpx, libxcb +
# libxcb-shm + libxcb-randr, libpipewire, xdotool, imagemagick, ffmpeg,
# pulseaudio-utils, ripgrep, Xvfb, and toolchain build deps.
./scripts/setup-linux.sh
./scripts/setup-linux.sh --check

# Windows (Server 2022 / 11), PowerShell — see ./windows-support.md
./scripts/setup-windows.ps1

Manual Linux install if you would rather not run the script:

sudo apt install build-essential binutils pkg-config libclang-dev \
  libvpx-dev libpipewire-0.3-dev libxcb1-dev libxcb-shm0-dev libxcb-randr0-dev \
  xdotool x11-utils imagemagick ffmpeg xvfb pulseaudio-utils ripgrep xdg-utils \
  ca-certificates libnss3 libatk-bridge2.0-0 libgtk-3-0 libxcomposite1 \
  libxdamage1 libxrandr2 libxss1 libgbm1 libdrm2 libxkbcommon0 libcups2

See Integrations for what each tool is used for, and Windows Support for the Windows toolchain in detail.

Building

cargo build --release     # optimized
cargo build               # debug
cargo check               # type-check only (fast)
./target/release/intendant setup browsers  # managed Chrome for Testing for browser workspaces

Managed browser setup accepts --check, --force, --channel stable|beta|dev|canary, --json, and --print-path; run intendant setup browsers --help for exact usage.

A release build produces three binaries:

  • target/release/intendant-runtime — the command executor. Reads JSON commands on stdin, runs them, writes JSON results to stdout, and never holds API keys. Its write sandbox defaults on for macOS/Linux and is opt-in on Windows.
  • target/release/intendant — the controller. Manages the LLM conversation, calls model APIs, dispatches tool calls to the runtime subprocess, and hosts every frontend (web dashboard, MCP, control socket).
  • target/release/intendant-connect — the self-hostable account, discovery, and route-metadata rendezvous. It has no daemon authority.

The two-binary split is the security boundary; see Architecture.

Installing

cargo install --path .

All three binaries land in ~/.cargo/bin/. The intendant binary embeds the default system prompts and the web assets (HTML, JS, compiled WASM) at compile time, so it runs from any directory without the source tree.

WASM builds automatically

There are two browser WASM crates: crates/presence-web (fed by shared crates/presence-core) produces static/wasm-web/, and crates/station-web produces static/wasm-station/. A normal cargo build rebuilds stale WASM for you: build.rs compares each crate’s source timestamps with its compiled .wasm, invokes wasm-pack in a separate target directory when needed, then re-embeds the resulting artifacts.

Artifact generation is version-gated. Only the wasm-pack version in .wasm-pack-version — currently 0.14.0 — may rebuild the committed output. If wasm-pack is absent or a different version is installed, cargo build warns and keeps the committed artifacts rather than creating byte churn.

The canonical manual fallback rebuilds both crates with the same flags as build.rs:

bash scripts/build-wasm.sh
cargo build --release -p intendant   # re-embed

Regenerate and commit WASM artifacts on macOS only. The emitted bytes are not deterministic across host triples; the aarch64 macOS build is the canonical artifact host used by the drift gate.

The earlier guidance that “cargo build alone does NOT rebuild WASM” is no longer true — build.rs handles it.

macOS app bundle

scripts/bundle-macos.sh compiles a small Swift wrapper (macos-app/*.swift) with swiftc, bundles it with the release intendant binary, codesigns it (a persistent self-signed identity, ad-hoc fallback), and installs to /Applications/Intendant.app. The same script carries the release signing seam: with INTENDANT_SIGN_IDENTITY and the notary variables set, it can produce a Developer ID-signed, notarized bundle. Those credentials are not provisioned for the current release setup, and no such artifact has been published.

The wrapper hosts a WKWebView that loads the dashboard over a custom intendant:// URL scheme. This is deliberate: WKWebView does not treat http://localhost as a secure context, so navigator.mediaDevices (microphone and camera) would be unavailable. Serving from a registered custom scheme restores the secure context the live-voice and camera features need.

When the generated access cert set is readable in <state-root>/access-certs (~/.intendant/access-certs by default), the wrapper automatically starts the bundled daemon with native --mtls. The in-app intendant:// bridge then speaks HTTPS/WSS to the local backend, pins the generated server certificate, and presents the generated client.p12 for its own local bridge. Remote browsers use https://<mac-ip>:<port> and must have an enrolled client identity. If no complete access cert set is installed, the bundled daemon fails closed with setup guidance rather than serving plain HTTP. Explicit launch flags still win: open -a Intendant --args --tls forces TLS-only, and --no-tls --bind 127.0.0.1 is the explicit local plaintext/debug escape.

The same secure-context requirement applies to remote browsers using Station’s WebGPU renderer, microphone/camera features, browser screen capture, and other privileged browser APIs; see Web Dashboard: Secure Browser Contexts.

Dashboard on demand. The app starts the daemon immediately but defers the dashboard SPA behind a lightweight placeholder with an Activate Dashboard button — the SPA’s web-content process is the expensive part (it grows past a gigabyte during long streaming sessions), and a Mac used as a remote daemon never needs it locally. Closing the window frees the WKWebView entirely while the daemon keeps running; reopen it from the Dock (the placeholder returns). Quitting the app (Cmd+Q or the Dock menu) is what stops the daemon. An activated dashboard whose window stays hidden or minimized for 3 hours unloads back to the placeholder automatically. Two environment variables tune this: INTENDANT_AUTO_DASHBOARD=1 loads the SPA at launch without the placeholder (implied by INTENDANT_DIAG=1, which smoke harnesses rely on), and INTENDANT_DASHBOARD_IDLE_UNLOAD_SECS overrides the idle-unload threshold (0 disables it).

./scripts/bundle-macos.sh           # release build + install to /Applications
./scripts/bundle-macos.sh debug     # debug build + install
INSTALL_APP=0 ./scripts/bundle-macos.sh   # build the bundle without installing

macOS releases: signing and notarization

The repository ships a tag-triggered release workflow with two independent signing lanes. The PGP lane is required: pushing a v* tag runs .github/workflows/release.yml on the self-hosted macOS runner, which release-builds the binaries, runs scripts/bundle-macos.sh, detach-signs every artifact it will publish with the Intendant release signing key (scripts/release-pgp-sign.sh), publishes a GitHub Release — the app archive Intendant-<version>-macos-<arch>*.zip, the stamped install.sh / install.ps1, the .sha256 sidecars, a detached armored .asc signature per artifact, and the public key itself — and commits the release manifest (every artifact’s hash plus the signing-key fingerprint) to the public transparency log. A tag build without the PGP secrets goes red before building anything, and the sign step self-verifies every signature against the repo-committed public key before anything publishes, so a release whose own verify ritual would fail can never ship.

The Apple Developer ID lane is deliberately dormant for this alpha (owner-ratified distribution lane, 2026-07-29): no Developer ID or notarization credentials are provisioned and no signed/notarized app release has been published, so the app archive keeps its explicit -unsigned-dev suffix, Gatekeeper warns on first open, and the artifact is not a distribution trust anchor — authenticity rides the PGP signatures and the transparency log. The lane is parked, not deleted: once its secrets are provisioned, a tag build engages it fully (signed AND notarized, or the run goes red) on top of the PGP lane.

Verifying a release

The release signing key is committed at the repo root as RELEASE-SIGNING-KEY.asc, published byte-identical beside every release, linked from intendant.dev, and pinned at compile time by intendant hosted-verify (src/bin/caller/pgp_identity.rs; a unit test derives the pin from the committed bytes). Primary fingerprint (Ed25519, certify-only primary + signing subkey):

A9B3 89C0 58DD 177B 3303 A135 22FC 08F0 A26D 3D18
# once — import the key (from a repo checkout, or the release's own assets)
gpg --import RELEASE-SIGNING-KEY.asc
gpg --fingerprint release@intendant.dev   # compare against the fingerprint above

# per download — the signature proves the bytes, the log proves the history
gpg --verify Intendant-<version>-macos-arm64-unsigned-dev.zip.asc \
             Intendant-<version>-macos-arm64-unsigned-dev.zip
intendant hosted-verify --releases <tag>

gpg --verify proves the artifact bytes were signed by the release key. hosted-verify --releases proves the rest out of band: the release is committed to the append-only transparency log under a tree head consistent with the local pin, the logged signing-key fingerprint equals the binary’s compiled-in pin, the published key asset hashes to exactly the committed key bytes, every artifact carries its .asc, and GitHub’s asset metadata matches the log (--download upgrades to re-hashing the artifacts; shasum -a 256 -c <zip>.sha256 still works as a quick checksum). Cryptographic signature verification deliberately stays gpg’s job — the binary pins which key, gpg proves these bytes.

Installing a release: download the zip with its .asc and checksum from GitHub releases, verify as above, unzip, and drag Intendant.app to /Applications. On first launch macOS asks for the TCC permissions (Screen Recording, Accessibility, Microphone) exactly as with a local build — and, the alpha being Apple-unsigned, Gatekeeper warns once on open. The app checks GitHub for a newer release at launch (silently, release builds only) and via Intendant → Check for Updates…; updating is always manual — the prompt only opens the release page in your browser.

What the dormant Apple lane would add. With all signing and notarization secrets provisioned, a release bundle is signed with a Developer ID Application certificate under the hardened runtime with a secure timestamp, notarized by Apple, and stapled — Gatekeeper opens it without warnings, and a tampered bundle fails validation. Signing is inside-out: bundled non-system dylibs (Contents/Frameworks, e.g. libvpx), then the embedded intendant-bin / intendant-runtime, then the bundle. All executables carry the minimal entitlements in macos-app/entitlements.plist — microphone (device.audio-input, live voice), camera (device.camera, presence video input), and Apple Events (automation.apple-events, the daemon’s osascript paths); the rationale for what is present and absent is commented in that file. The app bundle stamps its version (CFBundleShortVersionString) from the tag; local dev builds get a git describe stamp, so a release archive and a local build are told apart by version string, signatures, and the log — not by the shared -unsigned-dev suffix. Versions are visible in Intendant → About Intendant.

Provisioning the release secrets (repo → Settings → Secrets and variables → Actions). Official tag builds fail closed on the PGP pair: a v* tag must be PGP-signed and committed to the transparency log, or the run goes red before anything is published. A workflow_dispatch dry-run without secrets still exercises the pipeline end to end and keeps only the explicitly -unsigned-dev artifact on the workflow run (with the PGP pair configured, the dry-run’s workflow artifact is signed too). Each secret group is all-or-nothing and a partial group fails the run.

PGP signing key — PGP_SIGN_KEY_B64, PGP_SIGN_KEY_PASSPHRASE (required for tag releases):

  1. The release key lives in the operator escrow on the release owner’s machine (~/.intendant/release-signing/; its README documents the mint, rotation, and revocation ceremonies). The certify-only primary key never enters CI — only the signing subkey does, and it carries a two-year expiry as a dead-man switch.
  2. base64 -i ~/.intendant/release-signing/secret-subkey-ci.asc | pbcopyPGP_SIGN_KEY_B64; the escrow’s passphrase file content → PGP_SIGN_KEY_PASSPHRASE.
  3. The release runner needs gpg on PATH (brew install gnupg on the fleet Mac); the workflow imports the subkey into a throwaway GNUPGHOME under RUNNER_TEMP and removes it in an always() cleanup step.
  4. Before cutting a release, scripts/release-pgp-dryrun.sh rehearses the whole lane locally — sign with the escrowed subkey, submit to a local rendezvous (including the log door’s refusal of an unsigned manifest), hosted-verify --releases --download against it, and the gpg ritual — with no tag, GitHub, or fleet involvement.

Signing identity — MACOS_SIGN_P12_B64, MACOS_SIGN_P12_PASSWORD (dormant Apple lane):

  1. In your Apple Developer account (Certificates → +), create a Developer ID Application certificate, or use Xcode → Settings → Accounts → Manage Certificates to create it in your login keychain.
  2. In Keychain Access, expand the certificate, select the certificate and its private key, File → Export Items… → .p12, and set a strong password. Export with the certificate chain included (select the Developer ID intermediate alongside if offered) — the release job validates the identity and rejects chain-less exports.
  3. base64 -i cert.p12 | pbcopy, paste into the MACOS_SIGN_P12_B64 secret; put the export password in MACOS_SIGN_P12_PASSWORD.

Notarization — NOTARY_KEY_B64, NOTARY_KEY_ID, NOTARY_ISSUER_ID:

  1. In App Store Connect → Users and Access → Integrations → App Store Connect API → Team Keys, generate a key with the Developer role. Download the .p8 (one-time download).
  2. base64 -i AuthKey_XXXX.p8 | pbcopyNOTARY_KEY_B64; the key’s Key IDNOTARY_KEY_ID; the page’s Issuer IDNOTARY_ISSUER_ID.

The workflow imports the identity into a throwaway keychain (created, used, and deleted with the original keychain search list restored even on failure) and materializes the .p8 only for the run. To sign a build by hand instead, scripts/bundle-macos.sh takes the same seam as environment variables: INTENDANT_SIGN_IDENTITY (activates hardened-runtime distribution signing), INTENDANT_SIGN_KEYCHAIN, INTENDANT_NOTARY_KEY_FILE / INTENDANT_NOTARY_KEY_ID / INTENDANT_NOTARY_ISSUER (notarize + staple), INTENDANT_ARTIFACT_DIR (versioned zip + checksum), and INTENDANT_APP_VERSION (version override; defaults to git describe).

Native ChatGPT OAuth

Intendant Native can call OpenAI through a ChatGPT subscription without delegating the agent loop to Codex and without copying Codex’s auth file:

intendant auth chatgpt login       # OpenAI device-code sign-in
intendant auth chatgpt status      # source, masked account, and expiry
intendant auth chatgpt logout      # best-effort remote revoke + local removal

intendant --provider openai --openai-auth chatgpt "Inspect this repository"

The login belongs to Intendant and lives at <INTENDANT_HOME>/auth/openai-chatgpt.json (0600 on Unix and owner-private on Windows). Intendant does not read or mutate ~/.codex/auth.json. The store is atomically replaced under a cross-process lock when OpenAI rotates tokens; the ID token is used only to derive the account identifier and is not persisted. logout removes the local credential even if the best-effort provider revoke fails. This is durable on-box subscription authority, so use a lease instead when the box should go dry automatically.

--openai-auth (or OPENAI_AUTH_MODE) is deliberately separate from --provider:

  • auto (default) preserves existing cost behavior: an OpenAI API key wins; otherwise an active native ChatGPT lease or local login is used.
  • api-key requires metered OPENAI_API_KEY authority.
  • chatgpt requires subscription OAuth authority and never falls back to an API key.

The API-key lane uses api.openai.com/v1/responses; the ChatGPT lane uses the ChatGPT Codex Responses service with the account header, SSE wire shape, encrypted reasoning continuity, and a stable session prompt-cache key. The runtime subprocess never receives either credential.

The ChatGPT service accepts Intendant’s ordinary function tools but not the metered Responses API’s native computer tool. Regular native sessions therefore omit that one tool on the subscription transport instead of failing the model turn. A dedicated OpenAI computer-use provider still requires OPENAI_AUTH_MODE=api-key; automatic CU selection can fall through to a configured Anthropic or Gemini provider.

API keys (.env)

Two ways to give a daemon credentials, by trust posture:

  • Keys on disk (.env) — this section. Right for a machine you fully trust: your laptop, a workstation you sit at.
  • No keys on disk — a daemon remains unfueled until an authorized trusted loopback/direct-mTLS session lends time-boxed leases from that daemon’s separate daemon-store vault, or relays supported provider calls through the trusted browser. A Connect account link or account-vault blob cannot fuel a daemon in this build because no delivery bridge ships; see Credential Custody. Right for rented, shared, or disposable boxes.

On startup the controller loads environment variables from .env, searching in this order (later files do not override variables already set):

  1. Current directory and its parents (dotenvy::dotenv())
  2. Project root — the git top-level, <project-root>/.env
  3. Global config~/.config/intendant/.env

.env and intendant.toml are git-ignored, so secrets never land in the repo. For use-anywhere-after-cargo install, put your keys in ~/.config/intendant/.env:

# Provide at least one of these:
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GEMINI_API_KEY=AI...

# When more than one key is present, pick the main provider explicitly:
PROVIDER=openai            # "openai" | "anthropic" | "gemini"
MODEL_NAME=gpt-5.5         # optional; a provider default is used if omitted

OPENAI, ANTHROPIC, and GEMINI are accepted as aliases for the corresponding *_API_KEY variables. Provider auto-detection (when PROVIDER is unset) prefers OpenAI when the selected OpenAI auth mode has authority (API key first, then native ChatGPT OAuth in auto mode), then Anthropic, then Gemini. See Configuration for the full environment reference and per-provider default models.

Daemon installs run projectless. The search above runs once, at process startup, relative to wherever the daemon was launched — for the installed macOS app and service installs that is usually $HOME. A launch directory without a project marker (.git / intendant.toml) is not treated as a project: the daemon runs projectless (its .env layer for that directory comes only from the ordinary cwd walk-up, and each dashboard session picks its own project directory). The project you pick when creating a session does not feed this startup search. What a session’s project .env does contribute: the three provider key variables (and only those — never PROVIDER, model, or endpoint settings) join that session’s key resolution as the last layer, after credential leases and the daemon’s own environment. For a daemon, the reliable places for keys are Dashboard → Settings → API Keys (persists to ~/.config/intendant/.env and applies immediately, no restart), that global file itself (restart required if edited by hand), or vault leases per Credential Custody.

Your first run

# A one-off task. By default the web dashboard comes up (see below) and the
# controller prints the dashboard URL; open it in a browser to watch/steer.
./target/release/intendant "List the files in /tmp"

# Pipe a task in (non-TTY stdin auto-selects headless mode):
echo "summarize README.md" | ./target/release/intendant

# Interactive: with no task argument, you are prompted for one.
./target/release/intendant

Mode selection

The dashboard is the UI; the controller picks an execution shape from the flags:

  • Web dashboard is on by default. It runs unless you pass --no-web, --mcp, or --json. The server binds port 8765, auto-incrementing through 8785 if that port is taken; the chosen port is printed at startup.
  • Idle start (intendant with no task) runs the persistent daemon: the session supervisor owns every launch, driven from the dashboard.
  • A task on the command line runs as the foreground session under the same gateway, then falls through to the daemon loop when it ends.
  • --mcp turns the process into an MCP server on stdio (no dashboard).
  • --json emits JSONL events to stdout (headless stdio; no dashboard).
  • --no-web runs one headless task/session in the terminal (the agent loop may take many model/tool turns), with log output on stderr and no UI. --json adds scripted stdin follow-ups and approvals.

So a plain intendant "task" on a desktop gives you a dashboard URL; the terminal itself is a launcher and log tail. (--no-tui from the retired terminal UI era is still accepted as a no-op.)

Resume and continue

./target/release/intendant --continue "fix that bug"   # most recent session
./target/release/intendant -c "fix that bug"            # short form
./target/release/intendant --resume abc123 "continue"   # by session id / prefix / path
./target/release/intendant -r abc123 "continue"
./target/release/intendant --resume                     # no id given → acts like --continue

Launching specific frontends

# Web dashboard explicitly (and/or pick a port)
./target/release/intendant --web
./target/release/intendant --web 9000

# Default dashboard: mTLS with installed access certs
./target/release/intendant

# TLS-only: HTTPS/WSS without requiring browser client certificates
./target/release/intendant --tls

# Explicit local/plaintext debug escape
./target/release/intendant --no-tls --bind 127.0.0.1

# Headless task/session (dashboard off)
./target/release/intendant --no-web "task"

# MCP server on stdio
./target/release/intendant --mcp "Deploy the application"

# Headless JSONL stream
./target/release/intendant --json "echo hello"

# macOS app bundle (after scripts/bundle-macos.sh)
open -a Intendant

# The bundle starts the backend with mTLS by default.
# Forward --tls only when you intentionally want TLS without client cert auth,
# or --no-tls --bind 127.0.0.1 only for explicit plaintext/debug use.
open -a Intendant --args --tls

CLI flag reference

The authoritative source is the argument parser in src/bin/caller/main.rs (print_help and the parse loop). Flags that take a value error out if the value is missing.

FlagArgumentDescription
--provider<name>Force provider: openai, anthropic, or gemini (sets PROVIDER)
--openai-auth<mode>Select OpenAI authority/transport: auto, api-key, or chatgpt (sets OPENAI_AUTH_MODE; independent of --provider)
--model<name>Override the model (sets MODEL_NAME)
--task-file<path>Read the initial task from a file instead of argv
--autonomy<level>Autonomy level: low, medium, high, full (loose parse; unknown → medium)
--log-file<dir>Override the session log directory (default $INTENDANT_HOME/logs/<uuid>/, falling back to ~/.intendant/logs/<uuid>/)
--continue, -cResume the most recent session for this project
--resume, -r[id]Resume a session by id, prefix, or path; with no id behaves like --continue
--no-tuiDeprecated no-op (the terminal TUI was removed); headless is --no-web
--mcpRun as an MCP server on stdio (disables the dashboard)
--verbose, -vShow debug-level log entries
--control-socketEnable the Unix control socket at /tmp/intendant-<pid>.sock
--jsonEmit JSONL events to stdout (headless stdio; disables the dashboard)
--sandboxForce filesystem sandboxing on for the runtime (Landlock on Linux 5.13+, Seatbelt on macOS, restricted tokens on Windows). Wins even if --no-sandbox is also present
--no-sandboxForce the runtime write sandbox off. With neither flag, [sandbox] enabled wins when set; otherwise the platform default is on for macOS/Linux and off for Windows
--directForce single-agent mode (skip the orchestrator / sub-agent delegation)
--no-presenceDisable the presence layer (talk to the worker agent directly)
--web[port]Start the web dashboard. On by default; optional numeric port (default 8765)
--no-webDisable the web dashboard; run headless
--bind<addr>IP address for the web dashboard listener. Use 127.0.0.1 for local/plaintext automation
--no-tlsServe the dashboard over plain HTTP. Explicit local/debug escape; wildcard bind refuses startup when a public interface exists
--allow-public-plaintextOverride the --no-tls public-interface startup guard; does not grant remote callers local/root authority
--tlsServe HTTPS/WSS and provide a secure context; certless local-root is loopback-only and remote protected routes still require mTLS
--tls-cert<path>PEM cert (chain) overriding the default cert selection; implies --tls (pair with --tls-key)
--tls-key<path>PEM private key matching --tls-cert; implies --tls
--mtlsRequire browser/client certificates signed by the Intendant access CA. This is the default dashboard transport
--mtls-ca<path>PEM CA bundle for --mtls client certificate verification
--transcriptionEnable server-side speech transcription (overrides [transcription] enabled)
--record-display<id>Record an existing X11 display, e.g. 50 for :50 (repeatable)
--agent<backend>Use an external coding-agent backend: codex, claude-code, kimi, or pi
--advertise-url<url>WebSocket URL to advertise to federation peers in this daemon’s Agent Card (repeatable, preference order; overrides [server.advertise])
--help, -hPrint help and exit
--version, -VPrint version + build provenance (package version, commit short SHA with -dirty marker, build timestamp, target triple) and exit. Also accepted by intendant-runtime; the daemon reports the same line as daemon_version in intendant ctl status

Correction vs. older docs: --web is on by default and no longer “implies --mcp”. The dashboard runs unless disabled by --no-web, --mcp, or --json. Earlier documentation treated --web as opt-in — that is no longer accurate.

A non-flag token (one that does not start with -) is collected into the task string; an unknown flag is an error.

Native provider authentication is a keyless administrative subcommand: intendant auth chatgpt login|status|logout. It is intercepted before project and provider initialization, so status, login, and logout work even when the daemon has no current model authority.

Dashboard access over TLS

The dashboard defaults to native mTLS. Run intendant access setup first so the daemon has server.crt, server.key, and ca.crt, and so dashboard consumer devices can enroll a client identity.

1. Built-in mTLS / TLS (any platform, pure-Rust)

./target/release/intendant        # default: mTLS
./target/release/intendant --tls  # TLS-only

With no transport flag, the gateway serves HTTPS/WSS with client-certificate authentication required. --tls (or [server.tls] enabled = true in intendant.toml) serves HTTPS/WSS without requiring browser client certificates. With no explicit cert override, TLS-only first uses the installed Intendant access server certificate (server.crt / server.key) when present, then falls back to minting a self-signed certificate at startup (SAN = bind IP + localhost, plus an optional configured hostname). The TLS stack is pure Rust (rustls + rcgen) — no OpenSSL, no nginx — and works on Windows too. See Configuration for [server.tls] and --tls-cert / --tls-key.

intendant access setup stores the access CA, server cert, and client identity in the current user’s native cert store (<state-root>/access-certs on macOS/Linux, the OS data directory on Windows). Run setup as the same user that launches the daemon; the native gateway reads server.crt / server.key directly from that store.

To be explicit about the default, pass --mtls:

./target/release/intendant --mtls

--mtls verifies browser/client certificates against the installed Intendant access CA (ca.crt) unless --mtls-ca or [server.mtls] ca overrides it. Clients without the installed client identity cannot complete the TLS handshake. If access certs are missing, startup fails closed with setup guidance. Use --no-tls only when you intentionally want plain HTTP for local/programmatic debugging. Prefer --no-tls --bind 127.0.0.1; wildcard plaintext refuses startup when the host has a public interface unless --allow-public-plaintext is passed.

2. Native access cert enrollment (intendant access setup)

For mutual-TLS with client certificates (so only enrolled devices can connect), the intendant access subcommand creates a per-user access certificate authority, server cert, client identity, and local-IAM owner grant. macOS/Linux setup can then start a strict enrollment page for other browsers and mobile devices; Windows setup instead prints exact PowerShell commands for importing the same material into that user’s local certificate stores:

intendant access setup            # generate and seed CA/server/client material
intendant access recert           # regenerate the server cert after access addresses change
intendant access list             # show current setup state
intendant access serve-certs      # macOS/Linux: strict HTTPS client enrollment
intendant access remove           # remove the per-user access cert store

Useful flags: --port <N> (native dashboard HTTPS port to advertise, default 8765), --cert-port <N> (HTTPS enrollment server, default 9999), repeatable --ip <IP>, repeatable --host <DNS>, --name <label>, --force, and --no-serve-certs. --cert-port and --no-serve-certs are accepted but unused on Windows, where setup never starts a distribution server. Removed LAN/proxy flags are rejected; certificate setup no longer configures an upstream proxy.

--name is the daemon display label used by the Agent Card and dashboard targets. If omitted, setup now uses the system hostname when available and falls back to the primary IP only as a last resort. Existing IP-based labels continue to work, but the dashboard prefers a human label or hostname over a stale IP when both are present.

On Windows, setup finishes after generation and prints copy/paste-ready Import-Certificate / Import-PfxCertificate commands targeting Cert:\CurrentUser\Root and Cert:\CurrentUser\My. The commands read the generated PKCS#12 password directly into a PowerShell SecureString; they do not echo it into shell history. Close and reopen Edge or Chrome afterwards. serve-certs returns an explicit unsupported/manual-import error on Windows. remove deletes the generated access directory, but certificates manually imported into Windows certificate stores remain until removed with certmgr.msc.

Remote client certificate enrollment is deliberately strict. The temporary enrollment server is HTTPS, using the same access server certificate as the dashboard. Before the CLI reveals the one-time enrollment secret, the operator must copy the server certificate SHA-256 fingerprint observed in the browser certificate UI and paste it into the intendant access terminal. Treat any enrollment web content as untrusted until that terminal check succeeds: stop at the browser certificate warning/details UI, do not continue to page content, enter secrets, click downloads, or install anything first. If the browser cannot expose certificate details before loading the page, inspect the server certificate with a client-side certificate tool instead. Only after the pasted fingerprint matches the local server.crt can the browser redeem the secret and download enrollment artifacts. Apple clients get a single intendant.mobileconfig profile; other clients can download ca.crt plus client.p12 (or the same identity as client.pfx). This avoids the unsafe pattern where a MITM page can serve active content or steal a secret from an unauthenticated download page. A fleet DNS/WebPKI certificate is deliberately not an enrollment shortcut: the server always presents the direct access certificate and always requires the browser-observed fingerprint. Hosted DNS or origin control must never authorize release of the shared owner/root client bundle.

The enrollment page uses the browser’s User-Agent only to prioritize the instructions and download buttons for that device. The authorization decision is always the strict terminal-paired session cookie.

Use this HTTPS/mTLS path or native --tls with a trusted certificate for dashboard features that require a secure browser context. A locally built macOS app wrapper also supplies a secure context for development, but is not a distribution anchor: Station WebGPU, microphone/camera, browser screen capture, and stricter clipboard APIs. Plain http://<host-ip>:8765 is available only when launched with --no-tls; it is intended for local/programmatic debugging, is guarded on public wildcard binds, and does not enable those browser-gated features.

The client certificate is exported as client.p12, a password-protected PKCS#12 bundle for installation on iOS / Android / desktop browsers. The password is shown only on the unlocked enrollment page. The Apple intendant.mobileconfig profile embeds that same client identity, the access CA, and the PKCS#12 password, so it is served only after strict pairing succeeds. On macOS, install downloaded profiles from System Settings → General → Device Management; if that pane is hidden, search System Settings for “Profiles”. If Safari still shows Not Secure, open Keychain Access and set the Intendant CA to Always Trust; installing the profile is not enough unless the CA is trusted for websites. If macOS reports that the profile certificate could not be verified, install ca.crt and client.p12 manually from the same unlocked page, or regenerate access cert material with intendant access setup --force so the Apple profile uses an Apple-compatible client identity bundle and certificate payloads.

Apple device requirement for client.p12

client.p12 is packaged in Apple auto-detect-compatible PKCS#12 form: PBES1 3DES-CBC with a SHA-1 MAC. This is intentionally less modern than PBES2/AES because macOS profile installation and security import can reject PBES2/AES bundles as a password/MAC authentication error unless the caller explicitly forces PKCS#12 format. New access cert material still uses RSA-2048 certificates with SHA-256 signatures, matching Apple’s documented certificate configuration-profile payload compatibility.

Android and desktop Chrome/Firefox also import the Apple-compatible bundle.

Testing

cargo test -p intendant --bins
cargo test -p intendant-core -p intendant-display -p intendant-platform \
  -p owner-plane-core -p owner-plane-reducer
cargo test -p intendant --test e2e
cargo clippy --workspace -- -D warnings

These keyless commands cover all three package binaries, the workspace library gates (including the stamped owner-plane corpus), the real-binary E2E suite, and workspace-wide Clippy. Unit tests are inline #[cfg(test)] modules. The #[tokio::test] cases in tests/e2e/main.rs spawn the real binaries against the deterministic scripted mock provider (PROVIDER=mock + INTENDANT_MOCK_SCRIPT). They need no API key or network, use the synthetic 1280×720 display backend rather than native capture, and run in CI on macOS, Linux, and Windows. Real-LLM or headed-display scenarios live under tests/skills/ and are deliberately outside CI. See Session Logging for the logging/search coverage exercised through that real-binary suite.

Runtime (standalone)

The runtime executes a JSON command batch on stdin and writes results to stdout:

echo '{"commands":[{"function":"execAsAgent","nonce":1,"command":"echo hello"}]}' \
  | ./target/release/intendant-runtime

See Runtime Protocol for the full command schema.