Surface overlays
A surface is an overlay of routes that is active only while a runtime
condition holds, and that shadows the built-in route for the same path.
Normal routes are registered at a unique (method, path) and built-ins always
win; a surface is evaluated by a middleware before route matching, so it can
replace a path another module already owns — for example the site module's
/ — for as long as its condition is true.
The motivating case is first-run onboarding: while no LLM provider is
configured, Agent OS serves a setup wizard at / instead of the marketing
site. As soon as a provider is configured the overlay disappears, with no
module-graph rebuild and no restart beyond the one the wizard itself triggers.
Configuration
Surfaces are declared in config/surfaces/*.yaml:
id: onboarding
when: no_llm # always | no_llm | llm
priority: 100 # higher wins between matching surfaces
paths: ["/", "/index.html", "/site/"]
page: onboarding.html # HTML file, relative to this YAML …
# … or, instead of `page`:
# redirect: /platform
| Field | Meaning |
|---|---|
id |
Identity; a file with the same id replaces the embedded default. |
when |
Condition: always, no_llm, or llm (an LLM is configured). |
priority |
Tie-break when several surfaces match one path (higher first). |
paths |
Path patterns; :param and *rest wildcards, exact otherwise. |
page |
HTML file served verbatim when the surface matches. |
redirect |
302 target (mutually exclusive with page). |
A built-in onboarding surface ships with its page compiled into the binary, so
a fresh distribution works with no config. Add a config/surfaces/<id>.yaml
(and page: file) to customise it, or to add your own overlays — for example a
maintenance page:
id: maintenance
when: always
priority: 50
paths: ["/", "/:page"]
page: maintenance.html
Conditions
Conditions are a small, named vocabulary evaluated against a runtime state snapshot (not a general expression language), so config stays auditable:
no_llm— active iff no inference provider has a resolvable API key (provider.api_key, a namedmodels.*entry, orAGENT_OS_API_KEY/DEEPSEEK_API_KEYin the environment).llm— the complement.always— unconditional.
Precedence and safety
- The overlay middleware runs before routing: a matching, active surface wins over the built-in route.
- Between matching surfaces, the highest
prioritywins (equal priority: the earlier-registered surface). - Only
GET/HEADare overlaid. - Control paths are never overlaid:
/v1/*,/login*,/logout,/health,/metrics,/proxy*,/ui/*,/widget*,/dist*,/favicon.icoand the site stylesheet/favicon. An overlay can therefore never lock an operator out of configuring the server. - Requests that no surface claims fall through unchanged and are logged by the access log as usual.
First-run onboarding
When the server starts without an LLM, the built-in onboarding surface serves
a wizard at /, /index.html and /site/. The wizard posts to:
GET /v1/onboarding— status:{ llm_configured, base_url, model }(no secrets).POST /v1/onboarding/connect—{ base_url, model, api_key }; persistsconfig/llm.yaml(base URL + model, non-secret) andAGENT_OS_API_KEYin.env(git-ignored), then restarts the server so the settings take effect.
Both endpoints are public by design — the whole point is to let a fresh,
unconfigured server be set up. The connect endpoint refuses with 409 once a
provider exists, so it cannot be used to silently reconfigure a running server
(and it is never shadowed by an overlay).
config/llm.yaml overrides the provider: block of server.yaml at load time;
server.yaml itself is never rewritten, so its comments survive.
Admin UI
The admin shell (menu Platform) exposes two pages owned by the host:
/provider— (re)configure the LLM connection (GET/POST /v1/provider, admin-gated). Savesconfig/llm.yaml+AGENT_OS_API_KEYand restarts. Use this to change the provider after first run; unlike the public onboarding wizard it is always available./surfaces— force any surface on/off or back to auto (GET /v1/surfaces,POST /v1/surfaces/:idwith{ "mode": "on" | "off" | "auto" }, admin-gated). Forced toggles apply live and persist toconfig/surfaces.enabled(oneidper line forces on;-idforces off; absent = follow the condition). The same file is read at boot.
Both APIs are admin-gated (the gate opens when auth is not configured) and the pages are never shadowed by a surface.
Disabling
Delete the config/surfaces/<id>.yaml (or change its when) and restart. The
embedded onboarding surface is active whenever no LLM is configured. To keep the
normal site even in that state, override it with when: llm, which is active
only when an LLM exists (i.e. never during first run):
id: onboarding
when: llm
Or simply configure a provider.