A small branded web surface that sits beside the OpenClaw gateway: it says whether the assistant is on duty, what he's allowed to do, and lets you talk to him.
your browser ──▶ :18790 orderly-web (this: page, status, chat proxy)
│ bearer token attached here, on loopback
▼
:18789 openclaw (gateway: agents, console, tasks, logs)
Both ports bind loopback only. Nothing here is exposed to the internet.
- A current dashboard —
/dashboardis a peer desk showing live station, broker lane, order queue, and opt-in subscription snapshots. Quota is advisory only: it cannot gate, delay, reroute, or cancel a dispatch. See below. - An orchestration desk — a broker-backed lane board and an operator-reviewed, two-step dispatch flow. See below.
- The approval queue — drafts, calendar proposals, and digest-bound Stripe previews waiting for an operator decision. Approving marks one read and kept; it does not send it, and nothing here can. See below.
- A settings page for models and reply style. Model edits retain the guarded gateway-config path; reply style has its own private station record and changes wording only. See below.
- A connector control view on Agents. It distinguishes catalog entries, installed instances, reviewed attachments, and active runtimes. See below.
- The day — a standing agenda from the mail agent's existing read-only calendar scope.
- Duty state, from a real probe. The page asks
/api/status, which asks the gateway's/healthandsystemctl is-activeon every poll. The mascot is the indicator: on duty he blinks and looks around, off duty he desaturates and half-closes his eye, and the copy tells you what to do about it. Nothing on the page is a hardcoded claim. - Chat, in brand, without the browser holding a credential. See below.
- One-shot named-agent consultation. The operator can select one other active named identity from a named desk and send one bounded request. Only the request and one labeled response cross; both transcripts record them and no durable or agent-initiated delegation is created.
- Named-agent delegation policy control. Agents can review and revoke immutable-id target allowlists under fixed text-only ceilings. This package installs no browser, socket, or agent-callable execution route; its executor remains a separately injected fixture boundary.
- Named-agent routine definition control. Agents can review separate reusable text definitions with manual, daily, or weekly schedule declarations. Definitions start disabled, enabling makes them pending for host checks, and the installed surface has no scheduler, provider/model call, Run button, or execution route.
- The duty list with real scopes. Each capability carries the privilege it actually runs at, including the one that's switched off.
- The right console link for how you arrived. Through the SSH tunnel the console is a second forwarded port, so the link names it. Behind Tailscale Serve both are one origin, so the link is relative. The server decides from the request host.
/dashboard polls one read-only aggregate at /api/dashboard. Station facts come
from the same live gateway/service probes as /api/status; lane timestamps and
classes come from the broker's existing read endpoint; approval counts come from
the existing queue store. A broker outage, gateway outage, and quota failure are
reported independently. Terminal still means “ready for operator inspection.”
Subscription meters are off by default. To opt in, copy
dashboard-subscriptions.example.json to
~/.orderly/dashboard-subscriptions.json (or set ORDERLY_DASHBOARD_CONFIG) and
configure one row per subscription. Every row declares an adapter source of
either "codexbar-loopback" (recommended — see below), "codexbar-cli", or
"ollama-loopback" (the Ollama Cloud subscription only — see below).
Supported providers and their local probe_source values are:
codex (cli, oauth, api), claude (cli, oauth), kilo (cli, api),
and openai, copilot, gemini, openrouter, deepseek, moonshot, zai, or
ollama (api), kimi (cli, api). probe_source defaults to cli when available and otherwise
to the provider's sole local source; web and auto are rejected. On
codexbar-cli, account_index may select an explicitly configured CodexBar
account. Loopback rows do not accept account_index: they always follow the
active login owned by the user running codexbar serve. seat_refs are
explanatory labels only and do not attribute usage to a lane.
ollama-loopback exists because CodexBar knows the ollama provider but its
API strategy reports no windows for it, so that subscription could only ever
render the meterless "no quota windows published" row. Ollama Cloud does publish
the numbers, at GET /api/usage, so ollama-usage.mjs
reads them and serves them on loopback in the same document shape. It runs as
the credential-owning user under ollama-usage.service,
which is where the API key stays: the desk sends no credential over this
transport either. The row accepts no account_index and no provider other than
ollama. The upstream publishes no reset times, so the windows carry
percentages with a null reset rather than a derived one. Point the desk
elsewhere with ORDERLY_OLLAMA_USAGE_ENDPOINT; a non-loopback address disables
only this row.
Seat-engine Settings can also consume normalized provider-wire evidence from the
read-only credential-owner service at http://127.0.0.1:18793 (override only
with the loopback-only ORDERLY_ENGINE_EVIDENCE_ENDPOINT). The operator runs
the qualification command explicitly; opening Settings never spends a provider
call. See docs/model-evidence/README.md.
The desk receives the fixed endpoint/model identity, behavioral verdicts,
timestamps, and a raw-capture digest—not the provider key or raw response.
The credentialed engine-definition route accepts no direct write. Its
propose-provider action first runs the ordinary definition gates and requires
qualification records for the exact provider, endpoint, credential reference,
and every model tag. It returns a five-minute, one-use confirmationDigest
over that reviewed input, the records, and the complete current gateway-config
state. confirm-provider must return the identical definition and digest; a
field change, expired or replayed digest, web-process restart, changed evidence,
or changed config refuses before persistence. No-key definitions keep their
existing direct, desk-probed path.
codexbar serve refreshes lazily: it answers a request with the last completed
snapshot and only then starts a new one, and it does not refresh at all while
idle. Because a rows is CodexBars own updatedAt and the desk
probes at most once every five minutes, the desk was the only thing polling and
each probe returned a snapshot stamped at the previous probe — so rows were
written already half-expired and read stale for much of every cycle.
codexbar-warm.sh, run from
codexbar-warm.service, issues one loopback GET per
configured provider each minute so every desk probe finds a recent snapshot. It
holds no credential and changes nothing in the desk. Remove it if a future
CodexBar refreshes on its own interval while idle.
The host must install the pinned CodexBar Linux CLI v0.49.6 described in
CODEXBAR-NOTICE.md. Both transports verify that exact
version before requesting usage, time out after 30 seconds, never auto-discover
provider rows, never use browser-cookie sources, and never return raw CodexBar
data.
codexbar-loopback keeps provider credentials off this service, and is the
transport to prefer. Reading a subscription's quota requires that subscription's
credential. Under codexbar-cli the desk execs the CLI itself, so the desk
process must be able to read every provider credential — which means running the
web service as the credential-owning user. Under codexbar-loopback the
credential-owning user runs codexbar serve bound to 127.0.0.1, and the desk
reads it over HTTP: the service holds no provider credential, no CodexBar config
file, and no keychain access. GET /usage is unauthenticated on a loopback bind
and returns the same document usage --format json prints, so the desk needs no
bearer token either — there is no secret for it to hold, log, or leak.
The endpoint is ORDERLY_CODEXBAR_ENDPOINT (default http://127.0.0.1:18791)
and is asserted to be loopback when the probe is built: LAN, Tailscale, hostname,
https, and credential-bearing URLs are all refused, redirects are refused so
the desk cannot be sent off-host, no Authorization header is ever sent, and
responses are size-capped. A rejected endpoint disables only that transport;
codexbar-cli rows and the rest of the dashboard continue normally.
Under codexbar-loopback the strategy behind each provider is chosen by the
serve host's own CodexBar config, so probe_source documents intent rather than
selecting it — the desk has no argv to put it on. A host unit for the server is
in codexbar-serve.service.
The normalized cache defaults to
~/.orderly/dashboard-cache.json, is atomically replaced at mode 0600, and
contains only the current row per configured subscription. There is no history.
Reads never wait for a quota probe. A stale read starts one coalesced background refresh; the page's manual refresh uses the same five-minute rate limit. Failed or malformed probes retain last-known-good percentages as visibly stale, never as zero. With no config, no probe process starts and the rest of the dashboard continues normally.
/orchestration is the single Phase-1 coding-lane surface. It reads allowlisted repository
and preset names plus broker-owned lane records over HTTP on the broker's UNIX socket. The
socket defaults to ~/.orderly/broker.sock; set ORDERLY_BROKER_SOCKET to use another path.
If the socket is absent or the broker is down, the page itself still renders, identifies the
broker as offline, disables dispatch, and keeps the rest of the front door usable.
Dispatch is deliberately two-step. The desk first sends the typed brief to
POST /v1/dispatch/propose, then prints the returned digest exactly as JSON. Only a separate
confirmation sends its proposal_id and that displayed digest back as digest_ack. Lane
cancellation names the lane in a browser confirmation before it is requested. This server
does not resolve repositories, build argv, run a shell, or execute a lane; those are broker
responsibilities.
Every mutation — propose, confirm, cancel, and freeze — requires
X-Orderly-Operator. Same-origin and tailnet reachability are only transport boundaries and
grant no mutation authority. The browser keeps the token in one password input and a module
variable for the life of that tab: never cookies or localStorage, never a server-side store,
never a log. The front door checks that it exists before touching the socket and forwards it
to the broker, which performs its own check. Read-only lane GETs do not forward the token.
Terminal cards use the broker's terminal record as truth and are marked “ready for operator inspection.” The bounded worker excerpt is explicitly marked unverified; raw logs are not available through this page.
The gateway's OpenAI-compatible /v1/chat/completions endpoint is enabled on this
deployment, and a valid token for it is equivalent to operator access. So the token never
goes near the browser:
- The page posts plain conversation to
/api/chaton this server, which runs on the gateway host. It attaches the bearer from its own environment and forwards loopback to loopback. No credential is ever sent to a browser, written to a file, or logged. deploy/start-web.shputs it there, lifting exactly one variable —OPENCLAW_GATEWAY_TOKEN— out of~/.openclaw/.envat start time, the same way the gateway's own start script handles its keys. The other secret in that file stays behind. Nothing is copied to disk.- Without a token the page degrades honestly:
/api/chatanswers 503 with a plain explanation,/api/statusreportschat.available: false, and the composer disables itself and says why. - Replies stream (server-sent events, passed straight through). Only
roleandcontentsurvive the border in either direction, under length and turn caps.
Two desks, because they genuinely behave differently. Ask the coordinator and the work is handed to a specialist — the mail agent, or the researcher for anything on the web — so his first answer is an acknowledgement rather than the result. Ask the mail desk and you are talking to that agent directly, so inbox and calendar answers arrive first time. Naming a desk selects an already-sandboxed agent; it grants nothing.
A desk is a continuing conversation upstream, and that is load-bearing. Each desk carries a thread id, which this server turns into a gateway session key. It matters for more than tone: a specialist's result comes back to the session that spawned it, so a desk that forgot its session could be told "I've asked the researcher" and then have nowhere to go and ask. With the session held, asking again collects the answer — which is exactly what the follow-up control below does.
The browser names a thread, never a session. It sends an opaque id and the server builds the key from it, so the desk and the namespace are not the browser's to choose: it can only address its own threads, on the desk it is already talking to, and the gateway's reserved namespaces are unreachable by construction. With a session upstream only the new question crosses the border; without one, the page's history is the only memory there is, so all of it does.
Results stand apart from the talking. The gateway answers in prose, so the proxy asks —
in a system message — for one fenced orderly-card JSON block whenever a reply reports
inbox mail, calendar events or a draft. The page lifts that block out, renders it as a card
in the rail, and shows only the judgement in the conversation. A reply without a block is
just a reply; a malformed one is dropped and the prose still stands. Nothing on the page
depends on the block being there.
Links are the one place a reply becomes clickable, so they get the pedantry. A URL in a
reply is rendered as a real anchor, but the href is parsed by the URL constructor and kept
only if it comes out http or https — a javascript: or ftp: URL stays inert literal
text. For a bare URL the visible label is the URL, so the text can never disagree with the
target; for a markdown link the label is a text node and only the href is trusted. Every
anchor carries rel="noopener noreferrer nofollow". Nothing else about the model's output
becomes markup.
Waiting on a specialist. Ask the coordinator something that needs the researcher or the mail agent and it answers immediately with an acknowledgement — that is the transport, not a fault. The gateway's OpenAI-compatible stream carries content deltas and nothing else: no tool events, no spawn events, so there is no honest way to show "asking the researcher…" while it happens. What the page can read is what the reply says it did. When his last words were "I've asked the researcher, I'll relay it" and no result has arrived, a line appears under the thread offering to go and ask — one click instead of typing the follow-up yourself. It disappears the moment the answer lands.
Threads live in this browser. Each desk's words are kept in localStorage and survive a
reload, alongside the thread id that keeps the session upstream. This makes the page
remember, and keeps a conversation continuous. It is not the same thing as the coordinator
remembering you between conversations, which is his own capability and a much larger claim:
that lives in notes he writes in his own workspace on the station, under the scope the duty
list states. The transcript on this page is a property of the page. Nothing the page holds is
written to the station's disk on your behalf.
Closing a thread archives it. There is no bare "clear": Archive & clear files the conversation under an auto-title (its first line) with its desk and timestamp, retires its thread id, and empties the desk — so a cleared desk is genuinely a fresh conversation on both sides, not just a blank screen. The archive panel lists what is filed, and each row can be read (the thread area goes read-only, and the composer says so), restored (its words and its session come back together, and whatever is live on that desk is archived first, so the two swap rather than one landing on top of the other), or deleted behind a confirm. It is a list of closed conversations, not a document manager.
The ceiling is stated rather than hidden: at most 40 threads, in this browser only, and if
localStorage fills the oldest are dropped and the panel says so. If storage is unavailable
altogether — private browsing, storage disabled — the page says a reload will lose the
conversation instead of quietly pretending otherwise.
Model output is never treated as markup. Cards and prose are built as text nodes; the page's
CSP stays default-src 'self' with no inline script.
The gateway's own Control UI remains the full panel — transcripts, scheduled tasks, tool policy, logs — and the front door links to it rather than reimplementing it.
The mail agent already reads the calendar, read-only. /api/agenda gives that a standing
surface instead of making you ask the chat the same question every morning: it asks the mail
desk for the next seven days, lifts the orderly-card block out server-side so the browser
is handed structure rather than another string to parse, and caches the answer for ten minutes
so reopening the page doesn't quietly spend another model call. It never fires on its own —
the answer costs a call, so you ask for it.
It reaches nothing the chat couldn't already reach. No new capability, no new scope.
/api/reminders opens the file the coordinator keeps his list in — the one directory he is
already allowed to write to — and parses the lines out of it. It reads and nothing else: no
model call, no write path, and no scheduling.
Timed-reminder changes require verified scheduler-owner identity. The front door's gateway bearer does not supply that identity, so this page presents reminder data read-only and points to the existing owner-managed workflow for changes.
A reminder is a top-level list item and only that. The store explains its own format with an indented example, and an indent-tolerant parser read that example back as a real reminder, so the pattern is anchored at column zero.
/api/queue is the existing surface where the operator reviews pending work. Draft decisions are
bookkeeping; calendar and Stripe approvals are explicit typed host actions.
What it shows. Drafts an agent has written, each with where it came from: the routine that caused it, or the desk it was asked at. Where the text is known it is shown; where the card is only a log line it says so, rather than showing an empty box or implying the text is here when it is in Gmail.
Where the cards come from. Two sources, kept deliberately different:
PENDING.mdin the coordinator's workspace — an append-only log it writes when a routine or a Telegram conversation produced a draft. This server never writes to that file. One side appends, the other reads, and there is nothing to lock or corrupt.- Cards this server watched go past. The chat proxy already carries every reply, so a
reply carrying a
draft,event, orstripe-approvalcard files itself. That path is deterministic — the card comes out of the reply's own fenced block, nothing is asked to remember anything — and it is the only one that has the draft's body.
The same draft arriving down both paths is one card, matched on account, recipient and subject inside a half-hour window, with the richer one kept.
What the buttons mean. For a draft, Approve records review only and sending remains in the mail client. For a calendar proposal, Approve performs the typed calendar change after confirmation. For a Stripe preview, Approve sends only the fixed operation input plus the exact preview digest to the derived host-only approval socket; the connector re-reads its persisted preview and current Stripe state before applying. The browser cannot submit a socket path, URL, header, or credential. Discard leaves provider state unchanged.
Where decisions live. Decisions use the front-door store beside server.mjs, outside agent
workspaces. The mode-0600 file is replaced atomically, updates are serialized, and writes are
same-origin. Already-decided cards are refused without another provider write.
/settings changes which models he thinks with and how interactive replies read:
- Models — the station's main model, its fallbacks (ordered) and its utility model, plus a per-agent override for any agent that should differ from the rest.
- Providers — the configured provider's endpoint, its credential's variable name and whether that name is set (and in which file), and its model tags. Tags are editable because catalogues move: a provider retires a tag and the station needs the new one. Renaming a tag re-points everything that referenced it, so the config never holds a dangling reference.
- Reply style — station-wide plain text, ten compiled presets, optional per-agent refinements, and an effective instruction preview. It changes wording only and starts with every preset off.
- Duty — read-only: the sandbox ruling, elevated-tools state, global denies, every agent's tool profile and container network, which endpoints answer, and the channel allowlist as a count rather than a list of ids.
Everything else on that page is a readout, deliberately styled as one. Reply style is stored
in mode-0600 ~/.orderly/reply-style.json, not in OpenClaw's strictly validated config. A
style-only save is atomic on its own and needs no gateway restart.
/agents reads /api/channels separately from the identity and connector APIs. The gateway
returns only host-registered Telegram route labels, opaque ids, lifecycles, fixed media ceilings,
and opaque attachment ownership. The page can request a short-lived ruling that binds one
available route to one active named identity, confirm that exact digest and nonce, or request
suspend, resume, and detach. It cannot register a route, provide Telegram configuration, or
declare a runtime check passed.
When the gateway controller is disabled for isolated web tests, reading /api/channels returns an
empty view and writes nothing; mutation refuses as unavailable. The generic Telegram runtime
source remains fixture-only and exposes no web ingress route. The front door's gateway socket has
management and transcript verbs only, so a web caller cannot manufacture a Telegram message.
/agents lists sanitized delegation policies and provides propose/confirm management for set,
suspend, resume, and remove. The browser can choose only active named identities returned by the
gateway. It cannot choose a model, session, timeout, ceiling, source execution context, connector,
channel, media object, or request text. New and resumed policies stay pending until the host-only
controller runs every fixed negative and positive route probe.
The same-origin API forwards policy management to the gateway-owned controller. It exposes no
delegation execution endpoint, and /api/agents/consult remains the separate operator-triggered
one-shot path. agents/delegation.mjs is covered as an injected text-only fixture with durable
replay/outcome bookkeeping and identical two-transcript attribution; the installed production
service neither instantiates it nor gives a named agent a delegation tool.
/agents reads /api/routines separately from the identity, delegation, channel, and connector
records. It can propose and confirm create, revise, enable, suspend, resume, and remove actions for
one immutable ordinary named-agent owner. Each ruling expires after five minutes, is usable once,
and binds the exact instruction, schedule declaration, fixed contract ceilings, identity state,
and complete routine-definition state. New and revised definitions are disabled; enable and resume
record approved-pending-runtime, and only the direct host controller can run the fixed checks that
qualify one as active. Suspend removes that qualification. Confirmation itself performs no work.
The only schedule shapes are manual, daily with an exact local time and IANA time zone, and
weekly with one weekday, local time, and IANA time zone. They are declarations only. The fixed
limits allow six definitions per identity and 48 per station, 60 title characters, a
2,000-character/8 KiB instruction, no parameters or media, a 3,000-character/12 KiB result,
30 seconds, concurrency one, and hourly starts of two per routine, four per identity, and twelve
per station.
Fixture runs retain at most twelve audit records per definition; the main routine view returns only the latest three. The page shows status and timing plus result character count when present. The stored and returned result is metadata only—character count, byte count, and digest—rather than raw output. The separate history read is bounded by the same retention ceiling.
agents/routines.mjs contains a dependency-injected fixture runner, but production startup does
not instantiate it. The web and gateway services expose reads and reviewed definition lifecycle
only: there is no schedule evaluator, wakeup loop, event listener, provider or model call,
delivery path, Run button, or run/execute/trigger/test endpoint. A definition carries no inherited
connector, channel, Telegram, media, delegation, memory, session, credential, approval, write, or
model-selection authority. In isolated local-store test mode, reads return an empty inactive view
and mutation is unavailable.
/agents reads /api/connectors alongside the identity roster. It shows the compiled catalog,
installed instances, current lifecycle, approved operations, and attachment state. The page can
prepare and confirm an attachment digest for one active agent. The result remains
approved-pending-runtime until the Linux host mounts the derived socket and records the named
runtime checks.
The page has no account-key, endpoint, service-user, environment, scope, or socket-path field.
Instance registration and runtime activation use the host-local command in connectors/; the
browser receives only sanitized labels and operation metadata. Catalog-only kinds are roadmap
vocabulary, not installed integrations.
The attachment confirmation names the exact provider endpoint allowlist and binds it into the digest. A connector kind with no compiled endpoints cannot be activated. Suspension, detachment, and probe-gated resume are available on the desk through one fixed host runtime- control socket. The browser supplies only an attachment id and a fixed verb; the controller removes or restores the derived route, obtains its own named probe evidence, and only then changes the state record. The browser cannot submit a path or a probe result.
Provider keys are configured host-side. The page shows only the variable name OpenClaw wants
and whether it is present. settings.mjs reads environment files without capturing values, and
the supported-provider list comes from the installed OpenClaw docs/providers inventory.
A browser cannot name a path in the config. It sends a small fixed envelope — model choices and model tags — and three independent gates stand between that and the file:
- Typed edits. The envelope is applied by code that only knows how to touch those places. An unrecognised key is refused by name rather than ignored.
- Path allowlist. The candidate document is diffed against the original and every changed path must match the allowlist. If anything else moved, the write is refused and the offending path is named back to you.
- Invariants. The guarded subtrees —
gateway,channels,bindings,tools,meta, every agent'ssandbox/tools/workspace, every provider's credential reference — must come out deep-equal, and POLICY-GUARD's two rulings (sandboxall, elevated off) must still hold.
Only then does it back the config up beside itself (mode preserved, ten kept), write a temp
file, fsync it, and rename it into place — so a full file appears or nothing does.
Writes are same-origin only, one at a time, with a five-second floor between them, and the page asks you to confirm a plain-English list of what will change first.
The gateway watches its own config (gateway.reload, default hybrid) and applies changes
itself — hot-reloading where it can, restarting where it must, with systemd holding the service
up either way. So the page waits on evidence rather than announcing an outcome: it polls
/api/settings/health and reports which actually happened. In practice a model change
hot-reloads and the gateway never drops — Telegram and any running task are untouched, and the
gateway's own log names the same path the allowlist named.
A seat's engine is which endpoint and which model tag it thinks with. It can be a cloud provider, or an OpenAI-compatible server you run yourself — Ollama, llama.cpp, LM Studio, vLLM, on this box, your LAN or your tailnet. ORDERLY never runs a model: an engine is always an endpoint you already have.
Two files, one fact in each, and nothing editable in both:
~/.openclaw/openclaw.jsonkeeps what it always kept — the endpoint, its API mode, its credential reference, and each seat's model choice. A local model is just a provider whosebaseUrlis on loopback. An endpoint that wants no key isauth: "none", and no placeholder key is invented for it.~/.orderly/engines.json(seeengines.example.json) keeps what the gateway has no concept of: which tier a model has been reviewed for, what tool protocol it actually speaks, and each seat's effective context budget.
Tiers are ceilings, not labels. chat-research answers, and admits a single-tool operation
only where the probe has actually watched that engine make exactly one tool call. Ask a
chat-research seat for a multi-step chain and it refuses in plain English — naming what you
asked for, what the seat is, what is missing and what you can do — rather than running one step
and calling it done.
Tier limits are checked on the chat path, operation gate, configuration path, and engine probe.
The gateway's per-agent tool policy remains the source of in-turn capability. chained-task
requires the separately specified bounded-sequencer-v1 harness, which is not part of this
release, and coding-lane remains the dispatch broker's pinned seat rather than a selectable chat
engine.
Engine changes activate only after the probe succeeds. POST /api/engines/probe checks
address and scheme, reachability, provider acceptance, OpenAI-compatible response shape, exact
model tag, a one-line completion, declared limits, and one synthetic no-op tool call where the
engine declares a tool protocol. A failed probe leaves the current seat configuration in place
and reports the stage.
GET /api/engines is the readout: every seat's provider, sanitized endpoint, exact tag, context
window, budget, reply reservation, tool protocol, tier and fallbacks. The credential appears as
the name of an environment variable and whether one is wanted, never a value — the same rule
as the settings page. Every answered /api/chat turn records a bounded attestation. The
configured candidate is always named; X-Orderly-Engine and a fallback verdict appear only
when gateway response metadata uniquely proves the actual model. Otherwise the dashboard and
turn UI say that the actual engine is unverified instead of relabeling the primary as fact.
Named identities can explicitly inherit the gateway default or select an existing reviewed
chat-research seat tuple. Creation and later changes bind the exact model, fallbacks, context
budget and fallback policy into the server-issued identity digest; confirmation re-resolves the
source tuple, so a seat changed after review cannot ride the old ruling. An active identity must
be suspended before a change, and every accepted change returns it to pending for all live
container checks. The compiled profile changes only its model block—sandbox, tools,
capabilities, memory and network policy remain derived from the same guarded identity profile.
A station with no engines.json has no classified seats and behaves exactly as it did before
any of this existed. Tiers bind where you have written one down, and nowhere else.
bash agents/deploy/install.sh
sudo systemctl restart orderly-gateway.service
bash web/deploy/install.shThe first installer stages the named-agent controller under the gateway identity, migrates v0.4 named-agent state into the gateway-owned root when present, and installs a gateway-unit drop-in. That drop-in supervises the controller beside the unit's existing launch command; it does not replace or copy the host's launcher. The installer deliberately restarts nothing. The web installer creates its dedicated system account on a fresh host, replaces the front-door server and static files, leaves its decision stores in place, and restarts only the web unit. The explicit gateway restart is the operator-controlled cutover: it starts the controller and completes migration before the updated web desk connects to the sanitized Unix-socket view.
On v0.4 upgrades, an identity previously shown active is projected pending until its own container is provisioned and the live checks pass. No manifest edit is required.
It also detects two host-specific facts and writes them into the unit rather than assuming
them: where the installed OpenClaw keeps its docs/providers (used to enumerate supported
providers honestly), and which env files exist (used to report credential names, never
values). Both are read-only lookups.
Tailscale Serve — a bookmark. This is how the reference deployment is reached. Serve has to
be turned on for the tailnet once by its owner in the Tailscale admin console; until then
tailscale serve refuses and prints the enable link. After that:
bash web/deploy/tailscale-serve.shgives you https://<node>.<your-tailnet>.ts.net/ for the front door and /openclaw for the
console, tailnet-only, on any device you own. Serve config persists across reboots.
SSH tunnel — the fallback. Before Serve is enabled, or from anywhere off the tailnet,
web/deploy/ORDERLY-UI.command.template forwards both ports and opens the page. Copy it to
your Desktop, put your SSH host alias in it, make it executable. It keeps working either way.
Tailscale Serve is the supported deployment mode. Funnel is outside this setup because it publishes an internet-facing endpoint.
- Zero dependencies. Node's standard library only.
- No webfonts, no CDN, no analytics: the page loads with nothing leaving the box, which is the same promise the rest of the project makes.
- The status endpoint reads only non-secret shape out of
openclaw.json— agent ids, whether each is sandboxed, which channels are enabled, the primary model name. Tokens, account ids and paths never leave the host. It reports whether a gateway credential is present as a boolean, never the value. - Upstream failures are reported by shape, not by body: a gateway error page is never relayed verbatim to a browser.