Self-hosted agent for infrastructure

Root access,
on a short leash.

nanoinfra reaches your servers over SSH, Ansible or SSM with credentials you hold. A capability gate in a separate process decides on every remote command — and the agent that asks holds neither the credential nor the decision.

uv tool install nanoinfra
nanoinfra onboard --wizard

One decision, three answers, one record

Five processes,
five accounts

Measured on a running deployment

a container, v2.0.4

ps -eo user,comm --forest

▸ nanoinfra            gateway
  root                 \_ entrypoint.sh
  nanoinfra-exec       |   \_ exec
  root                 \_ entrypoint.sh
  nanoinfra-fetch      |   \_ fetch
  root                 \_ entrypoint.sh
  nanoinfra-mcp        |   \_ mcp
  root                 \_ entrypoint.sh
  nanoinfra-connector      \_ connector

The agent loop, the channels and the WebUI. It asks for every remote action and is answered by another process — it holds no credential and makes no decision.

Account

nanoinfra

the uid you installed as

Secrets key in this process

absent

exec'd with the variable removed

Serves

127.0.0.1:8765

WebUI and the OpenAI-compatible API

decisions allow approve deny

Every remote command
passes a gate

Four answers, three scopes, three contexts. The shipped policy refuses every remote command in an unattended turn, and only a standing grant you wrote into config permits one.

allow

runs

approve

asks a person

grant

matches config

deny

never runs

unattended · mutate.remote · host=grant group=deny all=deny

Read the gate reference →

A grant is a sentence, not a checkbox

You write it, a reviewer reads it, and it permits that command on that host and nothing adjacent. A deny stays a deny.

A refusal is on the record

The executor appends every decision to its own audit log, which the agent may read and may not write. Denial latches survive a restart because they are rebuilt from that log.

An automation rehearses first

You learn what a 03:00 run will meet when you create it, not at 03:00. A job that would be refused saves itself disabled, carrying the finding.

See all three in the simulated session ↗

The workspace is a
place you can look at

A tree, a preview, and bytes the browser can render

default 19 items · 6 hidden Upload · New folder
▸ cron/
▸ diagrams/
▸ memory/
▸ secrets/
▸ servers/
  AGENTS.md                    1.9 KB
  runbook-failover.md          6.0 KB
  staging-topology.drawio       13 KB
  staging-topology.png         242 KB

Preview · image/png, sniffed from the bytes

A saved topology diagram, rendered in the preview panel

An image, a PDF, or a download

The preview used to be text by construction: a NUL byte in the first 4 KiB was a refusal, so a picture the agent had just written never even became a clickable link. Now the type comes from the bytes — never the file name — and the panel renders it. A PDF opens in the browser's own viewer, sandboxed. Anything else offers itself as a download.

Upload a folder, and see what will be written first

Drag a directory in, review the plan, and it transfers in frame-sized chunks up to 100 MB. .git stays out unless you ask for it.

More than one workspace

A workspaces root with a switcher, and an existing install is moved under it rather than left outside the root its own picker reports against.

Every capability →

Reach it from where you already work

Ten channels, one agent loop. The gate does not care which one a message arrived on — an unattended turn is an unattended turn whether it came from cron or from a chat.

Telegram

DM pairing

Slack

socket mode

Discord

guild + DM

Matrix

E2EE rooms

WebUI

127.0.0.1:8765

Signal

signal-cli

Email

IMAP + SMTP

MS Teams

bot framework

Mattermost

self-hosted

WhatsApp

pairing code

Every capability →

Add a tool, and
decide where it runs

Three ways in, and each one lands somewhere different

In the agent

Skills

A directory with instructions in it. You read one before you enable it, and the agent writes you a new one on request.

~/.nanoinfra/skills/

In its own process

MCP servers

A stdio server gets its own account and its own Landlock policy, so a bad one costs you a process rather than a credential. You choose which of its tools the model sees.

stdio · sse · streamableHttp

In your code

Plugins

A Python entry point registers a tool with its own capability class, so the gate treats it like any other action.

nanoinfra.tools

From the blog

All posts →
decisions allow approve deny

Root access, on
a short leash.

Your machine, your credentials, and a boundary that holds when nobody is watching. Read the code before you trust it. It is small on purpose.