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

Configuration

Intendant is configured through three layers, in increasing specificity:

  1. intendant.toml — in the project root for a rooted daemon, or under the daemon state root (~/.intendant/intendant.toml by default) for a projectless daemon such as the bundled app. The latter stores daemon-wide defaults without making the state directory a session project or sandbox root (structure in src/bin/caller/project.rs).
  2. Environment variables (often via .env) — keys, provider/model overrides, and a few runtime toggles.
  3. CLI flags — per-invocation overrides (see Getting Started).

CLI flags win over env vars where they overlap (--provider sets PROVIDER, --openai-auth sets OPENAI_AUTH_MODE, and --model sets MODEL_NAME). intendant.toml and .env are both git-ignored.

Environment variables

The controller reads these from the process environment (populated from .env; see Getting Started for the search order).

Keys and provider selection

VariableAliasDefaultDescription
OPENAI_API_KEYOPENAIOpenAI key
ANTHROPIC_API_KEYANTHROPICAnthropic key
GEMINI_API_KEYGEMINIGoogle AI (Gemini) key
PROVIDERauto-detectopenai, anthropic, or gemini — which provider to use when multiple keys are set
OPENAI_AUTH_MODEautoOpenAI authority and wire transport: auto, api-key, or chatgpt. This is orthogonal to PROVIDER: use PROVIDER=openai to force OpenAI, then this setting to choose metered API-key billing or ChatGPT subscription OAuth
MODEL_NAMEper-providerMain model name
ANTHROPIC_ENDPOINThttps://api.anthropic.comAnthropic API base URL override for self-hosted API-compatible gateways and proxies
GEMINI_ENDPOINThttps://generativelanguage.googleapis.comGemini API base URL override for self-hosted API-compatible gateways and proxies

Auto-detection (when PROVIDER is unset): OpenAI is available when its selected auth mode has authority. In OPENAI_AUTH_MODE=auto, an OpenAI API key wins to preserve the historical billing choice; with no key, an active oauth:openai-chatgpt lease or Intendant’s local ChatGPT login makes OpenAI available. Provider selection then prefers OpenAI, followed by Anthropic and Gemini. Setting PROVIDER explicitly forces that provider and errors when its selected authority is unavailable. An explicit api-key or chatgpt mode never falls across to the other OpenAI billing source.

Native ChatGPT sign-in is managed with intendant auth chatgpt login|status|logout. It uses OpenAI’s device-code flow and a private, Intendant-owned store at <INTENDANT_HOME>/auth/openai-chatgpt.json; it neither reads nor writes Codex’s ~/.codex/auth.json. Requests use the ChatGPT Codex Responses service, whereas API-key auth keeps using https://api.openai.com/v1/responses. The mode applies to the native Responses providers used by the main agent, text presence, and computer use; OpenAI Realtime and transcription have their own credential paths and still require their documented API-key authority. Intendant deliberately provides no environment override for the OAuth issuer, token endpoints, or subscription Responses endpoint: redirecting bearer and refresh tokens through an agent-writable configuration value would turn a model setting into a credential-exfiltration seam.

PROVIDER=mock selects the keyless scripted provider (no network calls; built for the headless tests/e2e/ suite and demos) and requires INTENDANT_MOCK_SCRIPT=<path> — the script format is documented in src/bin/caller/provider_mock.rs. It is never auto-detected.

Per-provider default models (used when MODEL_NAME is unset):

ProviderDefault model
OpenAIgpt-5.5
Anthropicclaude-sonnet-4-5-20250929
Geminigemini-2.5-pro

Daemon state root

VariableDefaultDescription
INTENDANT_HOME~/.intendantOverrides the daemon state root — the one directory holding session logs, the session-index cache, recordings, quarantine, leased credentials, the optional native ChatGPT store (auth/openai-chatgpt.json), the service pidfile, the projectless daemon’s general settings (intendant.toml) and Connect config (connect.toml), the projectless upload/transfer global store (global-store/, pruned after 14 idle days at daemon startup), and the rest of the machine-local daemon state. On macOS/Linux it also holds access-certs/; Windows keeps that store under the OS data directory instead. The value is used verbatim as the root (no .intendant component is appended); a relative path resolves against the startup directory. Read once at first use and fixed for the process lifetime. Useful for scratch daemons and hermetic harnesses. Locations deliberately outside this root are unaffected: project-local .intendant/ directories, external-agent homes (~/.codex, ~/.claude, ~/.kimi-code, ~/.pi/agent — so a scratch daemon still sweeps the machine-global external stores into its message-search index unless INTENDANT_MESSAGE_SEARCH_DAEMON_HOME_ONLY is also set), the Windows access-cert store, the durable Ed25519 daemon identity private key at the OS data directory’s intendant/daemon-identity/ed25519.pk8 (0600 on Unix; the temp-directory fallback is only for platforms where no data directory resolves), and the current macOS durable Memory plane at ~/.intendant/memory-plane (which does not yet honor INTENDANT_HOME).
INTENDANT_MEMORY_EPHEMERALunsetSet to 1 to force the owner-plane Memory service to use an in-memory plane. With the variable unset, macOS attempts durable storage at ~/.intendant/memory-plane and falls back softly to an honestly labeled ephemeral plane if bootstrap fails; Linux and Windows currently use the ephemeral plane.
INTENDANT_MESSAGE_SEARCH_DAEMON_HOME_ONLYunsetSet to 1 (or true/yes/on, case-insensitive) to confine the message-search indexer’s source discovery to the daemon’s own state root. By default the indexer also sweeps the machine-global external-agent stores (~/.codex, ~/.claude/projects, ~/.kimi-code, ~/.pi/agent, honoring CODEX_HOME-class overrides) — those conversations belong to the machine, not to one daemon home, and are deliberately outside INTENDANT_HOME. With the switch on, sources discovered from the state root keep working — the daemon’s own session logs, leased-active/staged transcript homes, and per-session backend homes persisted under the logs root — except persisted homes at or inside a machine-global store: a session launched without an explicit backend home persists the machine-global default into its config (Codex verbatim, Kimi as a bridge subdirectory mirroring the store), and those stay excluded; explicit non-default homes are swept wherever they live. Any other value keeps the default. For hermetic rigs and scratch daemons that must not index the box’s real corpus; see Session Logging.
INTENDANT_LOOPBACK_TOKENunsetClient-side only. Explicit loopback admission token for intendant ctl and harnesses, overriding the automatic per-port file discovery (<state root>/loopback-tokens/<port>.token). Every daemon boot mints and persists that per-instance token (0600) and refuses tokenless loopback requests to owner-posture surfaces; see the trust-architecture chapter’s “Loopback trust vs. the runtime sandbox”. The daemon itself never reads this variable — a daemon-side env intake would leak into child processes — and the runtime child-environment allowlist never passes it to model-driven code.

Child-process environment

VariableDefaultDescription
INTENDANT_ENV_PASSTHROUGHunsetComma-separated exact env-var names (case-insensitive) added to the native runtime and supervised external CLI cleared-environment allowlists. Unknown variables do not otherwise inherit. Provider API keys never pass. Native runtime exec/PTY shells apply a second credential scrub that currently does not honor this override, so classified ambient credentials such as SSH_AUTH_SOCK remain unavailable there; named ordinary build variables do reach those shells. Treat every entry as an explicit grant of that variable’s value to model-driven code.

Model and behavior tuning

VariableDefaultDescription
MODEL_CONTEXT_WINDOWper-modelContext window in tokens (also settable via [model] context_window)
MAX_OUTPUT_TOKENSper-modelMax output tokens per API call (also [model] max_output_tokens)
USE_NATIVE_TOOLStrueUse the provider’s native tool-calling API; false falls back to text-based JSON extraction
STRUCTURED_OUTPUTprovider-dependentEnable JSON object mode for deterministic parsing
REASONING_EFFORTFor reasoning models: low, medium, high
REASONING_SUMMARYReasoning summary mode: auto, concise, detailed

[model] context_window / max_output_tokens from intendant.toml are applied into MODEL_CONTEXT_WINDOW / MAX_OUTPUT_TOKENS only when those env vars are not already set, so env/CLI always win. The ChatGPT subscription Responses transport intentionally omits max_output_tokens from its wire request and lets that service apply the subscribed model’s ceiling; the value still governs Intendant’s local budgeting. API-key OpenAI requests continue sending the field.

Presence and computer-use overrides

VariableDefaultDescription
PRESENCE_PROVIDERfalls back to PROVIDEROverride the presence layer’s provider
PRESENCE_MODELfalls back to PRESENCE_PROVIDER’s defaultOverride the presence model
CU_PROVIDERfalls back to PROVIDEROverride the computer-use model’s provider
CU_MODELOverride the computer-use model

These mirror the [presence] and [computer_use] sections below; the precedence is explicit config > env var > auto-detect.

Browser workspace overrides

VariableDefaultDescription
INTENDANT_BROWSER_WORKSPACE_EXECUTABLEmanaged browser cacheExplicit Chromium/Chrome-for-Testing executable for CDP browser workspaces
INTENDANT_BROWSER_WORKSPACE_ALLOW_SYSTEM_BROWSERfalse on macOS, true elsewhereOn macOS, explicitly permit CDP workspaces to launch system Chrome/Chromium apps such as /Applications/Google Chrome.app

The older INTENDANT_BROWSER_EXECUTABLE and INTENDANT_BROWSER_WORKSPACE_ALLOW_SYSTEM_CHROME names remain accepted as compatibility aliases; new configuration should use the names in the table.

The default CDP resolver prefers managed Playwright/Puppeteer/Chrome-for-Testing browser caches and Intendant’s own browser cache locations. This avoids attributing Google Chrome updater/app-bundle activity to Intendant on macOS. Set the explicit executable variable when a managed browser lives in a custom path, or choose provider=system_cdp for a deliberate one-off system-browser launch. Run intendant setup browsers to download Chrome for Testing into Intendant’s managed cache. The helper accepts --check, --force, --channel stable|beta|dev|canary, --json, and --print-path; use --check to verify the cache without network access.

Display diagnostics

VariableDefaultDescription
INTENDANT_DISPLAY_INPUT_TELEMETRYunset (off)Set to 1, true, yes, or on to emit once-per-second aggregate data-channel, authority-drop, input-queue, and injection timing counters while display input is active

Dashboard dev override

VariableDefaultDescription
INTENDANT_APP_HTML_PATHunsetServe the dashboard entry point from this file, re-read on every request, instead of the embedded copy

Development-only: point it at a checkout’s static/app.html, then iterate with cargo run -p app-html-assembler after each fragment edit — the next browser refresh picks up the change with no daemon rebuild or restart. The disk copy gets the same ?v= asset-URL rewriting as the embedded one; everything else (WASM, vendored JS/CSS, icons) stays embedded and still needs a normal build, except /vault-kernel.js: when a sibling vault-kernel.js exists beside the overridden app.html, the gateway also re-reads that file on every request so its bytes match the assembler-injected hash pin. A missing or unreadable sibling falls back to the embedded kernel, whose mismatch the page’s integrity check will reject. An app.html read failure serves a loud 500 naming the override rather than silently falling back to the embedded copy, and the gateway logs the active override at startup.

Session-log retention variables

VariableDefaultDescription
INTENDANT_LOG_MESSAGES_JSONunset (off)Write the per-turn full-conversation dump turns/turn_NNN_messages.json. Off by default — the context snapshot already archives the exact provider request. The dump is still written automatically, gate or no gate, when a provider cannot produce a request snapshot (mock/custom providers), so every turn keeps at least one exact input record
INTENDANT_CONTEXT_SNAPSHOT_KEEP_ALLunset (rotate)Keep every per-turn context-snapshot sidecar instead of rotating to the latest one per (source, session id) stream. Debugging aid — latest-only rotation is what keeps per-session context disk O(1) instead of O(turns × context)

Process-plumbing variables

Sub-agents no longer travel through the environment — they are supervised sessions spawned in-process via the spawn_sub_agent tool (see Multi-Agent Orchestration), so the old INTENDANT_ROLE / INTENDANT_ID / INTENDANT_RESULT_FILE / INTENDANT_PROGRESS_FILE family is gone. What remains:

VariableDescription
INTENDANT_TASKTask description fallback when no CLI task argument is given
INTENDANT_SYSTEM_PROMPTReplaces the resolved system prompt wholesale for a direct CLI invocation (escape hatch; per-session overrides use spawn_sub_agent’s system_prompt)
INTENDANT_PROJECT_ROOTExplicit project root for MCP helper surfaces that otherwise use the current directory; selects the controller-loop state directory and the project-local live-audio prompt override
INTENDANT_SANDBOX_WRITE_PATHSSandbox write paths (set by the caller when sandboxing; enforced by Landlock on Linux, Seatbelt on macOS, restricted tokens on Windows)
INTENDANT_LOG_DIRSession log directory (set by the caller for the runtime)

intendant.toml

Create intendant.toml in your project root (the git top-level). A daemon started without a project marker reads and writes the same schema at <INTENDANT_HOME>/intendant.toml; the dashboard’s Settings page uses that daemon-wide file while /api/project-root remains null. Every section is optional; an absent section uses its defaults. The structure and defaults below are taken directly from src/bin/caller/project.rs.

Memory is no longer project configuration: the legacy [memory] table and .intendant/memory.json store were removed. Owner-plane Memory durability is selected at daemon startup by platform and INTENDANT_MEMORY_EPHEMERAL, as described above.

[model]

KeyTypeDefaultDescription
context_windowintper-modelOverride the model’s context window (tokens)
max_output_tokensintper-modelOverride max output tokens per call

[orchestrator]

KeyTypeDefaultDescription
max_parallel_agentsint4Cap on concurrently running sub-agent children per parent session; spawn_sub_agent refuses beyond it

[approval]

Per-category approval rules. Each value is auto (run without asking), ask (prompt the human), or deny (refuse). These are layered under the global --autonomy level — see Autonomy and approval.

KeyTypeDefaultCategory
file_readruleautoFileRead
file_writeruleaskFileWrite
file_deleteruleaskFileDelete
command_execruleautoCommandExec
networkruleautoNetworkRequest
destructiveruleaskDestructive
display_controlruleaskDisplayControl
tool_callruleaskToolCall (outbound MCP, skills/orchestration, and external-agent tool/MCP permission requests)

Arbitrary CommandExec is governed by the strictest configured rule among command_exec and every effect a shell can reach: file_read, file_write, file_delete, network, destructive, and display_control (deny > ask > auto). Consequently the default effective shell rule is ask even though the command_exec field itself defaults to auto. This composition is deliberate: shell syntax cannot be classified completely, so effect rules must not be bypassable with indirection or a different executable spelling.

The HumanInput and LiveAudioSpawn categories always require a human and are not configurable here.

[presence]

The conversational presence layer that mediates between you and the worker agent (see Presence Layer).

KeyTypeDefaultDescription
enabledbooltrueEnable the presence layer
providerstringauto-detectProvider for text-mode presence
modelstringgemini-3-flash-previewText-mode presence model
context_windowint1048576Context window for text-mode presence
live_providerstringauto-detectProvider for browser-side live (voice) presence
live_modelstringprovider defaultLive presence model
live_context_windowint32768Context window for live presence

The compiled-in default text presence model is gemini-3-flash-preview. Text presence auto-detection prefers Gemini when GEMINI_API_KEY is set.

[transcription]

Server-side speech-to-text via the Whisper API (or a compatible endpoint). See Web Dashboard.

KeyTypeDefaultDescription
enabledboolfalse (within a present section)Enable server-side transcription
providerstringopenaiTranscription provider
modelstringwhisper-1Transcription model
endpointstringOpenAI defaultCustom endpoint URL (e.g. self-hosted whisper.cpp)
languagestringauto-detectISO-639-1 language hint
buffer_secsfloat3.0Audio buffered before each API call (seconds)

Note on the enabled default: when the entire [transcription] section is absent, the field’s struct default applies. When the section is present but enabled is omitted, the bool defaults to false. The CLI --transcription flag and a present enabled = true both turn it on.

[recording]

ffmpeg-based recording of agent displays (see Integrations).

KeyTypeDefaultDescription
enabledboolfalseEnable display recording
framerateint15Capture frames per second
segment_duration_secsint60Length of each recording segment
qualitystringmediumlow (CRF 35), medium (CRF 28), high (CRF 20)
max_retention_hoursintunsetAuto-delete segments older than this

[computer_use]

Provider/model used for visual-grounding (computer-use) tasks. See Computer Use & Live Audio. The separate provider/model selection matters for frame-grounded CU dispatches and for the vaulted CU-first routing (below); the dashboard hides the selection rows while that routing is off.

KeyTypeDefaultDescription
providerstringauto-detectCU model provider
modelstringauto-detectCU model
backendstringautoInput/screenshot backend: x11, wayland, macos, windows, or auto

[experimental]

Vaulted features: kept runnable in the tree, all off by default, and production behavior must not depend on them.

KeyTypeDefaultDescription
cu_first_routingboolfalseIntercept every non-direct task with a fast CU model that completes it on the display or escalates to the main agent (vaulted 2026-07-04: adds a model hop to every task and, under subscription-based external agents, an API-key model dependency)

[readopt]

The boot auto-readopt pass: at daemon boot, the dead boot’s mid-work sessions (started-without-terminal agenda occurrences, mid-turn interruptions, limit-parked wrappers with pending work) are resume-attached under fresh wrappers with a continuation nudge, under the existing admission guards (see Agenda & Memory). Idle or completed sessions stay down — except through the unfinished-commission sweep that runs after the mid-work loop under this same knob: an idle seat whose agenda commission is still open (occurrence fail-closed by this boot, unattested, no live process) is woken to continue, and what cannot be woken is listed on one needs-you agenda task instead of stranding silently (failed runs are listed, never re-fired).

KeyTypeDefaultDescription
enabledbooltruefalse leaves dead-boot sessions down for the owner to resume by hand and skips the commission sweep. INTENDANT_BOOT_READOPT=0/1 overrides the file in either direction

[agent] and external backends

Routes coding tasks to an external CLI agent instead of the native loop (see Integrations).

[agent]:

KeyTypeDefaultDescription
default_backendstringunset (use native)codex, claude-code, kimi, or pi

[agent.codex]:

KeyTypeDefaultDescription
commandstringcodexPath or command name for the Codex binary
managed_commandstringunset (fall back to command)Path or command name for an Intendant-aware Codex fork. Managed-context sessions prefer this binary; vanilla sessions always use command. Leaving it unset preserves legacy setups but makes managed-fork capability ambiguous in the dashboard
modelstringunsetModel override
approval_policystringon-requestuntrusted, on-request, or never (UI set; on-failure is deprecated upstream)
sandboxstringworkspace-writeread-only, workspace-write, or danger-full-access
reasoning_effortstringunset (model default)none, minimal, low, medium, high, xhigh, max, or ultra (model-dependent; ultra enables automatic task delegation on supporting Codex models)
service_tierstringunset (inherit Codex default)priority enables Fast, flex requests Flex, standard is a sentinel that sends an explicit serviceTier: null to opt managed sessions out of Fast
web_searchboolfalseEnable the Responses-API web_search tool (codex --search)
network_accessboolfalseAllow outbound network in workspace-write sandbox (ignored for read-only / danger-full-access)
writable_rootsarray[]Extra writable roots, each passed as --add-dir (absolute, or resolved against project root)
managed_contextstringvanillavanilla for upstream/original-fork Codex; managed enables proactive Intendant context densification, rewind/backout tools, disables Codex auto-compaction, and requires the patched Codex app-server protocol with lineage prompt-cache-key support
context_archivestringsummaryContext snapshot archive mode (“Context replay” in the UI): summary records compact per-request visualization data with temporary provider traces, exact persists full provider request payloads for raw replay, off disables capture

context_recovery is accepted as a deprecated TOML alias for managed_context. New configs must use managed_context.

Codex app-server launches in managed_context = "managed" suppress inherited user-global Codex MCP/plugin/app servers by default and inject Intendant’s MCP endpoint plus the explicit toggles above. Set INTENDANT_CODEX_INHERIT_MCP_SERVERS=1 only for a managed launch that should inherit the user’s configured Codex MCP servers and plugins. Vanilla launches preserve Codex’s normal user configuration inheritance.

[agent.claude_code]:

KeyTypeDefaultDescription
commandstringclaudePath or command name
modelstringunsetModel override — an alias (fable, opus, sonnet, haiku; the CLI resolves it to the latest model) or a full model id
permission_modestringdefaultdefault (alias manual), acceptEdits, plan, auto (classifier-based approvals), dontAsk (auto-deny anything that would prompt), bypassPermissions. Semantics change: before Claude Code 2.1.206 Intendant coerced auto to default; it now selects the CLI’s real auto-approval mode, and the config load warns when it finds auto so an old config doesn’t escalate silently
effortstringunsetDaemon-default reasoning effort passed as --effort: low, medium, high, xhigh, max, ultracode (unset omits the flag). Settable live from the Settings tab’s “Claude reasoning” row (SetClaudeEffort); every launch lane (New Session, ctl task, scheduled agenda spawns) resolves explicit per-create pin → this default → the CLI’s model default
allowed_toolsarray[] (all)Restrict the tool set
max_budget_usdfloatunsetCLI-enforced dollar backstop passed as --max-budget-usd; cumulative for the session, its resumes, AND its forks — a forked or /btw side child inherits the parent’s counted spend (probed 2.1.206), so children of an exhausted parent fail immediately with the same hint. On exceed every further turn fails with error_max_budget_usd (surfaced as a backend error with a recovery hint). Must be positive: a zero/negative/non-finite value refuses the spawn instead of silently disarming (the CLI itself rejects --max-budget-usd 0)

[agent.kimi]:

KeyTypeDefaultDescription
commandstringkimiPath or command name for the Kimi Code binary
modelstringunsetModel override. Blank inherits Kimi’s configured default. The installed subscription catalog includes kimi-code/kimi-for-coding (K2.7 Coding), kimi-code/kimi-for-coding-highspeed (K2.7 Coding Highspeed), and kimi-code/k3 (K3); a custom configured id is also accepted
thinkingstringunsetThinking-effort override. Blank/inherit leaves the selected model’s Kimi default in control
permission_modestringmanualmanual (interactive approval), auto, or yolo (bypass). default is accepted as an alias for manual
plan_modeboolfalseStart new Kimi sessions in native plan mode
swarm_modeboolfalseEnable Kimi’s native swarm/sub-agent mode
allowed_toolsarray or unsetunsetExact active-tool names applied through Kimi’s authenticated agent RPC. Unset leaves the current Kimi profile in control; [] intentionally disables every optional tool. This is not Claude’s approval allowlist, whose empty value means unrestricted

All seven Kimi fields can be pinned per session. Model, thinking, permission, plan, swarm, and active-tool changes apply live to an attached session; changing the command requires restart. Intendant runs a private kimi server beneath a per-session bridge home, so concurrent Kimi sessions do not contend for the primary home’s server lock and Intendant never edits the user’s ~/.kimi-code/mcp.json.

[agent.pi]:

KeyTypeDefaultDescription
commandstringpiPath or command name for the Pi binary
modelstringunsetPi model pattern or provider/model id. Blank inherits Pi’s configured profile
thinkingstringunsetoff, minimal, low, medium, high, xhigh, or max; blank/inherit leaves the selected model’s Pi default in control. Unknown non-empty values are retained for forward compatibility and rejected visibly by Pi if unsupported
allowed_toolsarray or unsetunsetExact active Pi tool names at process launch. Unset keeps Pi’s profile, [] deliberately starts with no tools, and a non-empty array replaces the active set

All four Pi fields can be pinned per session. An exact model id (optionally provider/model) and thinking level apply live to an attached session and are persisted back into that session’s launch overlay; launch-only model patterns, command, and tools require restart. Global Settings writes [agent.pi] directly rather than maintaining a daemon-wide Pi-shaped runtime mirror: new sessions load TOML, reattached sessions load it as their base and reapply their persisted overlay, while active ones use Pi RPC. Supervision disables discovered extensions, loads one private approval extension, preserves normal AGENTS.md/CLAUDE.md context, and uses $INTENDANT ctl for platform capabilities because upstream Pi has no built-in MCP.

Unknown or empty values for authority-shaped fields such as Codex approval_policy/sandbox and Kimi permission_mode are normalized to their safe defaults so a config typo cannot silently escalate privileges.

[live_audio]

Untrusted voice sub-agent (zero tools, schema-validated) used for phone calls and live voice (see Computer Use & Live Audio).

KeyTypeDefaultDescription
enabledboolfalseEnable live-audio sessions
default_timeout_secsint300Session timeout
gemini_modelstringunsetGemini Live model
openai_modelstringunsetOpenAI Realtime model
sample_rateint24000Audio sample rate (Hz)

[sandbox]

Filesystem write sandboxing for the runtime — Landlock on Linux, Seatbelt on macOS, restricted tokens on Windows. On by default on macOS and Linux; opt-in on Windows, where a write grant is an inheritable ACE and granting a large tree rewrites every descendant’s DACL synchronously at startup — enabling it there accepts that first-stamp cost. Resolution order: --sandbox forces on, --no-sandbox forces off, otherwise an explicit enabled here decides, otherwise the platform default.

KeyTypeDefaultDescription
enabledboolunset (= platform default)Explicitly enable/disable filesystem sandboxing; the CLI flags override it
extra_write_pathsarray[]Extra writable paths beyond the default grant set: project root (plus the session’s own project root for dashboard-picked projects), the OS scratch dir (TMPDIR / /tmp / %TEMP%), the session log dir and the state root’s logs/ subtree, and — Unix only — the toolchain caches (~/.cargo, ~/.rustup, the user cache dir). The state root itself is deliberately not granted: it also holds access credentials, leased auth, and custody state. On Windows toolchain-cache writes are instead granted on demand through the denial-consent card

The dashboard’s Settings → Security card edits both values live (new commands pick the change up without a restart; a --sandbox/--no-sandbox flag pins the live state for that daemon run, so saves then only persist intent), and approving a sandbox write-denial consent prompt can append grants — for the session, or persisted here via “always allow”. On Windows, an extra-path-only Settings save currently resolves an omitted enabled value as true, not the Windows platform default, and does not perform startup’s full ACE-stamping lifecycle. Treat that as a Settings defect: include the intended enabled state explicitly and restart after changing Windows sandbox grants.

Once the sandbox reaches a runtime, an enforcement failure aborts that run rather than continuing unconfined: for example, a Linux kernel without Landlock support refuses the runtime until --no-sandbox (or enabled = false) makes the opt-out explicit, and Windows restricted-token re-exec fails closed. One controller-edge caveat remains: if startup cannot encode the write-grant environment, it currently logs the error, removes the sandbox variable, and continues; a Windows ACE-stamping failure instead leaves the restricted runtime unable to write. Reads stay open except the credential carve-outs (macOS denies ~/.ssh, ~/.gnupg, the intendant config home, the .env search path, and the native ChatGPT auth/ store at the OS layer; on Linux, Landlock cannot express read carve-outs under a broad read grant, so project/config .env files remain readable — credential custody is the tracked fix).

[webrtc]

ICE servers for the WebRTC display transport (see Display Pipeline).

KeyTypeDefaultDescription
ice_serversarray of tables[] (local-only)STUN/TURN servers
federation_allow_h264boolfalseAllow the federated (peer-to-peer) display path to negotiate H.264; default pins VP8 for lossy TURN-relayed paths. The local same-machine path is unaffected

Each ice_servers entry: urls (array, required), optional username, optional credential.

The Connect SNI relay carries daemon-terminated HTTPS/WSS control, not raw WebRTC media. A hosted lease may view or drive only agent-visible displays, and remote media requires either successful direct ICE or a configured TURN entry. Production TURN credentials should be short-lived.

[connect]

Intendant Connect client for public-origin account/route discovery. This is disabled by default and does not replace local/offline dashboard mTLS. When enabled, the daemon registers signed presence and route-code metadata and polls the rendezvous mailbox. The retired Connect browser offer/ICE/close path remains refused before dashboard-control, IAM, or enrollment mutation. A separate, restart-only flag can enable the bounded fleet-origin lease lane without changing that refusal or the Connect account’s role:none posture.

KeyTypeDefaultDescription
enabledboolfalseEnable outbound Connect rendezvous polling
rendezvous_urlstringhttps://connect.intendant.dev when enabledBase URL of the Connect/rendezvous service (the hosted instance unless overridden)
daemon_idstringdaemon identity public keyPublic daemon id at the rendezvous service
auth_tokenstringunsetOptional bearer token for daemon-to-service authentication; not dashboard authorization
poll_timeout_msinteger15000Long-poll timeout per daemon /next request
retry_delay_msinteger1000Delay after transient rendezvous errors
relay_enabledboolfalseHold a reachability-relay control channel to Connect so this daemon’s NAT’d fleet name is reachable through the relay’s SNI passthrough (docs/src/self-hosted-rendezvous.md). Custom exact-name routes additionally prove possession of their publicly trusted certificate key. Dial-backs terminate on a dedicated loopback-only gateway ingress and cannot enter the trusted-local lane. Requires relay_endpoint
relay_endpointstringunsethost:port of the relay’s raw passthrough port, where this daemon dials back browser connections (e.g. relay.example.com:443)
hosted_control_enabledboolfalseEnable the daemon-local hosted lease doorbell and exact lease-proof/ticket carve on fleet/relay ingress. Restart-only; deliberately has no environment-variable override
relay_peer_admissionboolfalseAdmit this daemon’s paired peer daemons (Approved, unexpired peer identity records) through fleet-name/relay ingress into the ordinary peer transport-auth ladder; peer profiles stay the sole control-depth authority (docs/src/self-hosted-rendezvous.md § Relay peer admission). With the relay live, also auto-appends the fleet-name relay candidate ("relay": true) last on the agent card once registration reports the fleet name — operator advertise entries keep walk-order precedence. Independent of hosted_control_enabled in both directions. Restart-only; deliberately has no environment-variable override
custom_domain.enabledboolfalseEnable the separate user-owned-name lane. Restart-only; no environment-variable override
custom_domain.namestringunsetExact ASCII/punycode DNS name served by the daemon
custom_domain.rp_idstringcustom_domain.nameWebAuthn relying-party id; when present it must equal the exact name
custom_domain.acme_issuance_enabledboolfalseAllow certificate orders after the displayed ACME account URI has been pinned in CAA

[connect.custom_domain.dns] selects the DNS-01 provider. provider = "cloudflare" requires zone_id, with optional token_env (default CLOUDFLARE_API_TOKEN) and propagation_delay_secs (default 10, maximum 3600). provider = "rfc2136" requires server, zone, and key_name, with optional secret_env (default INTENDANT_RFC2136_TSIG_SECRET), ttl_secs (default 60), and propagation_delay_secs (default 2, maximum 3600). Provider secrets come from the dns:cloudflare / dns:rfc2136 daemon credential lease or the named environment fallback, never this table. Alternate names must end in _API_TOKEN or _TSIG_SECRET respectively so they remain excluded from runtime children; custom names cannot occupy the reserved INTENDANT_ namespace. See Your Fleet, Your Name for CNAME, CAA account pinning, and passkey enrollment.

INTENDANT_CONNECT_RELAY_ENDPOINT force-enables the relay tunnel and sets relay_endpoint. The relay is availability-only: it never terminates TLS, holds no certificate, and grants no authority — a relayed browser connection reaches this daemon bearing immutable relay/fleet provenance. It stays discovery-only unless hosted_control_enabled is true and the request proves an active hosted lease, or relay_peer_admission is true and the connection presents an active paired peer identity; for everything else /mcp, direct signaling, and trusted-local fallback remain unavailable.

No file editing is required for the common case: the dashboard’s Access → Intendant Connect card toggles enabled (persisting it here), shows registration/link state, reveals the single-use twelve-word locally minted claim code to trusted manage-grade sessions, and can release the account/route link. The INTENDANT_CONNECT_RENDEZVOUS_URL environment variable still force-enables the client and overrides the file (the card reports when it does). Hosted control uses its own Access → Hosted control card: an owner can set the ceiling, review pending requests, revoke active leases, and mark sessions eligible. That ceremony is not part of the Connect toggle.

Projectless daemons (the bundled macOS app’s normal shape — no .git/intendant.toml at the launch directory) keep general defaults in the daemon-scoped intendant.toml, but Connect’s credential-bearing table remains in its dedicated owner-only connect.toml beside it. The toggle and daemon boot both use that dedicated file. Rooted daemons are unaffected.

Hosted Connect uses the same daemon-side settings:

[connect]
enabled = true
rendezvous_url = "https://connect.intendant.dev"
daemon_id = "vortex-deb-x11-intendant"
auth_token = "same daemon token configured on intendant-connect"
hosted_control_enabled = false

[connect.custom_domain]
enabled = false
name = "box.example.com"
acme_issuance_enabled = false

[connect.custom_domain.dns]
provider = "cloudflare"
zone_id = "your-zone-id"
token_env = "CLOUDFLARE_API_TOKEN"

The service is a separate binary:

INTENDANT_CONNECT_TOKEN="shared daemon bearer token" \
  ./target/release/intendant-connect \
    --listen 127.0.0.1:9876 \
    --origin https://connect.intendant.dev \
    --rp-id intendant.dev \
    --data-file <state-file>

--static-root PATH and INTENDANT_CONNECT_STATIC_ROOT are deprecated compatibility inputs. They are accepted (a missing flag value still errors) but ignored. The default Connect binary serves only explicit, compile-time embedded routes and assets; it never mounts a filesystem fallback. In particular, a supplied directory cannot expose app.html, WASM, or vault-kernel.js from the hosted origin.

The service-side reachability relay is off by default and gated on an all-or-nothing flag group (mirroring the --dns-* fleet-DNS group):

Flag / envDescription
--relay-listen / INTENDANT_CONNECT_RELAY_LISTENRaw TCP listen address for the SNI-passthrough relay (e.g. 0.0.0.0:443). Must receive raw TLS — do not front it with a TLS-terminating proxy
--relay-address / INTENDANT_CONNECT_RELAY_ADDRESSComma-separated public address(es) the relay is reachable at, published in fleet DNS for relay-mode daemons

Both are required together or neither. See docs/src/self-hosted-rendezvous.md for the full deployment description.

intendant-connect is intended to sit behind public TLS for the configured origin. It handles passkey-only account sessions, single-use account/route linking, daemon list/release/label UI, account-backed fleet target metadata, and a capped audit log. /app always redirects to /connect. When a daemon’s signed registration says hosted control is enabled and it has a relay-mode fleet-DNS route, its card may show Request control; that action only navigates to the daemon’s public fleet-origin doorbell. A separately remembered, client-signed and passkey-decrypted direct route may show Open direct route, which merely navigates away from Connect to that daemon’s mTLS origin and grants nothing by itself. A Connect account assertion never authenticates to the daemon, and no claim, generic grant, or compatibility-ceiling edit can enable hosted control. The explicit daemon flag plus trusted approval of an individual request mints the only usable hosted principal. The state file durably stores users/passkeys, daemon account links, labels, hashed claim codes, account-scoped fleet navigation records, and audit events. The daemon locally generates each single-use 12-word BIP39 code and signs a fresh registration containing only its SHA-256 hash. Its printed URL carries the phrase in /connect#claim_code=...; the browser scrubs the fragment, hashes locally, and sends only the digest. Connect never receives or returns plaintext, and its claim API rejects plaintext/query compatibility. WebAuthn challenge state, rate limits, web sessions, and short-lived daemon-session credentials are memory-only.

For new *.intendant.dev deployments, the default RP ID is intendant.dev, so passkeys can be scoped to the owned parent domain. The current connect.intendant.dev production-alpha instance was originally launched with INTENDANT_CONNECT_RP_ID=connect.intendant.dev; keep that setting until the alpha account is deliberately migrated, because changing RP ID invalidates previously registered passkeys and requires users to register new credentials.

For production alpha, terminate public TLS at a reverse proxy and keep intendant-connect bound to 127.0.0.1. The proxy should forward Host, set X-Forwarded-For/X-Real-IP, and strip any inbound copies of those headers before setting them because the service uses them for simple rate-limit buckets. Use /healthz for liveness and /readyz for readiness. Back up the state file and store INTENDANT_CONNECT_TOKEN in the deployment secret store.

Cookie-backed user mutations require a same-origin request and the per-session CSRF token returned by /api/me. The bundled Connect UI sets that header automatically, including hosted fleet sync through /api/fleet/targets/sync and fleet-record deletion through /api/fleet/targets/{target_id}/forget.

Production-alpha operations are intentionally boring and repeatable, but live target details are intentionally not tracked in this public repository. Keep the host, SSH user, key path, remote source directory, systemd service name, state file, and local readiness URL in a private operator env file or pass them with the matching command-line flags:

cat > ~/.config/intendant/connect-prod-alpha.env <<'EOF'
CONNECT_HOST=<ssh-host>
CONNECT_SSH_USER=<ssh-user>
CONNECT_SSH_KEY=<private-ssh-key-path>
CONNECT_REMOTE_SOURCE=<remote-source-directory>
CONNECT_SERVICE=<systemd-service-name>
CONNECT_REMOTE_READYZ_URL=<local-readiness-url>
CONNECT_REMOTE_STATE=<remote-state-json-path>
CONNECT_PUBLIC_ORIGIN=https://connect.intendant.dev
EOF

CONNECT_OPS_ENV=~/.config/intendant/connect-prod-alpha.env \
  scripts/deploy-connect-prod-alpha.sh

The deploy script refuses to run without the private target values. It does not copy or print the daemon bearer token; the token remains in the remote systemd environment.

Backups should be encrypted because the state file is the authoritative account and route registry. It stores account handles, passkey public-key records, daemon account links and labels, hashed claim codes, and audit entries. It does not store plaintext claim codes, WebAuthn challenges, active browser sessions, pending offers, routing tokens, or rate-limit buckets. Create an encrypted backup with:

CONNECT_OPS_ENV=~/.config/intendant/connect-prod-alpha.env \
  scripts/connect-state-backup.sh \
  --passphrase-file ~/.config/intendant/connect-backup.passphrase

The backup is written under ~/.local/share/intendant/connect-backups/ by default, alongside a SHA-256 checksum file. Plaintext backup is available only with an explicit --allow-plaintext flag for local diagnostics.

Restore is deliberately explicit because it replaces the production state file, takes a pre-restore backup on the host, restarts the service, and verifies readiness:

scripts/connect-state-restore.sh --yes \
  --passphrase-file ~/.config/intendant/connect-backup.passphrase \
  ~/.local/share/intendant/connect-backups/intendant-connect-state-YYYYMMDDTHHMMSSZ.json.enc

After a restore, existing Connect account sessions are gone because those web sessions are memory-only. Linked daemon routes remain associated with the restored account state. Direct daemon dashboards are separate and are not Connect sessions. The restored association is not daemon ownership or IAM authority.

The service accepts these deployment flags and equivalent environment variables:

EnvEquivalent flagDefaultDescription
INTENDANT_CONNECT_LISTEN--listen127.0.0.1:9876HTTP listen address
INTENDANT_CONNECT_ORIGIN--originhttp://localhost:<port>Public browser origin for redirects, install snippets, and WebAuthn origin checks
INTENDANT_CONNECT_RP_ID--rp-idorigin host, or intendant.dev for *.intendant.devWebAuthn relying-party id
INTENDANT_CONNECT_STATIC_ROOT--static-rootignoredDeprecated compatibility input. Accepted but never read or mounted; Connect serves only its explicit embedded allowlist
INTENDANT_CONNECT_DATA_FILE--data-fileplatform data dir intendant/connect/state.jsonJSON state file
INTENDANT_CONNECT_TOKEN--daemon-tokenunsetShared deployment bearer for registration unless open registration is enabled; still guards admin surfaces. It does not replace the daemon’s signed registration proof or rotating daemon-session credential
INTENDANT_CONNECT_RELEASE_TOKEN--release-tokenunsetDedicated bearer for POST /api/log/release-manifest. Deliberately separate from the operator/admin daemon token; without it, release-manifest submission returns 503
INTENDANT_CONNECT_INVITE_REQUIRED--invite-requiredfalseRequire a valid invite code for new account registration
INTENDANT_CONNECT_OPEN_REGISTRATION--open-registrationfalseSkip only the shared deployment bearer on registration. Daemons still sign a fresh key-possession proof, and successful registration rotates a short-lived daemon-session credential required by poll/answer/error/dry/claim-proof endpoints
INTENDANT_CONNECT_DNS_ZONE--dns-zoneunsetDelegated fleet DNS zone. Must be configured together with INTENDANT_CONNECT_DNS_NS_NAME and INTENDANT_CONNECT_DNS_LISTEN
INTENDANT_CONNECT_DNS_NS_NAME--dns-ns-nameunsetAuthoritative nameserver host served in the fleet-zone apex SOA/NS records
INTENDANT_CONNECT_DNS_LISTEN--dns-listenunsetUDP+TCP bind address for the embedded authoritative DNS server
INTENDANT_CONNECT_RELAY_LISTEN--relay-listenunsetRaw TCP bind address for the TLS/SNI passthrough relay. Must be configured together with at least one relay address
INTENDANT_CONNECT_RELAY_ADDRESS--relay-addressunsetComma-separated public IP list published for relay-mode fleet DNS

The service also accepts environment-only operational overrides for self-hosting and tests:

EnvDefaultDescription
INTENDANT_CONNECT_DOH_URLhttps://cloudflare-dns.com/dns-queryDNS-over-HTTPS endpoint used for _intendant.<domain> TXT attestation
INTENDANT_CONNECT_GIST_BASEhttps://gist.githubusercontent.com/Allowed raw gist URL prefix for GitHub handle attestation
INTENDANT_CONNECT_PRESENCE_OFFLINE_MS180000Linked daemon polling gap that counts as offline for presence alerts
INTENDANT_CONNECT_PRESENCE_POLL_MS30000Presence-alert monitor poll interval
INTENDANT_CONNECT_RECLAIM_AFTER_MS0 (off)Dormant-handle reclamation threshold for accounts with no linked daemon routes and no recent sign-in
INTENDANT_CONNECT_RECLAIM_POLL_MS21600000Dormant-handle reclamation poll interval

For local E2E without editing intendant.toml, the daemon also accepts environment overrides:

INTENDANT_CONNECT_RENDEZVOUS_URL=http://127.0.0.1:9876 \
INTENDANT_CONNECT_DAEMON_ID=connect-e2e-daemon \
INTENDANT_CONNECT_TOKEN=shared-dev-token \
  ./target/release/intendant --web 8876

There are three committed validators. The hosted production-alpha validator starts intendant-connect, a daemon, and a browser virtual authenticator:

node scripts/validate-connect-hosted-mvp.cjs

The protocol-emulator validator starts a local rendezvous HTTP origin:

PLAYWRIGHT_NODE_PATH=/path/to/node_modules \
  node scripts/validate-connect-rendezvous.cjs

The emulator intentionally has no account signup, passkey ceremony, claim code, durable device registry, or authorization policy of its own. Its hosted-origin pass is now negative-only: it injects legacy control events and expects the daemon to refuse them before mutation. Direct/local validators cover authenticated dashboard-control transport separately. The hosted MVP validator covers account/passkey flow, authority-free route linking, service-side 403 with no enqueue, release, and audit.

The transport validator drives the fresh-box ceremony end to end — passkey signup, fragment-only one-time discovery link, retired /app redirect, and service/daemon refusal — and asserts that even a local browser-key grant cannot create a legacy hosted control session while the new feature flag is off. The gateway unit/E2E hosted_lease_is_the_only_relay_control_authority covers the enabled lane’s signed doorbell, trusted approval, proof replay refusal, one-use WebSocket ticket, action wall, and revocation closure through the real relay ingress. Lower-level direct/local harnesses cover the ICE/DataChannel paths (see Self-Hosted Rendezvous → End-to-end transport validation):

node scripts/connect-transport-e2e.cjs

[server] (daemon and federation)

What this daemon advertises to peers and requires of inbound connections. Those are related but independently configured; in particular the Agent Card’s advertised transport is not inferred from the gateway’s TLS mode. Most deployments only ever touch [server.tls].

[server]:

KeyTypeDefaultDescription
bindstring/IPwildcard dual-stack, then 0.0.0.0 fallbackIP address the dashboard listens on. Use 127.0.0.1 or a specific interface for local/plaintext automation
advertisearray[] (auto-detect)WebSocket URLs to advertise in this daemon’s Agent Card, preference order. Repeatable CLI --advertise-url replaces this list entirely. The selected CLI or config list is prepended to auto-detected fallback URLs

[server.tls] — native TLS-only HTTPS/WSS for the dashboard (pure-Rust rustls + rcgen, all platforms; ORed with the --tls flag). The dashboard defaults to mTLS. This section supplies HTTPS/WSS and a browser secure context, but it does not give a certless remote browser daemon authority: only loopback certless requests receive the local-root posture. TLS-only mode does not load a client CA, so protected remote HTTP/WebSocket/control requests are denied unless they carry a separately authenticated peer or hosted-lease identity; use the default mTLS mode when client-certificate authentication is required:

KeyTypeDefaultDescription
enabledboolfalseEnable TLS-only HTTPS/WSS. Certless access is local-root only on loopback; remote protected routes require an authenticated peer/hosted-lease identity and ordinary client certificates cannot authenticate because this mode loads no client CA
certstringinstalled access certs, then auto self-signedPEM cert (chain) overriding the default cert selection; pair with key
keystringPEM private key (PKCS#8, PKCS#1, or SEC1) matching cert
hostnamestringExtra SAN hostname for the self-signed cert (in addition to bind IP + localhost)

When TLS-only mode is enabled and cert/key are omitted, Intendant first looks for the installed access server certificate in the per-user platform cert directory (server.crt / server.key, normally created by intendant access setup). If that pair is absent, it falls back to an ephemeral self-signed certificate.

[server.mtls] — native client-certificate authentication for the dashboard (ORed with the --mtls flag; this is the default dashboard transport):

KeyTypeDefaultDescription
enabledboolfalseEnable client-certificate authentication (also the default dashboard transport unless --tls / [server.tls] or --no-tls is used). TLS accepts a certless handshake so public shell/discovery bytes remain reachable; protected HTTP routes and every ordinary WebSocket then require a CA-verified client certificate or another authenticated peer/hosted-lease identity
castringinstalled access CAPEM CA bundle used to verify client certificates

Use [server.tls] for a secure context on loopback or for public/bootstrap content, not as a remote authentication bypass. Use default mTLS or [server.mtls] for a remote dashboard with daemon authority.

Use default mTLS, [server.tls], or --tls when a remote browser needs secure-context-gated features: Station WebGPU, microphone/camera, browser screen capture, or stricter clipboard APIs. An HTTPS reverse proxy can provide a secure context for public bytes, but ordinary TLS termination does not authenticate a controller. A proxy that forwards to daemon loopback becomes a root trust boundary: remote control is safe only when the proxy enforces an approved client identity and its upstream is protected from other local callers. A local development build of the packaged macOS app also supplies a secure context for its bundled daemon, but the current unsigned artifact is not a distribution anchor. Plain http://<host-ip> is not enough for those APIs, and --no-tls on a wildcard listener refuses startup when the host has a public interface unless --allow-public-plaintext is passed. The macOS app wrapper starts its bundled backend with native mTLS by default and fails closed with setup guidance when access certs are missing; see Web Dashboard: Secure Browser Contexts. Neither --tls nor --allow-public-plaintext synthesizes TrustedLocal for a remote caller. A custom Origin is routing metadata, not authentication.

Peer access requests use the unauthenticated /api/peer-pairing/requests doorbell endpoint so one daemon can ask another for pairing approval. It is bounded and rate-limited, and approval still happens locally before any client certificate is issued. Set INTENDANT_PEER_ACCESS_REQUESTS=0 to disable public request creation entirely.

[server.peer_access_requests] — public access-request hardening:

KeyTypeDefaultDescription
enabledbooltrueAllow unauthenticated callers to create bounded pending peer access requests; INTENDANT_PEER_ACCESS_REQUESTS=0 still disables this at runtime
body_limit_bytesinteger4096Maximum body size for POST /api/peer-pairing/requests
ttl_secsinteger600Lifetime of a pending request before it expires
max_pendinginteger32Global cap on simultaneously pending requests
max_pending_per_sourceinteger5Cap on simultaneously pending requests from one source IP/hint
rate_limit_window_secsinteger60Sliding-window duration for create-rate limits
max_creates_per_windowinteger64Global request creations allowed per rate-limit window
max_creates_per_source_per_windowinteger8Request creations allowed from one source IP/hint per rate-limit window

[server.auth] — advanced compatibility auth for federation peers:

KeyTypeDefaultDescription
advertised_transportstringnoneWhat the Agent Card advertises: none, mutual-tls, or pin-self-cert
bearer_tokenstringnoneLegacy/advanced: require Authorization: Bearer <token> on inbound HTTP/WS; prefer mTLS/client certificates for normal access

advertised_transport is declarative rather than derived. Its compatibility default remains none even though the dashboard gateway now defaults to mTLS. Federation deployments using that default gateway must set advertised_transport = "mutual-tls" (or pin-self-cert) so the Agent Card does not understate what a connecting peer must present.

[[peer]] — federated peers

Each [[peer]] block auto-registers a remote daemon at startup. Only card_url is required.

KeyTypeDefaultDescription
card_urlstring(required)URL of the peer’s Agent Card (.../.well-known/agent-card.json)
labelstringfrom cardDisplay label override in the dashboard’s Access targets
bearer_tokenstringnoneLegacy/advanced outbound token for peers that still require [server.auth] bearer_token
via_urlsarray[]Connecting-side WebSocket URL overrides; when set, these replace the transports advertised by the peer’s Agent Card
client_certstringinstalled access client cert when presentPeer-issued client certificate PEM for outbound mTLS; must be paired with client_key
client_keystringinstalled access client key when presentPrivate key PEM for client_cert; must be paired with client_cert
pinned_fingerprintsarray[]Operator-pinned SHA-256 cert fingerprints; when set, replaces the card’s auth.transport claim
identity_public_keystringnoneThe peer daemon’s Ed25519 identity public key (base64url), persisted by v2 invites and doorbell approvals. When set, DNS-name candidates verify the presented certificate through the peer’s signed identity attestation — rotation-proof for fleet-name and relayed dials — and fail closed without a valid one; IP-literal candidates keep the raw pin. Absent: raw-pin behavior everywhere (docs/src/peer-federation.md § Identity-bound verification)
browser_tcp_via_urlstringfrom primaryExplicit URL the browser uses to reach this peer’s HTTP port for WebRTC ICE-TCP
certificate_witness_vantageunknown, same_lan, or remoteunknownLocal operator statement about this daemon’s network relationship to the peer for certificate-witness weighting. Set remote only for an independently operated outside-network observer; public destination addresses are not sufficient.

Manual dashboard additions live only in the in-memory registry unless the operator checks Save to intendant.toml. Pairing flows are durable by default: intendant peer join <invite> and intendant peer complete <request-id> write these fields plus pinned_fingerprints to intendant.toml. For independent mTLS daemons, configure client_cert / client_key with a client identity issued by the peer’s access CA. The installed local access client cert fallback is only sufficient when the peer trusts the same issuing CA.

These entries describe daemon-to-daemon peer routes. They do not grant browser or Connect user access by themselves. Peer profiles use names such as peer-operator and peer-root; older operator, admin-peer, and peer-daemon values are still accepted as aliases.

In the Access UI and /api/access/overview, a [[peer]] entry appears as a peer-daemon principal with a peer-profile grant to a daemon target. Browser mTLS access is a user/client grant instead; browser-key rows are record-only in this alpha, and a Connect passkey authenticates only the hosted account and route UI. The same page shows both kinds of records, but the config entry only persists the peer route and its daemon-to-daemon credentials. Peer profiles are not human IAM: peer-root maps to peer inspection/management and access inspection, while human/account access management remains owner/root user-client authority.

Local Access/IAM state

Local IAM foundation state is stored beside the native access certificates, normally ~/.intendant/access-certs/iam.json on Unix-like platforms. It is not part of intendant.toml because it belongs to the local daemon identity and may later contain per-device/user audit metadata rather than project configuration.

Schema version 3 contains:

FieldMeaning
schema_versionState schema version; currently 3
principalsLocal managed human/device principal records
rolesBuilt-in or local role templates
grantsLocal IAM grant records targeting daemon IDs. Optional expires_at_unix_ms stops enforcement after that instant (shown as expired); issued_via records a delegated org issuer, and fs_scope confines mediated file reads/writes to normalized roots
audit_eventsLocal IAM audit metadata
role_ceilingsPer-binding-kind effective-role caps for low-provenance sessions (see below)
hosted_originsOrigins treated as hosted app sources when recorded on client-key bindings
trusted_orgsOrganizations whose signed grant documents this daemon accepts, each with a local max_role cap (implemented; see Trust Architecture)
tierOptional owner-selected daemon trust tier (integrated or disposable). It informs doctrine and UI warnings but grants or denies nothing by itself
hosted_controlDaemon-local hosted policy, pending doorbells, active/recent lease records, and qualifying signed-app anchors. Defaults to a Tasks ceiling and empty request/lease/anchor sets

The daemon loads this file into /api/access/overview under the iam object and exposes the raw state through GET /api/access/iam/state. Root dashboard sessions, peer daemon profiles, and active scoped user/client grants pass through the IAM operation evaluator. Shipped alpha browser authentication is loopback/local presence or a browser mTLS certificate presented over an independently verified direct daemon route. client_key WebCrypto records remain in the schema for fleet signatures, attribution, migration, and future identity work, but direct /ws, direct dashboard-control offers, and the reserved future native-bridge code do not authenticate them. A combined human_user principal may carry those records plus optional account/provider and organization metadata. connect_account records are also inert compatibility/audit metadata and never authenticate. A hosted tab key appears only inside an exact hosted_lease principal minted by the dedicated runtime after trusted confirmation; it is not a general client_key login. Loopback sessions and the verified owner browser mTLS certificate keep the root-compatible fallback so direct/self-hosted access remains first-class. Certless remote TLS/plaintext requests do not. Connect’s origin has no dashboard-session path: the service rejects browser signaling and the daemon drops legacy events before key/grant resolution. The optional fleet-origin lease lane instead uses proof-bound daemon HTTP and one-use WebSocket tickets. A key record in draft or revoked status denies instead of falling back to anything else. Root users can create these records through the Access UI, POST /api/access/iam/user-client-grants, or dashboard-control api_access_iam_upsert_user_client_grant; existing records can be activated, drafted, revoked, or role-changed through POST /api/access/iam/grants/update or dashboard-control api_access_iam_update_grant.

The user-client grant upsert request accepts kind = "client_key", "browser_certificate", "human_user", "agent_session", or "local_process". human_user is the local IAM shape for a real person. In the alpha only its mTLS binding is an active browser authenticator; a browser-key binding is record-only. connect_account remains readable in the IAM vocabulary for compatibility and audit, but grant upsert rejects it without mutating IAM state: Connect account links are metadata only and cannot receive a role. Generic grant APIs also reject the reserved hosted principal kind, grant source, and role ids; only hosted-control transactions may write them. client_key grant records take client_key_fingerprint (base64url, case-sensitive), an optional client_key public key for audit, and an optional client_key_origin recorded by the trusted session that creates the grant. The optional account_provider, verified_provider, handle, organization_id, and organization_name fields are local metadata today; the hosted Connect service does not yet verify OAuth providers or organization membership.

Hosted-provenance compatibility state. role_ceilings retains the two historical binding categories and serializes as

{ "connect_account": "role:none", "client_key": "role:none" }

A hosted-origin key may have a legacy grant record, but Connect cannot present or exercise it. Legacy events are rejected before grant resolution, and the compatibility map remains fail-closed. Client keys whose recorded enrollment origin is in hosted_origins (default ["https://connect.intendant.dev"]) are also hosted, as is the retired connect-bootstrap origin. Every load normalizes both entries to role:none; missing, empty, or hand-edited entries cannot enable hosted control.

A typed direct address is trustless first contact, while a fleet-certificate name is daemon-served code on a rendezvous-named route (first-contact rung two). Adding an exact origin to hosted_origins now refuses it at role:none; it is not a lower-authority control mode.

The former compatibility-role cap UI and mutation route are retired. role:none is a zero-permission, ceiling-only builtin — it can never be granted to a principal — and compiled policy is the authority for ambient hosted provenance rather than the persisted map. The separate Hosted control card writes hosted_control.policy: View < Tasks < Operate, default Tasks, with a maximum lease lifetime up to the compiled 24-hour limit. Raising the ceiling is a dedicated owner ceremony; integrated daemons must acknowledge the hardening nudge before Operate. Lowering it revokes leases above the new ceiling; raising it never upgrades an existing lease.

The schema-v2 migration revokes earlier alpha state fail-closed. It revokes every active grant on a principal whose client-key origin is connect-bootstrap, records a revoke_legacy_connect_bootstrap audit event, and restores both hosted ceilings to role:none. Direct root grants survive, and the Connect account/route link remains discovery metadata, but the legacy browser key requires trusted re-enrollment.

Schema v3 adds default hosted_control state without changing direct grants. During an alpha upgrade, restarting only the service cannot tear down an already-established legacy P2P DataChannel. Upgrade/restart the daemon, close old Connect tabs, and let the IAM migration revoke legacy bootstrap grants. New mixed-version attempts are blocked at both service and daemon.

The enforced built-in user/client roles are role:scoped-human, role:observer, role:session-reader, role:terminal, role:files-read, role:files-write, role:peer-user, role:operator, and role:root. role:peer-user admits this principal to the local daemon’s peer-routed terminal/file/display surfaces; the target peer still authorizes each tunnel under this daemon’s grant there. role:peer-profile is visible in the same Access overview so daemon peer grants and human grants can be compared, but it is daemon-to-daemon only and cannot be assigned to a browser certificate or Connect account.

Hosted leases use three reserved compiled role ids: role:hosted-view, role:hosted-tasks, and role:hosted-operate. Their persisted role rows are descriptive only; authorization intersects the active lease, current ceiling, exact compiled operation set, route/method/frame classification, and concrete action/target wall. No preset contains access.manage, credentials.manage, organization-root operations, approval.resolve, or the ability to raise its own ceiling.

Terminal capability is three separate permissions: terminal.view (attach to a visible session, scrollback + live output), terminal.write (type into, resize, or close a visible session), and shell.spawn (create new shells). role:terminal carries view+write only — a collaborator role; spawning is reserved for role:operator and above. Shell sessions belong to the principal that spawned them and are private by default; the owner (or a root session) can mark one shared from the Terminal tab, which is what makes it visible to other principals. The pre-split aggregate id terminal.use is still honored in custom roles and org grant caps as implying all three.

mcp_servers

External MCP servers to connect to as a client (see Integrations and the trust note below). Each is an array-of-tables entry.

KeyTypeDefaultDescription
namestring(required)Server name; tools are exposed as mcp__<name>_<tool>
commandstring(required)Executable to spawn
argsarray[]Arguments
envtable{}Environment for the child process

Trust model: an mcp_servers entry is spawned as a child process with your full privileges (Command::new(command).args(args)). Intendant performs no checksum, signature, or sandbox check on it — adding one is equivalent to adding a line to your ~/.zshrc that runs a binary. The default is mcp_servers = [], and intendant.toml is git-ignored, so the repo ships no MCP servers. Treat copying an intendant.toml between machines like copying shell rc files: read it before sourcing. See MCP Server.

Worked example

A reasonably full intendant.toml:

[model]
context_window = 200000
max_output_tokens = 8192

[orchestrator]
max_parallel_agents = 4

[approval]
file_read = "auto"
file_write = "ask"
file_delete = "ask"
command_exec = "auto"
network = "auto"
destructive = "ask"
display_control = "ask"
tool_call = "ask"

[presence]
enabled = true
provider = "gemini"
model = "gemini-3-flash-preview"
context_window = 1048576
live_provider = "gemini"
live_model = "gemini-2.5-flash-native-audio-preview-12-2025"
live_context_window = 32768

[transcription]
enabled = false
provider = "openai"
model = "whisper-1"
language = "en"
# endpoint = "http://localhost:8080/v1/audio/transcriptions"

[recording]
enabled = false
framerate = 15
segment_duration_secs = 60
quality = "medium"
# max_retention_hours = 24

[computer_use]
provider = "gemini"
model = "gemini-2.5-flash"
backend = "auto"

[agent]
default_backend = "codex"

[agent.codex]
command = "codex"
# Intendant-aware Codex fork; spawned instead of `command` when
# managed_context = "managed" (managed mode only works with the fork).
# managed_command = "/path/to/intendant-aware-codex"
model = "gpt-5.5"
approval_policy = "on-request"
sandbox = "workspace-write"
reasoning_effort = "medium"
web_search = false
network_access = false
writable_roots = []

[agent.claude_code]
command = "claude"
permission_mode = "default"
allowed_tools = []

[agent.kimi]
command = "kimi"
# model = "kimi-code/kimi-for-coding"
# thinking = "high"
permission_mode = "manual"
plan_mode = false
swarm_mode = false
# allowed_tools = ["AskUserQuestion", "Write"]

[agent.pi]
command = "pi"
# model = "openai-codex/gpt-5.6-sol"
# thinking = "high"
# allowed_tools = ["read", "grep", "find", "ls", "bash"]

[live_audio]
enabled = false
default_timeout_secs = 300
gemini_model = "gemini-2.5-flash-native-audio-preview-12-2025"
openai_model = "gpt-4o-realtime-preview"
sample_rate = 24000

[sandbox]
# Omit `enabled` for the platform default (on macOS/Linux, off Windows).
# enabled = true
extra_write_paths = []

[webrtc]
federation_allow_h264 = false

[[webrtc.ice_servers]]
urls = ["stun:stun.l.google.com:19302"]

# [[webrtc.ice_servers]]
# urls = ["turn:turn.example.com:3478"]
# username = "user"
# credential = "pass"

[server]
# bind = "127.0.0.1" # optional; use for local/plaintext automation
advertise = ["wss://192.168.1.42:8765/ws"]

[server.tls]
enabled = false
# cert = "/etc/intendant/server.crt"
# key  = "/etc/intendant/server.key"

[server.auth]
advertised_transport = "none"
# bearer_token = "legacy-advanced-only"

# [[peer]]
# card_url = "https://peer.example.com/.well-known/agent-card.json"
# via_urls = ["wss://peer.tailnet.example:8765/ws"]
# client_cert = "/etc/intendant/peers/peer-client.crt"
# client_key = "/etc/intendant/peers/peer-client.key"
# bearer_token = "legacy-token-if-the-peer-requires-one"
# certificate_witness_vantage = "remote" # only for a known outside-network peer

[[mcp_servers]]
name = "filesystem"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]

[[mcp_servers]]
name = "github"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]

[mcp_servers.env]
GITHUB_TOKEN = "ghp_..."

Autonomy and approval

Approval is decided by a three-layer model (full UI details in Autonomy & Approvals):

  1. Global autonomy--autonomy <low|medium|high|full> (defaults to medium). low asks for everything except file reads; full keeps the human entirely out of the loop (auto-approve) except for HumanInput and LiveAudioSpawn.
  2. Category rules — the [approval] section above (auto/ask/deny) per category. Arbitrary shell execution inherits the strictest rule of every effect it can reach.
  3. Per-action approvaly / s / a / n (approve / skip / approve-all / deny) prompts in any frontend.

The ten action categories are: FileRead, FileWrite, FileDelete, CommandExec, NetworkRequest, Destructive, HumanInput, LiveAudioSpawn, DisplayControl, ToolCall. DisplayControl uses a session-grant model — approve once and subsequent display actions skip the prompt (revocable in-frontend). HumanInput and LiveAudioSpawn always require a human regardless of autonomy level or category rule.

Skills

Skills are named instruction sets stored as SKILL.md files with YAML frontmatter. Discovery checks these locations in order; the first skill with a given name wins:

  1. <project-root>/.agents/skills/<name>/SKILL.md (Agent Skills standard)
  2. <project-root>/skills/<name>/SKILL.md (visible repository path)
  3. ~/.agents/skills/<name>/SKILL.md (personal standard path)
---
name: deploy
description: Deploy the application to production
autonomy: high
disable-auto-invocation: true
---

## Steps

1. Run tests
2. Build release binary
3. Deploy to server

Frontmatter fields: name (required), description (required), autonomy and sandbox (parsed, reserved — not yet enforced), disable-auto-invocation (only the user can trigger it; splits the injected catalog’s auto vs manual sections), compatibility (free-text environment requirements, surfaced verbatim in the injected catalog after the description so the model can rule a skill out before invoking it — descriptions stay purpose-only; other harnesses currently drop this field before the model sees it, so a one-line guard at the top of the body remains the late gate for them). disable-model-invocation and allowed-tools are accepted for forward compatibility but are not yet interpreted. Project skills take precedence over personal skills of the same name.

Global distribution

At startup (daemon, headless, and MCP modes), the daemon installs every skill shipped inside the Intendant binary independently into ~/.agents/skills/ for Codex and Intendant and ~/.claude/skills/ for Claude Code. Intendant never aliases, replaces, or otherwise takes ownership of either global root. If a root is a symlink, junction, file, or other non-directory object, it is left untouched and reported. Inside a normal directory, only per-skill directories carrying .intendant-installed are refreshed or removed; content-identical installs are no-ops, stale marked copies are swept on upgrade, and an unmarked user-authored directory with the same name always wins.

The shipped sources remain tracked under this repository’s skills/ and are embedded in the binary (builtin_skills.rs, pinned to that directory by test). Starting a supervised Claude Code or Codex session performs no skill copying and writes nothing into the project.

Personal global skills are installed manually in the backend’s own path (~/.agents/skills/ for Codex and Intendant or ~/.claude/skills/ for Claude Code). Personal project-scoped skills likewise stay in the backend’s project path (<project>/.agents/skills/ or <project>/.claude/skills/) and must be covered by that project’s ignore rules; this repository ignores both. Intendant does not mirror personal skills between backend-specific paths.

INTENDANT.md project instructions

Place INTENDANT.md in your project root or at ~/.config/intendant/INTENDANT.md for global instructions. Both are loaded if present (global first, project-local second) and injected into the conversation at session start, after the working-directory context and before the skill catalog. Memory is pull-only and is not injected into fresh conversations.

System prompts

System prompts are compiled into the binary, so intendant works from any directory. Two base variants exist:

  • SysPrompt.md — full prompt with JSON schema and per-function docs (used with text-based JSON extraction).
  • SysPrompt_tools.md — condensed prompt for native tool calling (function docs live in the API tool definitions).

The active variant is chosen automatically based on whether the provider has native tool calling enabled. Prompts resolve via a 3-layer cascade (highest priority first):

  1. Project root<git-root>/SysPrompt.md (or SysPrompt_tools.md)
  2. Global config~/.config/intendant/SysPrompt.md
  3. Compiled-in default — always available, zero-config

Role-specific prompts (SysPrompt_orchestrator.md, SysPrompt_research.md, SysPrompt_implementation.md) follow the same cascade and append to the base. The presence layer uses SysPrompt_presence.md; the live-audio voice agent uses SysPromptLiveAudio.md with {PLAYBOOK} / {RESPONSE_SCHEMA} placeholders substituted at runtime.