Skip to main content

Control UI (browser)

The Control UI is a small Vite + Lit single-page app served by the runtime gateway:
  • default: http://<host>:18789/
  • optional prefix: set gateway.controlUi.basePath (e.g. /fased)
It speaks directly to the Gateway WebSocket on the same port. For first-run product setup after onboarding, see Control UI Setup Model. The short version:
  • /dash is the compact widget dashboard.
  • /chat is the live browser chat surface.
  • /agents is the normal setup workbench: Setup, Models, Channels, Skills, Tools, Memory, Sessions, Services, Tasks (including Coordination), and Files for the selected Agent.
  • Agent Tasks shows saved work definitions first. Task templates create scheduled tasks; workflow/graph templates create review flows; run history is audit, not the task itself.
  • /wallet, /mining, /federation, and /marketplace own their focused runtime workflows.
  • /extensions owns global plugin/runtime lifecycle; channel account routing still belongs in Agents/Channels.
  • /notifications, /usage, and /logs are operator monitoring pages.
  • /config is Advanced. It now groups raw Config, Debug, and Nodes under one advanced surface.
The older global provider/channel/service/skills/task routes remain routable for compatibility, deep links, or admin views. They are hidden or demoted from the primary navigation because normal setup starts from the selected Agent. For layout, card, modal, tab, helper-icon, and dashboard widget rules, see Control UI Design.

Quick open (local)

If the Gateway is running on the same computer, open: If the page fails to load, start the Gateway first: fased gateway. Auth is supplied during the WebSocket handshake via:
  • connect.params.auth.token
  • connect.params.auth.password
fased dashboard opens and copies an auth-ready local URL using a fragment token (#token=...). The UI stores the token and removes it from the browser URL after load. If you open the page manually, paste the onboarding token into Control UI settings. Passwords are accepted for the session but are not persisted.

Device pairing (first connection)

When you connect to the Control UI from a new browser or device, the Gateway requires a one-time pairing approval — even if you’re on the same Tailnet with gateway.auth.allowTailscale: true. This is a security measure to prevent unauthorized access. What you’ll see: “disconnected (1008): pairing required” To approve the device:
# List pending requests
fased devices list

# Approve by request ID
fased devices approve <requestId>
Once approved, the device is remembered and won’t require re-approval unless you revoke it with fased devices revoke --device <id> --role <role>. See Devices CLI for token rotation and revocation. Notes:
  • Local connections (localhost / 127.0.0.1) are auto-approved.
  • Remote connections (LAN, Tailnet, etc.) require explicit approval.
  • Each browser profile generates a unique device ID, so switching browsers or clearing browser data will require re-pairing.

What it can do

The Control UI operates the self-hosted runtime without requiring a messaging channel first.
  • Dashboard: compact widget board for Agents, Usage, Wallets, Mining, and Fased Network state.
  • Chat: browser chat for the selected Agent/session, with session switching, model overrides, stats, task/session panels, and streamed tool cards.
  • Agents: setup workbench for Models, Channels, Skills, Tools, Memory, Sessions, Services, Tasks, Coordination, and Files.
  • Wallet, Mining, Fased Network, Marketplace: focused runtime pages for the actions and records owned by those domains.
  • Usage, Notifications, Logs: operator monitoring and recent activity.
  • Advanced: Config, Debug, Nodes, exec approvals, raw RPC tools, and update actions.

Tasks and history boundary

Agent > Tasks is the saved-work control surface; domain pages keep authority. Agent > Tasks manages definitions and can open run history. Wallets, Mining, Marketplace, Channels, and Services still own actions that can sign, mine, deliver, send externally, or change external state.
  • Use Task for scheduled work.
  • Use Trigger for external HTTP entrypoints.
  • Use Workflow for a multi-step review or procedure.
  • Use Graph for the visual editor over the same workflow JSON.
  • Use Templates only as starters for saved definitions.
Saved definitions and run history are intentionally separate. Definitions describe what can run. History rows record what already happened, group related records by correlationId, and link back to source pages when a domain owns the action. Delivery defaults to an announce summary for isolated tasks. Use none for internal-only runs, or webhook delivery when delivery.mode = "webhook" and delivery.to is a valid HTTP(S) webhook URL.

Chat behavior

  • chat.send is non-blocking: it acks immediately with { runId, status: "started" } and the response streams via chat events.
  • The Control UI subscribes to sessions.changed and the selected session’s session.message stream, so the Sessions panel and active chat can update without waiting for manual refresh or polling.
  • Re-sending with the same idempotencyKey returns { status: "in_flight" } while running, and { status: "ok" } after completion.
  • chat.history responses are size-bounded for UI safety. When transcript entries are too large, Gateway may truncate long text fields, omit heavy metadata blocks, and replace oversized messages with a placeholder ([chat.history omitted: message too large]).
  • chat.inject appends an assistant note to the session transcript and broadcasts a chat event for UI-only updates (no agent run, no channel delivery).
  • Stop:
    • Click Stop (calls chat.abort)
    • Type /stop (or standalone abort phrases like stop, stop action, stop run, stop fased, please stop) to abort out-of-band
    • chat.abort supports { sessionKey } (no runId) to abort all active runs for that session
  • Abort partial retention:
    • When a run is aborted, partial assistant text can still be shown in the UI
    • Gateway persists aborted partial assistant text into transcript history when buffered output exists
    • Persisted entries include abort metadata so transcript consumers can tell abort partials from normal completion output

Integrated Tailscale Serve (preferred)

Keep the Gateway on loopback and let Tailscale Serve proxy it with HTTPS:
fased gateway --tailscale serve
Open:
  • https://<magicdns>/ (or your configured gateway.controlUi.basePath)
By default, Control UI/WebSocket Serve requests can authenticate via Tailscale identity headers (tailscale-user-login) when gateway.auth.allowTailscale is true. Fased verifies the identity by resolving the x-forwarded-for address with tailscale whois and matching it to the header, and only accepts these when the request hits loopback with Tailscale’s x-forwarded-* headers. Set gateway.auth.allowTailscale: false (or force gateway.auth.mode: "password") if you want to require a token/password even for Serve traffic. Tokenless Serve auth assumes the gateway host is trusted. If untrusted local code may run on that host, require token/password auth.

Bind to tailnet + token

fased gateway --bind tailnet --token "$(openssl rand -hex 32)"
Then open:
  • http://<tailscale-ip>:18789/ (or your configured gateway.controlUi.basePath)
Paste the token into the UI settings (sent as connect.params.auth.token).

Insecure HTTP

If you open the dashboard over plain HTTP (http://<lan-ip> or http://<tailscale-ip>), the browser runs in a non-secure context and blocks WebCrypto. By default, Fased blocks Control UI connections without device identity. Recommended fix: use HTTPS (Tailscale Serve) or open the UI locally:
  • https://<magicdns>/ (Serve)
  • http://localhost:18789/ (on the gateway host; preferred for browser auth features)
Insecure-auth toggle behavior:
{
  gateway: {
    controlUi: { allowInsecureAuth: true },
    bind: "tailnet",
    auth: { mode: "token", token: "replace-me" },
  },
}
allowInsecureAuth does not bypass Control UI device identity or pairing checks. Break-glass only:
{
  gateway: {
    controlUi: { dangerouslyDisableDeviceAuth: true },
    bind: "tailnet",
    auth: { mode: "token", token: "replace-me" },
  },
}
dangerouslyDisableDeviceAuth disables Control UI device identity checks and is a severe security downgrade. Revert quickly after emergency use. See Tailscale for HTTPS setup guidance.

Building the UI

The Gateway serves static files from dist/control-ui. Build them with:
pnpm ui:build # auto-installs UI deps on first run
Optional absolute base (when you want fixed asset URLs):
FASED_CONTROL_UI_BASE_PATH=/fased/ pnpm ui:build
For local development (separate dev server):
pnpm ui:dev # auto-installs UI deps on first run
Then point the UI at your Gateway WS URL (e.g. ws://127.0.0.1:18789).

Debugging/testing: dev server + remote Gateway

The Control UI is static files; the WebSocket target is configurable and can be different from the HTTP origin. This is handy when you want the Vite dev server locally but the Gateway runs elsewhere.
  1. Start the UI dev server: pnpm ui:dev
  2. Open a URL like:
http://localhost:5173/?gatewayUrl=ws://<gateway-host>:18789
Optional one-time auth (if needed):
http://localhost:5173/?gatewayUrl=wss://<gateway-host>:18789&token=<gateway-token>
Notes:
  • gatewayUrl is stored in localStorage after load and removed from the URL.
  • token is stored in localStorage; password is kept in memory only.
  • When gatewayUrl is set, the UI does not fall back to config or environment credentials. Provide token (or password) explicitly. Missing explicit credentials is an error.
  • Use wss:// when the Gateway is behind TLS (Tailscale Serve, HTTPS proxy, etc.).
  • gatewayUrl is only accepted in a top-level window (not embedded) to prevent clickjacking.
  • Non-loopback Control UI deployments must set gateway.controlUi.allowedOrigins explicitly (full origins). This includes remote dev setups.
  • gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true enables Host-header origin fallback mode, but it is a dangerous security mode.
Example:
{
  gateway: {
    controlUi: {
      allowedOrigins: ["http://localhost:5173"],
    },
  },
}
Remote access setup details: Remote access.