ADR 0010 — Headless setup & UI deployment tiers (lighter stack)
- Status: Accepted (2026-06-01) — design/decisions; implementation to follow ("we'll go")
- Date: 2026-06-01
- Deciders: Josh Mabry; protoAgent maintainers
- Tags: deployment, setup, ui, dependencies, headless, docker, operator
- Supersedes / Superseded by: generalizes the existing
--headlessflag
Accepted. Two coupled needs: (1) complete setup without the wizard UI — drop a config, supply secrets, the graph compiles (Roxy hit the
.setup-completegate with no UI to satisfy it); and (2) a lighter stack that doesn't deploy the UI at all — API + A2A only, without the React console or the heavy Gradio dependency. Decision: a single--ui {full,console,none}deployment tier (envPROTOAGENT_UI), a headless setup path (a--setupone-shot + boot-time auto-complete from a valid config), andgradiobecomes an optional dependency sonone/consolestacks are lean.
1. Context & Problem Statement
Current behavior:
- Setup is wizard-gated.
_init_langgraph_agent(server.py:138) refuses to compile the graph untilis_setup_complete()sees the.setup-completemarker, and only the wizard (/api/config/setup→mark_setup_complete()) writes it. A headless deploy that drops a validlanggraph-config.yamlstill boots with_graph = None("Open the UI to finish setup") — there's no UI to open. (Roxy hit exactly this.) --headlessis half a tier. It skips Gradio (the heavy, PyInstaller-hostile dep) but still serves the React console at/app+/static(server.py:2608-2633). There's no "no UI at all" mode.- Gradio is a hard dependency (
requirements.txt: gradio>=5.0) — installed even for an API/A2A-only server that never imports it.
So a headless agent (Roxy, any server summoned via A2A) can't (a) finish setup without a UI, or (b) run a lean stack without the console + gradio.
2. Decision
2.1 One deployment tier: --ui {full,console,none} (env PROTOAGENT_UI)
| Tier | Gradio (/) | React console (/app, /static) | API + A2A + /metrics | For |
|---|---|---|---|---|
full | ✅ | ✅ | ✅ | local dev / python server.py (default) |
console | ✕ | ✅ | ✅ | desktop sidecar (React is the UI) — today's --headless |
none | ✕ | ✕ | ✅ | headless servers / Roxy — the lighter stack |
noneskips the Gradio import/mount and the React-console +/staticmounts — pure API + A2A +/metrics.--headless/PROTOAGENT_HEADLESSis kept as a deprecated alias for--ui console(back-compatible; logs a deprecation note).- Default stays
fullforpython server.pyso local dev is unchanged.
2.2 Headless setup (no wizard)
Two ways to satisfy the .setup-complete gate without a UI:
python server.py --setup— one-shot:ensure_live_config(), validate the live config, thenmark_setup_complete()and exit. Idempotent; the explicit operator/CI path.- Boot-time auto-complete — when setup isn't complete but the config validates and the tier is
none(orPROTOAGENT_HEADLESS_SETUP=1), mark complete and compile. So a container just mounts a config + supplies the key + runs.
Validation (shared helper validate_for_headless(config) -> (ok, reason)): config parses, model.api_base is set, and the model api_key resolves (config/secrets.yaml or OPENAI_API_KEY env). Fail fast with the concrete reason if not — never silently mark a broken config complete, never boot a dead graph in a headless tier.
Why gate auto-complete on
none/an env, not always: infull/consolethe wizard is reachable and is the intended first-run funnel; auto-completing there would skip credential collection. Headless tiers have no wizard, so auto-complete (or--setup) is the only path.
2.3 gradio becomes optional (the lighter stack)
- Split deps:
requirements.txt= core (API / A2A / graph / stores — no gradio);requirements-ui.txt= gradio (thefulltier only). fullimports gradio lazily (already does); if it's missing, fail with a clear message: "gradio not installed —pip install -r requirements-ui.txtor run--ui console|none."- Docker default = the lean stack: core deps,
PROTOAGENT_UI=none— the image is almost always a headless server. A--build-arg UI=full(or a separate stage) adds gradio + the console for an all-in-one image.
3. Mechanism summary (for the build)
_main: replace the boolheadlesswith auitier resolved from--ui/PROTOAGENT_UI/ (deprecated)--headless. Gate the Gradio block onui == "full", and the React-console +/staticmounts onui != "none".--setupsubcommand/flag → validate +mark_setup_complete()+ exit._init_langgraph_agent: ifnot is_setup_complete()→ if headless-setup conditions hold and the config validates,mark_setup_complete()+ continue; else keep the current_graph=None+ log (full/console show the wizard).validate_for_headlessingraph/config_io.py(next to the marker helpers).requirements.txtslimmed;requirements-ui.txtadded; Dockerfile build-arg.
4. Security / safety
- Fail-fast, never silent. A headless tier with an invalid/cred-less config exits with the concrete reason — it does not mark setup complete or serve a dead graph.
- Secrets stay out of the config (ADR 0008 / secrets-overlay): the key resolves from
secrets.yamlor env;--setupnever writes secrets to the tracked YAML. - Smaller attack surface in
none: no console, no Gradio, fewer deps.
5. Consequences
Positive — headless agents (Roxy, A2A-summoned servers) go config → running with no UI; the default image is lean (no gradio, no console); one tier knob replaces the half-measure --headless.
Negative / costs — full now needs requirements-ui.txt too (documented; local-dev quickstart updated). The dependency split + Docker build-arg touch the deploy docs. --headless becomes a deprecated alias (kept working).
6. Alternatives considered
- Always auto-complete setup when a config exists — rejected: would skip the wizard's credential funnel in
full/console. Gated to headless tiers. - Keep gradio required, just don't import it — leaves the heavy dep in every image; rejected for the lighter-stack goal.
- A separate
protoagent-corepackage — cleaner long-term, heavier now; the requirements split + build-arg gets 90% of the win without repackaging.
7. Open questions
- Should the Docker default be
none(lean, surprises full-image users) orfull(heavy, safe)? Leaningnone— images are servers;--build-arg UI=fullfor the all-in-one. --setupas a flag onserver.pyvs ascripts/setup.pyCLI? Leaning a flag (one entrypoint).- Do we also want a
GET /healthz/readiness signal that reflects "graph compiled" for thenonetier (no UI to eyeball)? Likely yes — small follow-up.
8. Related
- ADR 0007 — Operator Primitives — Roxy, the prime headless tenant.
- ADR 0008 — Sandboxing — secrets-overlay + the OpenShell deploy this complements.
- Code:
server.py(_main,_init_langgraph_agent, the mount blocks),graph/config_io.py(is_setup_complete/mark_setup_complete),requirements.txt,Dockerfile.