Skip to main content

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:

  1. Open Settings → Models and configure a provider, credential, and active model preset.
  2. Send Hello! in a new topic to prove the selected model works.
  3. Start a separate topic before project work, then choose the intended workspace and access mode.
  4. 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.
  5. 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​

AreaUse it for
TopicsStart, switch, search, fork, and delete browser topics
Agent activitySee thinking, tool calls, file edits with diffs, command output, and generated artifacts in context
WorkspacePick the project workspace before asking for file or shell work
AccessChoose the access mode for local capabilities allowed by your gateway configuration
ComposerSend text, images, voice input, slash commands, and @ mentions for topics, Apps, or MCP presets
ChannelsConnect and validate chat platforms, install their optional support, and manage saved channel setup
AppsInstall, test, update, and use local CLI App adapters and MCP presets
SkillsInspect available built-in and workspace skills before relying on them
AutomationsReview, 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
ApprovalsRead one suspended remote action and approve or deny it. See Approvals Inbox
SettingsAdjust 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 providesAgent workspace continues to provide
Project AGENTS.mdSOUL.md and USER.md
Relative file paths and shell working directoryLong-term memory and history
The normal read/write boundary in Restricted modeCustom 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.active in config, applied when the agent starts. See data-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.

SectionPermission
Runtime / identityYours — this is the persona
Safety notesFixed
Bootstrap filesYours per workspace, through AGENTS.md and friends
Tool usage notesFixed
MemoryYours
Skills, connectors and tool groups advertisedFrom config
Agent addendumAppended, 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 as nanoinfra 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:

TabAnswers
UsageWhat a window cost, per day and per model, with a cost column once pricing names a rate
LiveWhat is happening now, as nine point-in-time gauges. A gauge that could not be read shows —, which is not zero
CallsOne 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
ApprovalsWhether 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:

  1. The remaining time, as minutes and seconds. It counts down once per second.
  2. The command and the hosts, exactly as the executor will run them. The executor rendered that text, and the agent did not describe it.
  3. The resolved host list, one name per host, plus a count. A group of fourteen hosts shows fourteen names.
  4. 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.
  5. 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:

RuleWhat the screen says
not_an_approverThis account is not in gates.approvers. Add it to that list, then answer again.
no_second_pathOnly one authenticated path exists. Add a second path, or declare a standing grant.
digest_mismatchYour answer covers other bytes. Reload this page, read the payload again, then answer.
already_answeredThis action already has an answer. One action takes one answer.
unknown_requestThe 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 an approve decision 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.

DeploymentThe actor string
A bare API tokenwebui
A trusted proxy that asserts an identitywebui:<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:

  1. 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.
  2. The approvers table. Each row holds a channel and a sender. The panel states that membership in a channel allowFrom list grants nothing.
  3. The authenticated paths list. Each entry names a channel that can carry an approval.
  4. The standing grants table. Each row holds contexts, hosts, and commands. The editor states that a command must match the resolved command exactly.
  5. 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 default marker. 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.recordCommandText is 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:

  1. nanoinfra agent -m "Hello!" works in the same Python environment.
  2. ~/.nanoinfra/config.json does not explicitly set channels.websocket.enabled to false.
  3. nanoinfra gateway is still running.
  4. You are opening port 8765, not the gateway health port.
  5. 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.