WebUI
The WebUI is nanoinfra's browser workbench for persistent topics, visible agent activity, workspace controls, Apps, Skills, settings, and Automations in one place.
The published nanoinfra wheel already includes the WebUI bundle. You only need
the webui/ source directory when you are changing the frontend itself.
Open the WebUI
Use the launcher:
nanoinfra webui
nanoinfra webui creates the config/workspace when needed, enables the local
WebSocket channel after confirmation, and generates a WebUI bootstrap secret
when one is missing. It then starts the gateway and opens the browser. With a
fresh config, it can open before a model is configured so you can finish setup
in Settings → Models. The first-run path binds the WebUI to 127.0.0.1 by
default, so it is not available from other devices on your LAN.
Run it in the background when you do not want to keep a terminal open:
nanoinfra webui --background
Complete first-time model setup in a foreground nanoinfra webui session before using
--background.
Manage the background gateway with nanoinfra gateway status, nanoinfra gateway logs, nanoinfra gateway restart, and nanoinfra gateway stop.
Manual config still works. Same-machine localhost WebUI access can run without
a browser password. Set tokenIssueSecret when you want a browser password on
localhost:
{
"channels": {
"websocket": {
"enabled": true,
"host": "127.0.0.1",
"tokenIssueSecret": "your-webui-password",
"websocketRequiresToken": true
}
}
}
To reach the WebUI from another device, see LAN Access.
The WebUI is served by the WebSocket channel on port 8765 by default. The
gateway health endpoint, 18790 by default, is not the browser UI.
First 10 Minutes
Use the WebUI as the primary setup surface:
- Open Settings → Models and configure a provider, credential, and active model preset.
- Send
Hello!in a new topic to prove the selected model works. - Start a separate topic before project work, then choose the intended workspace and access mode.
- Add only one capability next: a chat channel in Settings → Channels, a web/voice/image provider in Settings, or an App/MCP integration in Apps.
- Restart when the WebUI shows a restart requirement, then test that capability with the smallest possible request.
This path avoids hand-editing config.json for normal setup. Use the reference docs when you need an option the WebUI does not expose or when you manage config as code.
What It Is For
| Area | Use it for |
|---|---|
| Topics | Start, switch, search, fork, and delete browser topics |
| Agent activity | See thinking, tool calls, file edits with diffs, command output, and generated artifacts in context |
| Workspace | Pick the project workspace before asking for file or shell work |
| Access | Choose the access mode for local capabilities allowed by your gateway configuration |
| Composer | Send text, images, voice input, slash commands, and @ mentions for topics, Apps, or MCP presets |
| Channels | Connect and validate chat platforms, install their optional support, and manage saved channel setup |
| Apps | Install, test, update, and use local CLI App adapters and MCP presets |
| Skills | Inspect available built-in and workspace skills before relying on them |
| Automations | Review, search, run, pause, edit, and delete scheduled and local-trigger agent turns. Read each one's recent runs and remembered state, set when it should notify you and which skills it loads, and issue or revoke the key that lets an external system fire it. See Automations |
| Approvals | Read one suspended remote action and approve or deny it. See Approvals Inbox |
| Settings | Adjust models, providers, image generation, voice, web tools, runtime, and safety options |
Topic Workspace
The sidebar is the topic switcher. Each topic keeps its own history, title, workspace selection, and linked automations. Use a new topic when you want a separate context. Use fork when you want to continue from an existing point without changing the original thread.
The message timeline shows both user-visible replies and agent activity. Long tool or reasoning sections can be expanded when you need the details.
When the agent writes or edits files, the activity item shows the target path, status, changed line counts, and, when available, a unified diff. Use View diff to expand the change. Large diffs may hide unchanged lines or truncate the inline preview. Use Open file from a file edit to open the read-only file preview panel.
File previews follow the active session access mode. Restricted workspace access previews only files under the selected workspace. Full Access can preview files outside the workspace when that access mode is allowed by the gateway.
Workspace and Access
This section is about the project a chat answers about, chosen in the composer. For browsing and editing files, see Workspaces below — the two are deliberately independent.
Use the workspace picker before starting project-specific work. This gives the agent the right project context for file paths, shell commands, and session metadata.
Selecting a project does not replace the configured agent workspace. The two paths have different responsibilities:
| Selected project provides | Agent workspace continues to provide |
|---|---|
Project AGENTS.md | SOUL.md and USER.md |
| Relative file paths and shell working directory | Long-term memory and history |
| The normal read/write boundary in Restricted mode | Custom skills and instance state |
Project-local SOUL.md and USER.md files are ignored, and the agent workspace's
AGENTS.md is not inherited by a separately selected project. When the selected
project is the configured agent workspace, both roles naturally use the same
directory.
The access control in the composer controls the local capability level for the chat. It does not bypass your gateway, provider, shell sandbox, or operating system configuration. It only selects among the capabilities that are already available to the current topic.
In Restricted mode, ordinary file and shell work stays inside the selected
project. To preserve agent continuity, filesystem/search tools receive narrow,
read-only access to built-in skills, custom skills in the agent workspace, and
the exact agent memory/history.jsonl file. This does not grant access to
neighboring memory or profile files, and it does not allow writes outside the
selected project. These tool exceptions do not broaden the browser's file
preview boundary.
Remote WebUI connections may reduce access for the current workspace. Selecting a different workspace or enabling Full Access remains limited to local and native clients.
One Workspace Per Person
This applies to a deployment with an identity in front of it — a proxy that asserts
who you are. The trustedProxyAuth block verifies the assertion. See
WebSocket channel for the fields, and examples/auth/ in the
repository for a stack you can run. A deployment reached through a shared token has
one person as far as the gateway is concerned. Everything below reads as it did
before this existed.
A verified identity gets its own workspaces root. Not a workspace: a root, with
default inside it, exactly like the shared posture one level down. Your switcher
lists what is under your root, and a workspace you create lands there. The
directory is named from the issuer and the claim workspaceKeyClaim names — sub by
default. It is never named from your email address, because an address is mutable.
A rename would orphan a directory, and a reassigned address would inherit one.
Asking for somebody else's answers 403 that workspace is not yours. So does
asking for the shared default, because your root is your own directory. That one
is outside it. The refusal is a refusal and not a correction: a client that named
another path is told, rather than quietly served something else.
Your first workspace is seeded like any other — AGENTS.md, HEARTBEAT.md,
SOUL.md, USER.md, memory/ and prompts/. It happens only once, so a second
sign-in never overwrites what you edited. The stores (secrets/, servers/,
diagrams/, triggers/) are not created and never copied. The credential store and
the server inventory belong to the deployment, in the workspace the executor was
confined to at startup. They do not move with you.
Sessions belong to whoever started them. Your sidebar lists yours. Asking for
another person's session by key answers 404 rather than 403, because a 403 would
tell you it exists. Attaching to one over the socket is refused for the same reason.
A session written before identities existed, or by a shared-token deployment, belongs
to nobody and is not shown to a signed-in person.
What this does not change is authority. A remote action from any workspace goes
through the same executor, the same gate and the same approver set. Two people with
their own directories still cannot approve each other's actions unless
gates.approvers names them. Reaching the agent is not the same as being able to act
on a host through it.
Settings → Identity shows which of these you are in. It names the posture, the
claim that keys your storage, the workspace you actually write in, and whether it is
yours or shared. The sidebar footer names you and offers Sign out when the
deployment configured signOutPath. The session is the proxy's cookie, so the
gateway can only send your browser to a route that proxy serves.
Workspaces
This section is about files. The project a chat answers about is chosen separately, in the composer — see Workspace and Access above.
The Workspaces item in the sidebar is a file manager over the workspace, at
#/workspace. It is not tied to the chat: looking at files does not change which
project a conversation answers about.
The tree. Clicking a folder expands it in place, under its parent, and
collapsing it keeps what was already loaded. Double click, or "Open as root" in the
right-click menu, re-roots the tree at that folder and puts it in the URL. A reload
then lands back there. Dot entries — .git above all — are left out by default
and counted in the header. The eye toggle asks the server for them, which is why a
repository's object database never has to cross the wire to be hidden.
Actions. Right-click a row for rename, delete, download, new folder and upload.
The same actions appear on hover. Deleting a folder that is not empty asks a second
time and says what that means. recursive is never sent before the server has said
a tree is involved. Dragging a row onto a folder moves it, and a folder cannot
be dropped into its own subtree.
Upload. Drop files or a whole folder onto the tree, or use Upload / Upload folder. Either way a review dialog lists what is about to be written. It shows the tree as it will be created, with each folder's file count and size. Anything can be left out and put back. Dot entries start left out. Files up to 100 MB are sent in frame-sized parts and assembled by the gateway. The intermediate directories of a dropped folder are created from each file's own relative path.
The file picker path raises the browser's own "upload N files to this site?" prompt, which is the browser's and cannot be suppressed. Drag and drop does not.
Preview. Clicking a file opens it beside the tree. The preview uses the same renderers as the chat's preview (markdown, Mermaid, SVG), plus a wrap toggle for long lines. Drag its left edge to resize it, or collapse the tree entirely to give it the surface. The breadcrumb navigates: each segment inside the workspace roots the tree there. Binary files are not previewed — download them instead.
Choosing a workspace. The picker in the header lists the children of
tools.workspacesRoot, plus the configured agents.defaults.workspace. That path
is allowed wherever it sits, because config names it. "New workspace" creates one
under the root, seeded like the CLI does: AGENTS.md, SOUL.md, USER.md,
HEARTBEAT.md, memory/, prompts/, skills/ and a git repository. Any other
path is refused with that reason — see tools.workspacesRoot.
Everything here is confined to the selected workspace, and that confinement does
not read tools.restrictToWorkspace. That setting governs the agent's own file
tools, while this decides what an authenticated browser may reach. Turning the
former off does not widen the latter. A symlink pointing outside the workspace is
listed and marked, and refuses to open.
Composer
The composer supports plain messages, image attachments, voice input when
transcription is configured, slash commands, and @ mentions for installed Apps
or MCP presets. Select another topic from the @ menu to attach a stable
reference. Plain text that happens to start with @ does not attach history.
Restricted chats offer topics from the same project, while Full Access chats can
reference any WebUI topic. nanoinfra reads a referenced topic only when its
history is relevant and can link it in the response. The model badge shows the
current model or preset and links back to model settings when setup is
incomplete.
Referencing a server or a diagram
Type @ and the menu offers server: and diagram: alongside the Apps and
topics. Choose one — or type the prefix yourself. The list narrows as you type,
matching on the name and on the summary. So @server:prod finds a host tagged
prod, and not only one named that.
The point is to skip a search. Without a reference, a task that names a thing
begins with the agent calling list_servers and matching on a name. That is
cheap in a chat where you can see it pick and correct it. A mention resolves
once, when you write it.
What travels is the id, not the name you see. So a server renamed later still
resolves, and the message you wrote stays readable. The text carries the name, and
the reference carries the id. A name that cannot be one word, like
Example: blog on Azure, falls back to the id in the text.
What the agent receives is a reference and a summary, never the record. It gets the
id, the name, and enough to decide whether fetching detail is worth a call. That is
a server's provider and tags, or a diagram's node count. It reads the rest through
get_server or get_diagram, which is what puts a server's stored credential
behind a capability gate instead of in a prompt.
Mentioning a diagram references it. To put a diagram's contents in the
conversation, use /infradiagrams <name-or-id>, which is the verb for that.
A token naming something that does not exist stays plain text and sends nothing, so a typo cannot become a reference.
Automations take the same references, and there it matters more, because the match would otherwise be re-done on every unattended run. See Automations.
For image generation, configure an image provider first and then use the WebUI
image mode from the composer. See image-generation.md
for provider setup and output behavior.
Channels
Open Settings → Channels to connect chat apps without assembling JSON by hand. Search for a platform, open its setup panel, and follow the fields or QR flow shown for that channel. The guided setup can:
- install missing optional channel support when the WebUI is running locally.
- collect platform credentials while preserving previously saved values.
- handle supported QR-based login flows.
- validate the connection and show actionable setup errors.
- tell you when the gateway needs to restart.
The platform itself may still require you to create a bot, enable event permissions, copy a token, or configure a webhook. Use channels.md for those platform-side prerequisites and for manual JSON/reference options.
Test a new channel with a private DM. When a supported channel sends a pairing code, the WebUI surfaces the pending request so you can approve the sender. Keep access narrow. Do not use a wildcard allowlist unless public access is intentional.
Apps
Open Apps from the sidebar to manage tools that nanoinfra can attach to a chat turn. The default Ready view shows only tools that can be used immediately:
- Apps are local command-line adapters that nanoinfra runs on your machine. Installing an adapter does not modify the native desktop or web app it connects to.
- Integrations are MCP servers. Presets provide known configurations, and the custom integration panel accepts stdio, HTTP, and SSE servers.
- Connectors are data sources with a capability class per operation, so a
read and a write take different decisions. The row says who the connector acts
as, what the gate answers for each class, and which scopes an operator granted.
Test performs one declared read to prove the credential works. There is no
enable toggle: activation is
connectors.activein config, applied when the agent starts. Seedata-connectors.md.
Apps intentionally does not list nanoinfra runtime support packages such as
api or bedrock. Those packages enable providers, servers, or channels. They
are not tools that can be attached to a turn with @. Manage them from
System, Models, or Web. PDF and common Office document readers are
included in nanoinfra and activate automatically when a file is attached. The
equivalent CLI for optional integrations remains nanoinfra plugins. See
cli-reference.md.
Some MCP presets connect to hosted keyless endpoints. For example, the Firecrawl
preset uses Firecrawl's hosted MCP endpoint for search, scrape, crawl, and
extraction tools without requiring an API key. This does not replace nanoinfra's
built-in web search provider. Mention the Firecrawl MCP preset with @ when a
turn needs Firecrawl's richer web data tools.
The Parallel Search preset connects to the free, anonymous Parallel Search MCP
endpoint and exposes web_search and web_fetch without requiring an API key.
It is an optional integration and does not replace nanoinfra's built-in web search
provider. Mention @parallel-search when a turn should use it.
After an App or integration is available, mention it from the composer with
@ to attach that tool to the next message.
Skills
The Skills view shows the skill instructions available to the agent, including built-in skills and workspace-provided skills. Check this view when you want to know whether nanoinfra already has a focused workflow for a task. Do that before you ask it to perform the task.
Agents
A deployment can name several agents in agents.named, each with its own model,
its own tools and its own instructions. When it does, Agents appears in the
sidebar as a top-level destination and lists them. Each entry holds the name, the
line that explains what the agent is for, and which model answers as it. It also
holds how many tool groups, skills and delegates the agent carries.
The roster shows how many and never which. An agent's tool groups, skills and delegation roster are what it is allowed to reach. They are decided in the config file, and the browser is not where that list should be readable. Editing an agent means editing config. This page reads it.
Open an agent to see its Prompt tab: the sections its system prompt is assembled from, and what each one is yours to change.
| Section | Permission |
|---|---|
| Runtime / identity | Yours — this is the persona |
| Safety notes | Fixed |
| Bootstrap files | Yours per workspace, through AGENTS.md and friends |
| Tool usage notes | Fixed |
| Memory | Yours |
| Skills, connectors and tool groups advertised | From config |
| Agent addendum | Appended, always after the rest |
The two fixed rows are fixed for one reason. Deleting them leaves the capability gate refusing actions the model no longer knows the rules for. That is the worst of both: the action still fails, and the failure is no longer explicable. The addendum is the way to add instructions, and it can replace no section — it arrives after all of them.
A section a deployment is allowed to replace is marked Replaced rather than quietly swapped, both here and in the per-turn prompt breakdown. A record that hid a replacement would make two different prompts look identical.
The subagent-concurrency setting, previously Settings → Agents, is a row in this area. That section was never about agents, only about how many subagents one agent may run at once.
Also in the sidebar once agents are named: Abilities, which groups Apps and Skills. It is a menu grouping and nothing else — the same pages, the same links, one indent further in. A deployment that names no agents sees neither addition: the navigation stays exactly as described above.
Automations
Automations are agent turns that run later in a linked topic. Create them from the topic or channel where they are supposed to run so nanoinfra keeps the correct target context. When an automation runs, it normally delivers the result back to that topic.
For the full automation model, creation flow, trigger CLI usage, and delivery
semantics, see automations.md.
There are two user-facing automation types:
- Scheduled automations, created by the agent's cron tool, run at a time, interval, or cron expression.
- Local triggers, created with
/trigger <name>, run when you call a local command such asnanoinfra trigger trg_8K4P2Q9X "Review PR #4502".
Some recurring background checks should stay quiet unless there is something
useful to report. For those, use the protected heartbeat job by editing
HEARTBEAT.md instead of creating a chat automation.
Use the Automations view to:
- Filter by all, active, paused, needs-attention, or system jobs.
- Search by task name, message, trigger command, linked topic, schedule, or status.
- Sort by next run, last run, updated time, or name.
- Run scheduled automations now.
- Pause or resume, rename, or delete user-created automations.
- Copy the CLI command for local triggers.
- Inspect protected system automations without changing them.
- Read what a commissioning run found, rehearse the automation again, and grant the standing permission it needs.
Search accepts plain text and field filters such as name:backup,
chat:Signal, schedule:09:30, cron:"0 23 * * *", trigger, and
status:paused.
The Commissioning card on a scheduled automation carries the verdict of its
last rehearsal. It says whether a scheduled run would be permitted, and which
standing grant it would need. It also names the credential cell or latch that
would stop it anyway. Two actions sit there. Rehearse runs the automation
once with every gated action previewed, so it writes nothing and reaches no
host. Grant it writes the proposed grant to gates.standingGrants and asks
for a restart, because the gateway reads the policy at startup.
That card is not the approval card. An approval releases one suspended action.
Granting writes a permission that covers that command on those hosts in any
unattended turn, not this automation alone. A local trigger has no card,
because its message arrives from whoever fires it and cannot be rehearsed in
advance. See
automations.md#creating-one-rehearses-it.
An automation without a linked topic cannot be enabled or run from the WebUI, because nanoinfra would not know where to deliver the scheduled turn. Recreate it from the target topic or channel so the automation has complete context.
Local triggers do not have a WebUI "Run now" action because each run needs a
message. Use the copied nanoinfra trigger ... command and replace "message"
with the content that should be delivered.
What a Turn Cost
Every finished assistant message shows its own cost in the footer, beside the
timestamp: 3.2K in · 412 out · 87% cached · 4.1s. A turn is usually several
provider calls, and the figure is the whole turn.
A ~ prefix means part of the total was estimated rather than reported by the
provider. The cache share appears only when the provider reported one. See
Metrics for what those distinctions mean and
where the numbers are kept.
Metrics
Metrics in the sidebar is a destination rather than a page of Settings, because
none of the numbers on it is a setting. Four tabs:
| Tab | Answers |
|---|---|
| Usage | What a window cost, per day and per model, with a cost column once pricing names a rate |
| Live | What is happening now, as nine point-in-time gauges. A gauge that could not be read shows —, which is not zero |
| Calls | One row per tool call, filterable by tool, source, actor, outcome and gate decision. Never the arguments — the row is the call's address and the transcript holds what was said |
| Approvals | Whether the gate is working: how many actions it held, how many a person answered or refused, how many expired |
The Usage tab opens with a scale row — servers, skills, agents, MCP servers, connectors. That row is the one thing on that tab a date range does not scope.
Metrics is the full reference. It covers every figure, and which of them a provider reported rather than our tokenizer estimated. It also covers the Prometheus scrape endpoint, and what the store deliberately does not record.
Settings
Settings is the control surface for the browser session and for gateway-backed
runtime configuration. Twelve panels, each writing part of the same
config.json you could edit by hand.
Some settings apply immediately. Others need the gateway or the whole app restarted, and the WebUI says so beside the control. Which panel is which, and which config key each one writes, is in Settings.
Browser-only display preferences, such as file edit display mode, take effect immediately for the current browser and do not change gateway configuration.
Capability Gate Surfaces
The capability gate has four surfaces in the WebUI. For the policy behind them,
read capability-gates.md.
Approvals Inbox
Approvals in the sidebar opens the inbox at /approvals. It is the most
safety-critical screen in the product. A human reads it and authorizes a remote
command.
The row carries an unread count while an action waits, so an operator answers an approval during work rather than during configuration. The count comes from a read every five seconds, on every route rather than on this one alone. A hidden browser tab pauses that read.
Each card holds these parts:
- The remaining time, as minutes and seconds. It counts down once per second.
- The command and the hosts, exactly as the executor will run them. The executor rendered that text, and the agent did not describe it.
- The resolved host list, one name per host, plus a count. A group of fourteen hosts shows fourteen names.
- The context lines below a divider. They state that the digest does not cover them. The lines are the origin path, the approval path, the session, the capability class, the scope, the execution context, and the binding digest.
- A Deny control and an Approve control. Deny costs one click, the same as approve. Approve is a split button. See Approve and add.
Approve echoes the digest that arrived with the payload. A digest that does not match refuses the answer, and the action stays pending.
Deny costs one field fewer than approve, because a denial stops an action and authorizes no bytes. The card also states that a denial is terminal for that session, and that only an operator clears the latch afterwards.
Both controls turn off at zero. The card then reads:
This action expired. The executor refused it.
The Approve control is unavailable when the request arrived on the webui path
already. The card states the reason before the click, because path independence
needs a second path:
This request came from the path you are on. Approve it from another authenticated
path. One compromised account must not supply both halves.
Approve and Add
Every approval is a fresh decision, including the fifth identical one.
gates.standingGrants exists for exactly that, and it is the only unattended
allow path. But writing one used to mean leaving the inbox, opening
config.json, and typing the resolved command by hand while the action waited.
Approve is therefore a split button:
┌──────────────────────┬───┐
│ Approve │ ▾ │
└──────────────────────┴───┘
│
├─ Approve and add — expires in 24 hours
├─ Approve and add — expires in 7 days
└─ Approve and add — never expires
The bare click stays Approve. The default action of a split button is the
one people press without reading. So it is the one that grants nothing.
Deny still costs exactly one click, unchanged. A deny that cost more steps than an approve would put a thumb on the scale.
Never expires asks once more — not because it is wrong, and you may legitimately want it. It asks because it is the only option a click makes permanent. The confirmation is where the audit record gets an explicit "yes, permanent" rather than inferring it from a duration.
The caret is absent wherever Approve itself is unavailable: after the deadline,
and for a request that arrived on the webui path. The samePath rule is
untouched, with or without a grant.
The grant that gets written names the command the executor rendered, the hosts it
resolved, and the one context the action ran in. There is nothing on this screen
that describes it, and that is the point. A grant built from a browser-side string
would be a way to widen authority by editing a request. Its id names the
approval that created it, such as
approval-2026-09-03-systemctl-9f3a1c.
Two effects happen in two processes. The decision crosses into the executor
exactly as before. The grant is config, and config is written by the gateway.
The grant write never blocks the approved action: when config.json is
read-only, or the write fails, the action still runs and the screen reports both
facts.
You approved this action. The executor runs it now.
The action was approved. The grant was not saved: Read-only file system
An expired grant is not removed from your config. It appears in Settings →
Security marked expired, because a file the application edits is not the
authority config.json is meant to be. See
configuration-security.md#approve-and-add.
How a Refusal Reads
An answer that does not count returns the rule that refused it, and the screen turns that rule into one instruction. Examples:
| Rule | What the screen says |
|---|---|
not_an_approver | This account is not in gates.approvers. Add it to that list, then answer again. |
no_second_path | Only one authenticated path exists. Add a second path, or declare a standing grant. |
digest_mismatch | Your answer covers other bytes. Reload this page, read the payload again, then answer. |
already_answered | This action already has an answer. One action takes one answer. |
unknown_request | The executor does not hold this request. It expired, or somebody answered it. |
A rule with no sentence of its own falls back to the executor's own wording, so a new rule stays readable.
Two states differ from an empty queue on purpose:
- The gateway cannot reach the executor. The screen says so, because an empty list must never read as "no action waits".
- The gateway holds no inbox at all, and the route answers
503. The screen then states that anapprovedecision waits for the deadline and refuses.
An empty inbox carries its own note. Policy still refuses every action it refuses, so an empty inbox is not an open gate.
The Deployment Rule That Matters
gates.approvers[].sender must equal the actor string exactly. The WebUI builds
that string from the server-side session, so a browser that names itself in the
request body gains nothing.
| Deployment | The actor string |
|---|---|
| A bare API token | webui |
| A trusted proxy that asserts an identity | webui:<identity> |
A deployment behind Cloudflare Access therefore lists webui:ops@example.com and
not ops@example.com. See
configuration-security.md#who-may-answer-an-approval.
Both routes need the API token, because the answer authorizes a remote command.
Gate Policy Panel
Open Settings → Security and find Capability gates. The panel edits the
gates block of config.json, and config stays the source of authority.
The panel holds five parts:
- The policy matrix. One column per context, which is Interactive and Unattended. One row per class, which is Remote execution, Inventory writes, and Read a secret. Remote execution splits into one host, host group, and all hosts. Each cell selects Allow, Approve, Grant only, or Deny.
- The approvers table. Each row holds a channel and a sender. The panel
states that membership in a channel
allowFromlist grants nothing. - The authenticated paths list. Each entry names a channel that can carry an approval.
- The standing grants table. Each row holds contexts, hosts, and commands. The editor states that a command must match the resolved command exactly.
- The audit controls. Retention in days, plus a toggle for full command text. The toggle carries a warning, because a resolved command often holds a secret.
Three details help you review your own policy:
- A value that comes from a shipped default carries a
defaultmarker. A value you set carries none. - The all-hosts cell shows fixed text rather than a control. The schema accepts one value there.
- An empty table states what the deployment refuses. An empty grants table reads "No grants. No automation may run a remote command."
The panel shows a notice when the deployment has fewer than two authenticated paths. A group action then has no runtime approval path, and the notice names the two fixes: add a path, or declare a standing grant.
Latch Banner
A denial latches the capability class for that session. The affected session shows a banner above the transcript.
The banner states:
- the latched class, such as Remote execution
- the time of the denial, and the operator or actor when the log names one
- the count of attempts refused since the denial
- the reason text the gate recorded
View attempts expands the refused attempts that the audit log holds. Clear lifts the latch, and the audit log records the operator who lifted it.
This control lives outside the chat transcript on purpose. A chat command would let the model ask for its own latch to be lifted, so nothing the model writes reaches this control. Elapsed time never lifts a latch, and a new turn never lifts one.
A gateway with no WebUI channel has no latch control at all. It says so at start, and a latched session then stays latched.
Audit Log Viewer
Settings → Security → Gate decisions lists one record per gate decision, newest first. The list holds allows, denials, expiries, and latched refusals.
Filter by decision, capability class, execution context, and date range. Open one row for a detail view with every field of the record.
Four rules govern this view:
- It reads only. It offers no edit control and no delete control, because the log appends.
- It shows the command digest. It shows full command text only while
gates.audit.recordCommandTextis on, and it marks those records. - It shows a same-path warning whenever the request and the approval arrived on one channel, even when policy allowed the action.
- It shows the resolved targets beside the grant id, so you see which addresses ran rather than which names an operator typed.
A gateway that cannot reach the log says so. Read that message as a missing view rather than as an empty log, because the gate still records every decision.
LAN Access
To open the WebUI from another device on the same network, bind the WebSocket channel to all interfaces. The channel also needs a token or a token issue secret:
{
"channels": {
"websocket": {
"host": "0.0.0.0",
"port": 8765,
"tokenIssueSecret": "your-secret-here"
}
}
}
The gateway refuses to start with host set to "0.0.0.0" unless token or
tokenIssueSecret is configured. After the gateway starts, open
http://<your-ip>:8765 from the other device and enter the secret in the login
form.
Remote WebUI clients with a valid token can view and use Apps. Actions that install missing nanoinfra support packages, such as adding a channel dependency, are blocked by default. To let trusted remote administrators change the Python environment through the WebUI, opt in explicitly:
{
"tools": {
"webuiAllowRemotePackageInstall": true
}
}
Use this only for a private deployment where every authenticated WebUI user is trusted to change the Python environment that nanoinfra runs in. If you publish the WebUI through Nginx, Caddy, Cloudflare Tunnel, or a similar service, treat it as remote access. Leave package installs disabled unless that is intentional.
Optional feature installs use pip's configured package index, including
PIP_INDEX_URL.
Troubleshooting
If the page does not open, check these in order:
nanoinfra agent -m "Hello!"works in the same Python environment.~/.nanoinfra/config.jsondoes not explicitly setchannels.websocket.enabledtofalse.nanoinfra gatewayis still running.- You are opening port
8765, not the gateway health port. - LAN access uses
host: "0.0.0.0"and a token or token issue secret.
For detailed diagnostics, see
troubleshooting.md#webui-problems.
For frontend development, see ../webui/README.md.