# The invisible_dots guide Everything about running invisible_dots, in one page. The short version is the [README](../README.md). A **Dot** is a persistent AI agent with a computer of its own. You give it a name, a model and a set of permissions; invisible_dots gives it a QEMU virtual machine on your own PC, with a disk that persists, a Linux desktop, a shell, its own files, memory and skills, and browser identities that keep their cookies and logins from one task to the next. You talk to it from a web UI, the command line, an HTTP API, Telegram, or (opt-in) WhatsApp. The model is any model on OpenRouter, paid with your key. ## Highlights - **A computer, not a sandbox for one command.** Each Dot has a hardware-accelerated VM (KVM on Linux, Windows Hypervisor Platform on Windows) with a persistent disk. It powers off after a quiet spell (`idle_timeout`, 15 minutes in the sample) and starts again for the next message, the next task or one of its own automations; disk, identities, memory and skills stay. - **A browser built not to look automated.** The only browser of a Dot is [invisible-playwright-mcp](https://github.com/feder-cr/invisible_playwright_mcp), on [invisible_playwright](https://github.com/feder-cr/invisible_playwright): a Firefox patched in C++, with the fingerprint set inside the engine instead of injected into the page. Each identity keeps its own profile and fingerprint. - **It learns, as Claude Code does.** The Dot keeps its own memory, notes in `/home/dot/memory`, and its own skills, how it does a kind of task, in `/home/dot/skills`. There is nothing to set: it writes them with its file tools, and its prompt names them. - **You set what it may do.** Every tool belongs to a permission, and each permission is allow, ask or deny. An ask waits for your answer, survives a restart, and lets that one call run once. - **A crash loses nothing you saw accepted.** A restart of the server, a crash of the Dot's engine or a `kill -9` keeps every message and task you were told was accepted, and a tool call cut short is never run twice: the model is told its outcome is unknown and checks before trying again. - **Local and private.** The reasoning happens inside the Dot's VM; the control plane on your PC runs the VMs, the queue, the events, the approvals and the channels. invisible_dots has no server of its own and no telemetry. > [!IMPORTANT] > invisible_dots is alpha. No package or hosted service is published: the one > thing published is the golden image, built by CI from this repository's pinned > inputs and downloaded by `image build` when its inputs are yours; everything > else you build on your own machine. The acceptance run (`tests/e2e/run.ts`) > passes its 16 steps on a Linux host with KVM (the linux-host container on > WSL). On Windows a Dot's VM boots under the Windows Hypervisor Platform with the CPU > model `host,-vmx,-svm` (measured; plain `-cpu host` pauses it), and the rest > of the lifecycle there was driven by hand through the web client, not by an > automated run ([architecture: VM definition](architecture.md#34-vm-definition)). **On this page:** [Quickstart](#quickstart) · [What to ask a Dot](#what-to-ask-a-dot) · [What a Dot can do](#what-a-dot-can-do) · [The web UI](#the-web-ui) · [The command line](#the-command-line) · [Approvals](#approvals) · [The browser](#the-browser) · [Talk to it from your phone](#talk-to-it-from-your-phone) · [How it works](#how-it-works) · [Security](#security-model-and-known-limits) · [Privacy](#privacy) · [Configuration](#configuration) · [Troubleshooting](#troubleshooting) · [Updating](#updating-and-uninstalling) · [Status](#status-and-what-is-not-done-yet) · [Development](#development-and-tests) ## Quickstart Install Node, Go and Git, clone, build the command line, run `setup --all`, run `server`, and open the web UI to make the first Dot. ### Requirements - Linux or Windows, on x86-64, with hardware virtualization on. macOS is not supported. - Node 24 or newer, Git, and Go 1.25 or newer (to build the guest daemon). - QEMU 8.2 or newer; `setup --all` installs it and enables the accelerator. - At least 20 GiB free for the data directory (`doctor` checks it). Each Dot's disk is a copy-on-write overlay that grows as it writes, up to its `computer.disk` (40 GB in the sample). - Memory: `image build` boots a builder VM with 2 vCPUs and 4 GiB, and each running Dot takes its `computer.memory` (4 GB in the sample). Inside a Dot, each open browser takes roughly 0.8 GB, and at most `max_open` (default 3) are open at once. - A data directory path in plain ASCII with no comma. The default is `~/.invisible-dots`, under your account name, so an account named `José` fails at `doctor`; set `INVISIBLE_DOTS_HOME` to a path such as `C:\invisible-dots` before every command, the server included. - An [OpenRouter](https://openrouter.ai) key. ### 1. Get the tools and the code **Windows**, in PowerShell: ```powershell winget install -e --id OpenJS.NodeJS.LTS; winget install -e --id GoLang.Go; winget install -e --id Git.Git $env:Path = [Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [Environment]::GetEnvironmentVariable("Path", "User") git clone https://github.com/feder-cr/invisible_dots cd invisible_dots npm ci npm run build --workspace @invisible-dots/cli ``` **Linux** (Ubuntu 24.04), in bash: ```bash curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - sudo apt-get install -y nodejs git sudo snap install go --classic git clone https://github.com/feder-cr/invisible_dots cd invisible_dots npm ci npm run build --workspace @invisible-dots/cli ``` ### 2. Get the host ready, in one command The same command in PowerShell and bash, from the repository folder: ```sh node apps/cli/dist/invisible-dots.mjs setup --all ``` Run it as yourself, not as root or administrator: it asks for the rights it needs once. It runs four steps in order, and each one skips what is already done; you can still run any of them alone: | step | what it does | by hand | |---|---|---| | 1. The guest daemon | Builds `dot-agentd`, the program that runs inside every Dot's VM, for Linux whatever your host is. It stops here, before changing anything, when Go is missing, and prints the command that installs it. | `go -C guest/dot-agentd build -trimpath -o bin/dot-agentd ./cmd/dot-agentd` with `CGO_ENABLED=0 GOOS=linux GOARCH=amd64` | | 2. QEMU and its accelerator | Checks the host and fixes only what is missing. On **Windows** it enables the Windows Hypervisor Platform and installs QEMU in one elevated step (one UAC prompt). On **Linux** it installs QEMU with `sudo apt-get` (sudo asks for your password). | `invisible-dots setup` | | 3. The web client | Builds it, unless it is built already. | `npm run build --workspace @invisible-dots/web` | | 4. The guest images | Builds the runtime disk (the daemon and the engine), and downloads the golden image (Ubuntu 24.04, the desktop, the browser) that CI built for exactly these inputs, or builds it here when none is published (a changed pin, a fork, no network). A second run with unchanged inputs does nothing. | `invisible-dots image build` (`--no-download` always builds) | Two things can stop it on purpose, and both end with the same advice: run the same command again and it carries on. - On **Windows**, if enabling the accelerator needs a restart, it says so and exits with code 5 after step 2. Restart, come back to this folder and run it again. - On **Linux**, if you are not in the `kvm` group yet it prints `sudo usermod -aG kvm $USER`. Log out and in again, then run it again. Anything else that fails (a missing Go, a failed download, a build error) stops the run with the reason and with nothing after that step run. `doctor` prints one line per check and the command that fixes each failure; `setup --all` ends by telling you what to do next. Ctrl+C stops the image build cleanly, and a second Ctrl+C ends the command at once. ### 3. Start the server and open the web UI ```sh node apps/cli/dist/invisible-dots.mjs server ``` It runs in the foreground until Ctrl+C, and serves the web client too. Open http://127.0.0.2:3000: there is no login. The web client listens on 127.0.0.2, a loopback address of your PC that no Dot's VM reaches (a VM reaches only 127.0.0.1, as 10.0.2.2), and adds the API token to each API call itself. On a computer that is ready except for the key, Home shows a field for your OpenRouter key; then Create a Dot makes the first Dot. The web client is a companion: if it was not built or its port is taken, the server says why and the API and the command line carry on (`server --no-web` leaves it out).
Your first Dot from the command line instead In a second terminal in the same folder: ```sh node apps/cli/dist/invisible-dots.mjs secret openrouter node apps/cli/dist/invisible-dots.mjs init node apps/cli/dist/invisible-dots.mjs create dot.yaml node apps/cli/dist/invisible-dots.mjs message my-first-dot "List the files in your home folder and say how much disk is free." node apps/cli/dist/invisible-dots.mjs logs my-first-dot ``` `secret openrouter` asks for the key (it is never a command-line argument, and what you paste at the prompt is not shown on the screen). `init` writes `dot.yaml`, the Dot's name, instructions, model, resources and permissions, to edit before `create`. The first Dot takes a while to boot; the message waits in the queue until it is ready. `logs` prints the Dot's events and keeps following new ones until Ctrl+C (`--no-follow` prints and exits). It worked when `logs` shows a `message.assistant` event with the answer and what it cost (`spent_usd`). In the rest of this page, `invisible-dots` stands for `node apps/cli/dist/invisible-dots.mjs`, and `invisible-dots --help` lists every command. In **bash**, `alias invisible-dots="node '$PWD/apps/cli/dist/invisible-dots.mjs'"` in the repository folder makes it so (again in each new terminal); in **PowerShell**, type `npx invisible-dots` from the repository folder.
## What to ask a Dot Anything that needs a computer and a person's judgement, done the way a person would do it, for as long as it takes: > Open a browser identity called research, go to https://example.com and tell > me the page's title and its first sentence. Leave the browser open. > Every morning, check one-way fares from Milan to Lisbon for the next two > weeks, write them to ~/workspace/fares.csv and tell me the cheapest day. > Log in to the shop with the shopping identity, download this month's invoices > to ~/documents, and remember where the invoices page is for next time. The first is the conversation in the picture above. For the second, the Dot sets up an automation (it asks you first), and its computer is started each morning for it and sleeps again afterwards. For the third, its prompt tells it to keep what it learned as a note or a skill of its own, which the next prompt names. ## What a Dot can do Its tools, each behind a permission ([architecture: tools](architecture.md#83-tools)): - **Run commands** on its own computer: a shell with a timeout, background jobs, and programs on a pseudo-terminal that it reads as a screen of text. It installs a missing Ubuntu package with `sudo dot-install `. - **Read and write files**: read, list, find, grep, write, edit, apply a patch. - **Use its browsers**: create, launch and close identities; navigate (http and https only), read the page, take screenshots, click, type, select, scroll, go back and forward. - **Look at its desktop** with a screenshot. - **Use the MCP servers you give it**: a program on its computer or a URL, each one declared in its settings, with its own permission (asked by default). See [MCP servers](#mcp-servers). - **Schedule itself**: add, list and remove its own automations (at a time, every interval, or a cron expression). This one asks you first by default; to pause or remove one, ask the Dot. Work reaches it three ways: a message in its one persistent conversation, a queued task (with a priority and an optional start time), or one of its own automations firing. Each task has limits on steps, on context and on cost (`max_cost_per_task_usd`). Long threads are summarized, by the Dot's model or by another model you name (`models.summary`). **Memory and skills.** The Dot keeps its long-term memory itself, one note per file in `/home/dot/memory`, and finds it again with its file tools; the newest notes are named in its prompt. A skill says how to do a kind of task: a folder with a `SKILL.md`, the layout Claude Code reads, whose name and one-line description are in every prompt and whose steps the Dot reads before a task the skill covers ([architecture: memory](architecture.md#86-memory)). The built-in `invisible-playwright` skill teaches it the browser: one identity per person it acts as, a selector before coordinates before a screenshot, the real pointer and keyboard only, and saying a task is impossible rather than getting blocked. The Dot writes skills of its own in `/home/dot/skills` as it learns, and one of its own replaces a built-in one of the same name. > [!NOTE] > Automations run inside the Dot's computer, and the control plane starts the > computer for them. A Dot that is asleep is started shortly before an > automation is due (90 seconds ahead by default), and it is not put to sleep > while one is due, so a job every minute keeps it up. A run whose time passed > while the computer or the server was down is made once, late, when the > computer starts, never once for each occurrence it missed. A computer you > stopped yourself (`invisible-dots computer stop`, or Stop in the web UI) > stays off, and its automations are paused until you start it again; a message > or a task still starts it. A Dot has no web search or fetch tool, no sub-agents and no tool to message anyone: it reads the web through its own browser, and only the control plane talks to chats. ## The web UI The web UI is the same control plane as the command line, with a page for what you would otherwise read as text. It is set in Geist (bundled with it), has a light and a dark theme that follows your system unless you choose, and fits a phone screen. **First run.** On a computer that is not ready for a Dot yet, Home shows a setup checklist: each check of `doctor` that fails, with the command that fixes it and a button that copies it, and a field for the OpenRouter key while none is stored. The Settings page shows the same checks, takes a new key (and pushes it to the Dots that are running), and has the theme. **Create a Dot** is a form in three steps (Identity, Brain, Computer and safety) or the same configuration as YAML, and it refuses what the configuration schema refuses before anything is sent. The permissions start from one of three presets: | preset | what it does | |---|---| | Careful | Asks you before it runs a command or changes a file. | | Balanced | The defaults: works freely on its own computer, and asks before it deletes a browser identity or adds an automation. | | Autonomous | Never asks, except before it deletes a browser identity. | **The Inbox** is everything that needs you, from every Dot: the approvals that wait (the one that has waited longest first), a Dot in an error state, a task that failed in the last 24 hours, and a channel that has to be linked again. An approval says what the Dot wants to do, under which permission and why, shows a command, a diff of a file change, an address or a schedule in the form that reads best, and answers with Allow once, Always allow or Deny with a note. With the keyboard, once the list of approvals has the focus (click it or Tab to it), `j` and `k` move between the cards, `a` allows the selected one once and `d` denies it; a command or a deletion is not allowed by a key press alone. **A Dot's page** has seven tabs: - **Chat**: the one persistent conversation, with a quiet line for each tool the Dot used to answer and approvals you can answer in place. The Dot's computer is always beside it (above it on a narrow screen); both fill the window to the bottom. - **Tasks**: what is running (with its latest progress line and its cost), what is scheduled and queued, and the history. - **Computer**: Screen, Files and Usage. Screen is the Dot's desktop, where an open browser is a window. Files is a read-only walk through `/home/dot`. Usage shows what the computer was given and what it uses, the model spend, the next automation, and Start, Reboot and Stop. - **Skills**: the built-in skills and the Dot's own, read only. - **Channels**: Telegram and WhatsApp, below. - **Activity**: the whole event log as readable lines, filtered by kind, searchable, and exportable as JSON Lines. - **Settings**: the Dot's configuration, as a form or as YAML. Nothing is saved until you have seen the list of what changes, and a configuration changed somewhere else in the meantime is never overwritten. The desktop is shown as pictures that the control plane reads from the guest every few seconds while the page is in view, with a LIVE badge and a warning when a picture is more than 15 seconds old. It is a view and not a remote desktop: it says "The Dot has control", because nothing you do in it reaches the computer. Stop asks first, and says that the computer's automations do not run while you have stopped it. ## The command line | command | what it does | |---|---| | `invisible-dots list` | every Dot, with its state | | `invisible-dots status ` | a Dot, its computer and its recent tasks | | `invisible-dots message ` | a message in its one persistent conversation | | `invisible-dots task [--priority N] [--at ISO-8601]` | a queued task; a higher priority runs first, `--at` sets the earliest start | | `invisible-dots tasks ` | its tasks and their status | | `invisible-dots cancel ` | stop a task that has not finished | | `invisible-dots logs [--tail N] [--no-follow]` | its events, then new ones as they come | | `invisible-dots approvals [--all]` | pending approvals (all of them, the newest first, with `--all`) | | `invisible-dots approve [--always] [--note text]` | let that one call run; `--always` also allows its permission for the Dot from now on | | `invisible-dots reject [--note text]` | refuse it; the model is told no | | `invisible-dots computer start\|stop\|reboot` | power its computer on or off by hand | | `invisible-dots browser identities` | its browser identities | | `invisible-dots mcp ` | its MCP servers: where each is, and which secrets are set | | `invisible-dots secret mcp --dot [--clear]` | set a secret of one of its MCP servers, asked for like the key | | `invisible-dots channel list [--dot ]` | linked chats and who is paired | | `invisible-dots channel remove telegram\|whatsapp --dot ` | unlink a chat; its token or keys and paired people are deleted | | `invisible-dots secret openrouter --dot ` | a key for one Dot instead of the global one | | `invisible-dots update ` | replace its configuration; refused if it changed since the command read it | | `invisible-dots delete --yes` | delete it: its computer, its disk and its channels; without `--yes` it only says what would go | `` is a Dot's name or id, and `--json` gives machine-readable output. ## Approvals - Every tool call goes through one policy gate. `allow` runs it, `deny` tells the model no, `ask` stops the turn and records the call with its arguments ([architecture: policy](architecture.md#84-policy)). - The defaults are permissive for the Dot's own computer: running commands, files and the browser are allowed, except deleting a browser identity, which asks; automations ask; anything unknown is denied. Set any permission to `ask` or `deny` in the YAML to be asked first or to forbid it. - An approval can be answered in the web UI, the command line, a Telegram button or a WhatsApp reply. The first answer wins, a second is ignored, and the approved call runs at most once. `approve --always` turns that ask into an allow for the Dot. - Pending approvals survive a restart of the server or of the Dot. ## The browser Each browser identity is a separate Firefox profile under `/home/dot/browsers//`, with its own cookies, storage, logins and fingerprint, the same fingerprint at every launch. It runs on the Dot's desktop, so it appears in the Screen view and in screenshots of the desktop. The Dot creates, opens and closes identities with its tools, and `invisible-dots browser identities` lists them. A browser has no proxy of its own by default, and nothing asks for one: it inherits the egress of the Dot's VM, and its time zone, language and location follow the exit it actually uses. A proxy for one identity is an explicit option, set when that identity is created, as the URL the browser library reads (`http://user:pass@host:port` or `socks5://host:port`); the library, not this project, judges it when the identity launches. A Dot can have a VM proxy: the whole computer of the Dot, browser, commands and the engine's requests to OpenRouter included, then goes out through it. Set it in the Dot's settings (VM proxy) or with `invisible-dots secret proxy --dot ` (asked for like the key; `--clear` removes it). It is a SOCKS5 URL, `socks5://user:password@host:port`, stored encrypted and never shown again, and the Dot uses it from its next start. If the proxy cannot be reached, nothing leaves the VM. When a VM proxy and an identity proxy are both set, the identity's proxy is reached through the VM's tunnel. Launching is explicit: a page action on an identity that is not open fails, so denying `browser.identity.launch` cannot be undone by navigating. At most 3 identities are open at once (roughly 0.8 GB each), the least recently used one closing first, and at most 20 exist. Screenshots go to the model and are never stored. How well the browser holds up against bot checks is measured in [invisible_playwright's README](https://github.com/feder-cr/invisible_playwright), on the suites it names. invisible_dots adds no claim of its own: a site can still refuse a Dot for what it does, and the terms of the sites you point it at still apply. ## MCP servers A Dot uses the tools of any MCP server you declare, the way Claude Code and Codex do: in its settings (MCP servers), or in its YAML. ```yaml mcp_servers: time: command: uvx # runs on the Dot's computer, as the Dot's user args: [mcp-server-time] search: url: https://search.example/mcp secrets: [Authorization] # its value is set apart, never in the YAML ``` A server's tools are named `mcp__`, and one permission covers them all, `mcp.`, which asks you before each call until you allow it (or "Always allow" an approval). A secret, an API key or a token, is set in the settings next to its server or with `invisible-dots secret mcp --dot ` (asked for like the key; `--clear` removes it): it reaches the server as an environment variable or a header, never through the YAML or a command line. The Dot can add a server itself, as NanoClaw's agents can: it installs the program (`uvx`, which is there, or from GitHub with `uv tool install git+https://...`; for `npx` it runs `sudo dot-install nodejs npm`), then calls `add_mcp_server`. That asks you first (`mcp.servers.add`): the approval shows what the server runs or where it is, the secrets you will set, and why; approved, the server is in the Dot's settings and its tools are the Dot's from its next message, each asking you until you allow it. Only you change or remove a server. A server that cannot start says why, in its settings, in `invisible-dots mcp ` and to the Dot itself, and it is started again when the Dot's next message or task begins. ## Talk to it from your phone **Telegram.** Make a bot with @BotFather (one bot per Dot), then: ```sh invisible-dots channel add telegram --dot my-first-dot invisible-dots channel pair telegram --dot my-first-dot ``` `channel add` asks for the bot's token and stores it encrypted. `channel pair` prints a one-time link, valid ten minutes: open it in Telegram and press Start, and that account is the Dot's owner from then on. Messages from anyone else are dropped before they cost anything. Approvals arrive with Approve and Reject buttons, the arguments cut to 300 characters. Telegram bot chats are not end-to-end encrypted. Text only. A channel has three settings, all on by default: `approvals` (ask approvals in the chat), `show_arguments` (show the tool's arguments in them) and `notify_tasks` (what the Dot says on its own: finished and failed tasks, and the answers to its automations). The Channels tab of the web UI has a switch for each, and the API sets them too: ```bash curl -X PATCH http://127.0.0.1:8787/api/dots/my-first-dot/channels/telegram \ -H "Authorization: Bearer $(head -1 ~/.invisible-dots/config/api.token)" \ -H "Content-Type: application/json" \ -d '{"settings": {"show_arguments": false}}' ``` **WhatsApp** is opt-in, and it needs a warning first. > [!WARNING] > This is not an official way to use WhatsApp. It links the Dot as a device of > a personal account through an unofficial client (Baileys, pinned to one > release candidate), which WhatsApp's terms do not allow for automation and > which can get the account restricted or banned. Use a number of its own, such > as a spare SIM or eSIM, never the one you live on. Install the client once, from the repository folder, then start the server with `INVISIBLE_DOTS_WHATSAPP=1`: ```sh npm run whatsapp:install invisible-dots channel link whatsapp --dot my-first-dot invisible-dots channel pair whatsapp --dot my-first-dot ``` `channel link` shows a QR code to scan under WhatsApp > Settings > Linked devices; the linked device's keys are stored encrypted. `channel pair` prints a link that opens a chat with the number and `pair ` ready to send. The Dot never writes first, to anyone, answers only people who paired, in private chats, and takes text only. Approvals are answered in words (`yes ap-xxxxxx` or `no ap-xxxxxx`); a bare `yes` is an ordinary message. Baileys depends on `libsignal`, which is GPL-3.0, so nothing of it is installed by default: `npm ci` installs neither, and neither is in the bundled command. `npm run whatsapp:install` installs them into `optional/whatsapp/`, pinned to one release by a lock file with the integrity of every package, and the server loads them from there only when WhatsApp is linked ([THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md), [architecture: messaging channels](architecture.md#98-messaging-channels)). ## How it works The web UI and the command line call the HTTP API; the scheduler behind it keeps the queue in a database and starts and stops one QEMU VM per Dot; inside the VM, the engine reasons with OpenRouter, runs the model's commands through the computer daemon and drives its browsers on the VM's desktop; Telegram and WhatsApp reach the scheduler through the channel hub. ```mermaid flowchart LR subgraph PC["Your PC: one invisible-dots server process"] WEB["Web UI
127.0.0.2:3000"] CLI["CLI"] API["HTTP API and SSE
127.0.0.1:8787"] SCH["Scheduler
queue, outbox, approvals, sleep and wake"] HUB["Channel hub"] VMM["vm-manager
the QEMU driver"] DB[("PostgreSQL
embedded PGlite by default")] end subgraph VM["One QEMU VM per Dot"] AGD["dot-agentd
the computer daemon, user dotagentd"] ENG["Engine, a nanobot fork
user dotengine, key in memory"] DESK["Xvfb and XFCE desktop"] BR["invisible-playwright-mcp
one per open identity"] end WEB -->|"API calls with the token"| API CLI -->|"API calls with the token"| API API -->|"messages, tasks, answers"| SCH HUB -->|"chat messages and approvals"| SCH SCH -->|"queue, events, approvals"| DB SCH -->|"start, stop, reboot"| VMM VMM -->|"starts and stops"| VM SCH -->|"loopback port forward
token after an HMAC proof"| AGD AGD <-->|"proxy, and the model's commands"| ENG ENG -->|"browser tools over MCP"| BR BR -->|"windows on"| DESK ENG -->|"model requests"| OR["OpenRouter"] HUB <-->|"bot messages"| TG["Telegram"] HUB <-->|"linked device"| WA["WhatsApp, opt-in"] BR -->|"pages, from the VM's egress"| SITES["The web"] ``` - **One process on the host.** `invisible-dots server` runs the API, the scheduler, the VM driver and the channel hub, and starts the web client as a child (`--no-web` leaves it out). The database is an embedded PostgreSQL (PGlite) in the data directory, or an external PostgreSQL 16 or newer through `DATABASE_URL`. - **One VM per Dot.** QEMU with KVM or the Windows Hypervisor Platform; never software emulation. A copy-on-write disk over a golden image, plus a read-only runtime disk with our code. Both images are built from pinned, hashed inputs ([architecture: two images](architecture.md#33-two-images-two-lifetimes)). - **The engine** is a hard fork of [nanobot](https://github.com/HKUDS/nanobot) in Python, cut down to its core: the turn runner, OpenRouter, the tools above, automations, skills, and an MCP client that serves only the browser ([invisible_engine_dots/UPSTREAM.md](../invisible_engine_dots/UPSTREAM.md)). - **Durable on both sides.** The host writes what it tells a guest in the same transaction as the decision, so a restart loses nothing you saw accepted ([architecture: durable queue](architecture.md#92-durable-queue)). The engine keeps its state in one SQLite file and commits each step with the events that describe it; its event stream keeps its order across a `kill -9`. A task cut short by a crash is started again with a note that the last attempt was interrupted, and fails once it has been started three times ([architecture: crash recovery](architecture.md#87-crash-recovery)). - **The server can restart without stopping Dots.** QEMU runs detached, and a new server adopts the VMs it finds running. A VM that dies while its Dot has work is started again. The full contract every part is written against: [docs/architecture.md](architecture.md). ## Security model and known limits What protects you: - The OpenRouter key is never written to the image, the disk or the seed. It is pushed to the Dot after every start and held in the engine's memory only ([architecture: secrets](architecture.md#43-secrets)). - The engine and the computer daemon each run as a user of their own, and the model's commands as a third, `dot`, which can reach neither: it cannot read the engine's state or the Dot's token, cannot connect to the engine's socket (so it cannot change the Dot's permissions or approve its own calls), cannot stop either daemon and cannot become root ([architecture: processes](architecture.md#41-processes-systemd-each-unit-as-the-user-it-names)). The one thing it may do as root is install Ubuntu packages it is missing, `sudo dot-install ...`: the command takes package names and nothing else, so no option reaches apt; the packages' own install scripts run as root, and they come from the Ubuntu archive. - The channel between host and guest goes one way: the host calls the guest on a loopback port, with the Dot's token, and only after the guest proves it holds that token. The API needs a bearer token (the command line reads it from `config/api.token` itself); the web UI needs none and listens on 127.0.0.2, which no Dot's VM reaches. - Files the host shows from a Dot come only from `/home/dot`, at most 16 MiB, never served as a type a browser would run. - The proxy of an identity, when it has one, is not shown by the host: an identity says only that it has one, an approval shows it as `***`, and no event carries it. - Secrets on the host (the OpenRouter key, channel tokens, WhatsApp keys) are stored encrypted; see [Configuration](#configuration). What it does not protect against, by design or not yet: - The model's commands run as `dot`, which can read the proxy of an identity that was given one, password included, from the open browser's environment and from the browser server's own session file, and what the browser server says of a proxy it refuses reaches the model and the engine's log as it is. An identity with no proxy, the default, has nothing to read. - The split between the three users is the guest operating system's. A flaw in the guest's kernel, or a way to become root that this repository does not know of, would end it. The VM is what stands between the Dot and your PC then, with the reach into your PC's loopback listed below. - The computer daemon holds three capabilities (`CAP_SETUID`, `CAP_SETGID` and `CAP_KILL`, to run the model's work as `dot`) and may run one command as root, the power-off. A flaw in the daemon would be worth more than one in an ordinary program. Its one door, a TCP port inside the guest, needs the Dot's token, which `dot` cannot read. - Nothing limits how much disk, memory or processor time the model's commands use of their own VM, so a runaway command can starve the Dot's own daemons until the VM is stopped or restarted. - When typing in the browser is set to `ask`, the approval shows the text being typed, password or not. - Each Dot can reach services on your PC's loopback through `10.0.2.2`. Everything invisible_dots listens on there needs a credential the Dot lacks, but nothing else should listen there unauthenticated ([architecture: networking](architecture.md#36-networking-and-its-limits), [architecture: browser identities](architecture.md#6-browser-identities)). ## Privacy invisible_dots runs on your machine and has no server of its own. What leaves it, and to whom: - **OpenRouter** gets the conversation, the tool results and what the Dot reads, under your key. Requests carry the headers `HTTP-Referer: https://github.com/feder-cr/invisible_dots` and `X-Title: invisible_dots`. - **Telegram and WhatsApp** carry the messages and approval prompts of a linked channel, and can read them. Link previews are off, so neither service fetches a link of a message on its own; a person who taps a link opens it themselves. - **The sites a Dot visits** see its browser, coming from the egress of the Dot's VM, or from the identity's proxy when you gave that one identity a proxy. - **Publishers of what the images are made of** (Ubuntu, uv, the Python packages, the browser engine) serve the downloads when you build them; on Windows, `setup` downloads the official QEMU installer. - **The browser layer.** [invisible-playwright-mcp's privacy policy](https://github.com/feder-cr/invisible_playwright_mcp#privacy-policy) says each browser launch fetches a one-line counter file from a GitHub release, carrying no identifier. - **Address-echo services** (`api.ipify.org`, `icanhazip.com`, `checkip.amazonaws.com`) are asked by the browser library at each launch for the public address the browser exits from, to set the time zone and locale, and it keeps its GeoIP database current from its GitHub release. - **The web UI** loads nothing from other sites: its pages, scripts and styles come from the server on your PC, its fonts (Geist) included. A link you press (to @BotFather, to an OpenRouter page) opens that site. - **No telemetry of invisible_dots.** Nothing of its own reports on how you use it: Next.js's build telemetry is off in the web client's own config, so no way of building it sends any, and the Dots' Ubuntu has no crash reporter and no pollinate. The browser layer's launch counter, above, is that library's. Your Dots' disks, memory and conversations stay in the data directory and inside their VMs. ## Configuration A Dot is one YAML file, checked by one schema: name, instructions, model, computer (cpu, memory, disk, idle timeout), permissions, limits and MCP servers. Every field and its range is in [architecture: Dot configuration](architecture.md#7-dot-configuration); `invisible-dots init` writes a sample. One server runs per data directory. Secrets (the OpenRouter key, channel tokens, WhatsApp keys, MCP servers' secrets) are stored with AES-256-GCM under `config/master.key`; anyone who can read the data directory can decrypt them, so it is kept private to your user.
The environment variables the host reads | variable | default | what | |---|---|---| | `INVISIBLE_DOTS_HOME` | `~/.invisible-dots` | the data directory: keys, database, images, VM disks, logs. Plain ASCII, no comma | | `INVISIBLE_DOTS_LISTEN` | `127.0.0.1:8787` | where the server's API listens | | `INVISIBLE_DOTS_URL` | `http://127.0.0.1:8787` | where the CLI finds the server | | `INVISIBLE_DOTS_TOKEN` | first line of `config/api.token` | the API token, instead of the file | | `INVISIBLE_DOTS_WEB_LISTEN` | `127.0.0.2:3000` | where the web client listens; keep it off 127.0.0.1, which every Dot's VM reaches | | `INVISIBLE_DOTS_WEB_ALLOWED_HOSTS` | loopback only | other host names the web client answers to | | `INVISIBLE_DOTS_QEMU_DIR` | official installer's folder, then `PATH` | the one folder QEMU is looked for in | | `INVISIBLE_DOTS_WHATSAPP` | off | `1` offers the WhatsApp channel (after `npm run whatsapp:install`) | | `DATABASE_URL` | embedded PGlite | an external PostgreSQL 16 or newer | | `INVISIBLE_DOTS_DEBUG` | off | `1` adds debug lines to the server's log |
## Troubleshooting - `invisible-dots doctor` first: one line per check, and the command that fixes a failure. - The server logs to the terminal it runs in; `INVISIBLE_DOTS_DEBUG=1` adds debug lines. - A Dot that does not start: QEMU's own output is in `~/.invisible-dots/logs/qemu-.log`, and the guest's serial console in `~/.invisible-dots/vms//serial.log` (rewritten at each start). `invisible-dots list` shows each Dot's id. - What a Dot did and why it stopped: `invisible-dots logs ` and `invisible-dots tasks `, or the Dot's Activity tab in the web UI. - The web UI does not open: `invisible-dots server` says why it did not start it (not built, port taken) in its output. Its address is http://127.0.0.2:3000, not 127.0.0.1. ## Updating and uninstalling **Updating.** Stop the server (Ctrl+C), then in the repository folder: `git pull`, `npm ci`, the builds of the command line (`npm run build --workspace @invisible-dots/cli`), of the web client (`npm run build --workspace @invisible-dots/web`) and of dot-agentd (step 1 of `setup --all` above), `invisible-dots image build`, and `invisible-dots server` again. Running Dots keep running meanwhile: QEMU is not a child of the server. Each VM start takes the newest runtime image (our code), so `invisible-dots computer reboot` moves a Dot to it. A Dot keeps the golden image it was created on, and new Dots get the newest one; this version has no way to move a Dot to a new golden image. **Uninstalling.** Stop each Dot's computer (`invisible-dots computer stop`) while the server runs, since a VM outlives the server, then stop the server and delete the data directory (`~/.invisible-dots`, or your `INVISIBLE_DOTS_HOME`) and the repository folder. What `setup` installed stays until you remove it yourself: QEMU, the Windows Hypervisor Platform feature on Windows, and your membership of the `kvm` group on Linux. ## Status and what is not done yet Alpha. Nothing is released yet; the golden image is the one thing published. - **The real-VM acceptance run is not in CI.** It (`tests/e2e/run.ts`: build, create, browse, approve, kill the VM, restart, scan the disk for the key) passes its 16 steps on a Linux host with KVM, run by hand; CI keeps its contract in step with the product and tests the guest with the container smokes, which run its daemons without a VM. - **Windows is measured by hand, not by an automated run**: Dots on WHPX were driven through the web client (chat, tasks, approvals, files, browser identities, sleep and wake, reboot). It runs the same code path as Linux, and every difference between the two is listed in [architecture: one mechanism on every host](architecture.md#11-one-mechanism-on-every-host). - **The install is one command after the checkout, not before it.** Node, Go and Git, the clone, `npm ci` and the build of the command line itself are still typed by hand, because the command needs them to exist before it can run. - **Channels carry text only**, one Telegram bot per Dot; the official WhatsApp Cloud API is a later adapter on the same hub. - **Not in this version**: snapshots and rollback, backups, quotas, network policies, controlling the desktop or an interactive terminal for you (the web UI shows the desktop as pictures, and nothing you do there reaches the computer), artifacts, several hosts, organisations and roles, macOS hosts ([architecture: out of scope](architecture.md#10-out-of-scope-for-this-version)). ## Development and tests Besides the quickstart's tools, development needs Python 3.11 or newer for the engine, and Docker for the smokes. ```sh npm ci npm run typecheck npm test # vitest over the TypeScript workspace and tests/repo git config core.hooksPath .githooks # the pre-push gate: typecheck, vitest, go test, prose checks ``` The pre-push gate's prose checks need Python with the pinned invisible-core. The hook looks for it in `tmp/gates-venv` first: `python -m venv tmp/gates-venv`, then `tmp/gates-venv/bin/python -m pip install -r .github/gates-requirements.txt` (on Windows the interpreter is `tmp\gates-venv\Scripts\python.exe`). - The engine: `pip install -e ".[dev]"` and `pytest` in `invisible_engine_dots/` (Linux only: it uses unix sockets). - dot-agentd: `go test ./...` in `guest/dot-agentd/`. - The web client's browser tests: build it (`npm run build --workspace @invisible-dots/web`), install the browser they drive (`npx playwright install chromium`) and run `npm run test:e2e --workspace @invisible-dots/web`. Each run starts the real control plane over a fake VM layer and the built web client as the product runs it. - The engine smoke and the browser smoke run the guest's two daemons, and the real invisible-playwright-mcp with its Firefox, in Linux containers: `bash guest/image-builder/test/smoke/run.sh` (add `--suite browser`); only Docker is needed ([the smoke tests README](../guest/image-builder/test/smoke/README.md)). - The acceptance run on a real VM, `node tests/e2e/run.ts`, needs a Linux host with KVM and an OpenRouter key, and is not part of CI ([the acceptance run README](../tests/e2e/README.md)).
What CI runs, and where the code lives CI runs on pushes to main and on pull requests: typecheck and vitest on Linux and Windows, the database layer against a real PostgreSQL, dot-agentd's Go tests and static build, the engine's pytest, the ISO images read by the Linux kernel, both smokes, the web build with its Playwright tests, the opt-in WhatsApp client against its real library, and two prose gates (English only, and a README that promises nothing it cannot support). The vitest, Go, pytest and Playwright runs are counted against the floors in `.github/test-floors.json`, and the smokes fail on a skipped check, so a test that stops running fails the build. | folder | what | |---|---| | `apps/` | `api`, `scheduler`, `vm-manager`, `web`, `cli` | | `packages/` | shared types and schemas, database, events, channels, the ISO writer, the SDK | | `guest/` | `dot-agentd` (Go) and the image builder | | `invisible_engine_dots/` | the Dot's engine (Python) and its built-in skills | | `virtualization/` | the pinned QEMU, cloud-init templates, base image metadata | | `tests/` | checks over the whole repository, the real-VM run, and the agent benchmark |
## Acknowledgements - [nanobot](https://github.com/HKUDS/nanobot), the agent the Dot's engine is a fork of. - [invisible_playwright](https://github.com/feder-cr/invisible_playwright) and [invisible-playwright-mcp](https://github.com/feder-cr/invisible_playwright_mcp), the Dot's browser. - [QEMU](https://www.qemu.org), [Ubuntu cloud images](https://cloud-images.ubuntu.com), [PGlite](https://pglite.dev), [Next.js](https://nextjs.org) and [Baileys](https://github.com/WhiskeySockets/Baileys). - The Agent Skills layout of [Claude Code](https://github.com/anthropics/claude-code), which the Dot's skills follow. ## License MIT ([LICENSE](../LICENSE)). The engine under `invisible_engine_dots/` is a fork of nanobot, also MIT, under its own [LICENSE](../invisible_engine_dots/LICENSE); parts of this repository's history come from Open Multi-Agent, also MIT. Their notices are in [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md). QEMU (GPL-2.0) is installed from its official installer or your distribution and only run as a separate program, never bundled. The guest operating system, the browser engine and the packages in the golden image keep their own licenses; the published golden image is Ubuntu with those packages, and whoever copies it takes on those licenses ([architecture: licensing](architecture.md#113-licensing)). Nothing under the GPL is installed by the default `npm install`; the WhatsApp client, which brings a GPL-3.0 dependency, is an opt-in install. invisible_dots is an independent project, not affiliated with OpenRouter, Telegram, WhatsApp or Meta.