568 Commits
Author SHA1 Message Date
Federico Elia ef69f5e19c Merge pull request #1454 from feder-cr/libs-latest
browser: never pinned; the image takes the newest release and the engine reads the server it finds
golden-a61faff1b2d9 golden-0657d4ff61e9 golden-73536fa97ad6 golden-798090c64bb0 golden-8dca7f57f998 golden-9c2b3c5b93ef golden-a461e2d8e225 golden-67532b5680e8 golden-a92891c4ded8 golden-0b39ccd1b116 golden-b38d7171e8ec golden-1cf048d96945 golden-ce0e2cde6174 golden-d27023f0394d
2026-10-14 18:38:56 -07:00
feder-cr ed406afb14 smoke and dependencies: the stand-in is a server package, and the capture runs on the server's python
The engine smoke found its stand-in by fake_mcp_server.py on the command line; installed the way the real server is,
it runs as `python -I -B -m invisible_playwright_mcp`, and that command line is what the checks look for.
test_dependencies names server_capture.py as a module another interpreter runs: its invisible_playwright_mcp import
is the installed server's, not a dependency of the engine.
2026-10-15 02:18:52 +02:00
feder-cr 2968ca8372 browser: never pinned; the image takes the newest release and the engine reads the server it finds
A Dot keeps the golden image it was made from and boots every new runtime disk, so the engine already talked to
servers other than the one its pinned copy of the interface described. The engine now reads the interface from the
installed server when it starts (nanobot/dots/server_interface.py, server_capture.py run as dot by the server's own
python): the page tools are what that server serves, a page tool it does not serve is not offered (GET /tools says
so), and a Dot whose server cannot be read offers no page tool and its launches say why.

With nothing in the engine tied to a version, the image no longer pins one: each build asks PyPI for the newest
invisible-playwright-mcp, the newest invisible-playwright it accepts and the invisible-core that one pins
(browser-release.ts). The three enter the inputs digest, pins.env and the manifest, which records what was installed.
mcp-requirements.lock and capture-mcp-interface.py go; the copy of the interface becomes a test fixture. The owner's
no-other-browser rule is checked at build on what was actually installed (builder/other-browsers.txt), and the golden
image workflow runs every six hours so a new release is published as a prebuilt image.
2026-10-15 02:04:31 +02:00
Federico 4d20e947a1 Merge pull request #1450 from feder-cr/libs-0722
image: the browser is invisible-playwright-mcp 0.72.2 (invisible-playwright 0.35.7, invisible-core 45.39.0, firefox-45)
golden-27aac91db9e8
2026-10-13 14:33:32 -07:00
feder-cr 6712104ea5 image: the browser is invisible-playwright-mcp 0.72.2 (invisible-playwright 0.35.7, invisible-core 45.39.0, firefox-45)
The server's snapshot and read_html read again on the page that replaced the one they went to (page.evaluate_read),
and browser_evaluate, the model's code, runs once when its page navigates under it (B285). Under them, through
firefox-45: a page that replaces itself keeps its main world (B286), the real mouse no longer turns a click into a
drag, a session keeps focus when another starts beside it, iframes stay in the frame tree after a few navigations,
go_back/go_forward to a same-document entry answer, a browser that dies ends every wait at once, every input event
is born a few ms before the page sees it, and the first enumerateDevices of a session takes a time the seed
declares. The tools' schemas and descriptions are unchanged: the recaptured interface differs only in its version.
2026-10-13 22:25:33 +02:00
Federico a2584f2619 Merge pull request #1448 from feder-cr/no-char-caps
engine: no fixed cap on a tool's result; the cut and the routing follow the model's window, read from OpenRouter on every turn
golden-f60c6fbd46aa
2026-10-12 08:35:49 -07:00
feder-cr fe03ee6325 floors: the engine suite at the 2493 tests CI measured on this branch 2026-10-12 17:20:59 +02:00
feder-cr 5c920517e4 engine: a model's requests go only to the providers of its widest window; image: invisible-playwright-mcp 0.72.1
OpenRouter routes a request by the answer it asks for but not by the prompt it carries, so a request sized for
the model's widest window could land on a provider with a narrower one (glm-5.3-flash: Reka at 262144 among
providers at 1048576) and fail there. The providers read with the limits on every turn now route the model's
requests: provider.only is the slugs of the widest window's providers. When none of them can serve, OpenRouter
refuses with "No allowed providers are available" and the turn fails with those words.

invisible-playwright-mcp 0.72.1 drops its last caps: browser_type quotes the value the field kept whole, and an
error detail in its chat and routes comes whole. The tools' schemas are unchanged.
2026-10-12 16:35:10 +02:00
feder-cr 440e43a1f5 engine: no fixed cap on a tool's result; the cut is the model's window, read from OpenRouter on every turn
A result comes back whole. exec, glob/grep and read_file lose their fixed caps (10000/50000, 12000 and 2000
per line, 128000) and the general 12000-character cut is gone. A request that outgrows the model's window is
compacted first, as before; only if it still does not fit are the results of its latest round cut to what the
window leaves, measured with the estimate that judges the request, and the cut says how many characters came
through of how many and how to ask for less. A result is stored cut only beyond what no compaction could make
room for.

The window is read on every turn from the providers OpenRouter lists for the model
(GET /models/{model}/endpoints), not once per process from the whole list: of the providers serving it, the
widest window and the longest answer at that window.
2026-10-12 12:43:40 +02:00
Federico 00f63e88c1 Merge pull request #1447 from feder-cr/browser-libs
image: the browser is invisible-playwright-mcp 0.72.0 (invisible-playwright 0.35.0, invisible-core 41.36.0)
golden-e0c6bb5c740a
2026-10-12 07:53:06 +02:00
feder-cr 9610d12e29 image: the browser is invisible-playwright-mcp 0.72.0 (invisible-playwright 0.35.0, invisible-core 41.36.0)
The core keeps a frame's navigation history when Firefox re-announces the main frame on a cross-process
navigation, redoes a by-value call whose execution context a navigation destroyed, and answers a goto that
another navigation overtook with Playwright's "interrupted" error instead of waiting 45 s. The wrapper carries
invisible_playwright#321's fix. The server reads an element's text in one command and returns it whole:
max_chars is gone from browser_read_text and browser_snapshot. The interface is captured again from the lock.
2026-10-12 07:27:49 +02:00
Federico 81eb3ba067 Merge pull request #1446 from feder-cr/hero-from-photos
docs: the README animation is built from photos in the tree, with new characters
2026-10-12 03:01:05 +02:00
feder-cr 9151ce3386 docs: the README animation is built from photos in the tree, with new characters
The six hero GIFs were rendered from pages that lived only in a scratch folder, so
changing one character meant editing frames. docs/hero/ now holds every piece, in
three layers, each made from the one before:

  photos/  characters.png as an image model draws it, on white; cut_out.py turns
           it into one cut-out per theme (shadows kept on light, removed on dark,
           and only the white connected to the edge touched, so the eyes stay)
  html/    wide, mobile and small pages that compose the photos; renderLoop(t)
           draws the frame at second t, and opened in a browser each page plays
           its own loop
  build.mjs  photos -> frames (Chromium, 12 a second) -> docs/images/hero-*.gif,
           with the same palette and dither as before

A fourth page, html/social.html, builds docs/images/social-preview.png, the
repository's social preview, from the same photos.

The characters are new: the same four, colours inverted, each with a role it can
carry (a hard hat, sunglasses, a beret, a magnifying glass), all still angry. The
scenes, their words and their timing are unchanged.
2026-10-12 02:45:30 +02:00
Federico 96eee2ba8c Merge pull request #1445 from feder-cr/telegram-429-test
tests: the 429 poll test has a bot of its own, and the API test removes the channels it adds
2026-10-12 00:11:32 +02:00
feder-cr fc470314f2 api tests: the test of every Dot's channels removes the channels it added
It added two channels and left them running for the rest of the file and its teardown, the only channels there; on
one Windows run that teardown did not finish in 60 s (not reproduced in twelve runs under load here). A test stops
what it starts: the two channels are removed at its end, and the teardown is what it was before they came.
2026-10-12 00:01:03 +02:00
feder-cr 361b770285 channels tests: the 429 poll test has a bot of its own
It failed now and then in CI (four runs since 2026-10-08), always right after the test that polls TOKEN every few
milliseconds with a 409: a poll of that test still on its way when it ended was answered after the next test had
queued its 429 on the same token, as Telegram answers a request whose client has gone, so the stray poll took the
429 and the new poller connected without the error the test waits for. The 429 test now uses a token no other test
polls.
2026-10-11 23:45:40 +02:00
Federico 6a9cc8c113 Merge pull request #1444 from feder-cr/engine-path-and-shell
The engine runs nothing of the model's by name, reads every sentence of its browser server, and the health is its own context
golden-d9b4cfd820af
2026-10-11 23:31:39 +02:00
feder-cr fb3b667c01 ci: the vitest floors are what this branch's CI measured: 2209 on Linux and 2206 on Windows 2026-10-11 23:17:59 +02:00
feder-cr c5d48ac017 smoke: the browser smoke takes dot's PATH from the desktop's unit
GUEST_PATH was one PATH for the engine and the desktop; with the engine's unit on system directories only, the browser
smoke started the desktop and looked for invisible-playwright-mcp with a variable that no longer existed. It reads
dot's PATH from dot-desktop.service, as lib.sh reads the engine's and the daemon's from theirs.
2026-10-11 23:03:02 +02:00
feder-cr b82ca890ff e2e: step j asks the model to leave the browser open, which the step goes on to need
The step keeps research open "as it would be left at night" so that the server restart and the stop find it open, but
its task never said so; a model closed the browser after reading the title, as a tidy one may, and the step failed on
a choice the prompt left open. The task now asks to leave the browser open.
2026-10-11 22:58:37 +02:00
feder-cr 780fb3b46a engine: a browser opened is known by invisible-playwright-mcp's OPENED, captured like its other sentences
invisible-playwright-mcp 0.71.2 exports OPENED, the first sentence of browser_open's answer, which the engine matched
by its words ("browser is open") because the library had no constant for it. The lock moves to 0.71.2 (only that
package and its hashes), the capture records OPENED beside NOT_OPEN and GONE, and the engine reads all three from
invisible_playwright_mcp.json: no sentence of the server is written in the engine any more.
2026-10-11 22:43:34 +02:00
feder-cr 3d0da4e090 web: the health answer is a context of its own
The shell's context held the health with everything else, so its poll every 30 seconds gave the whole value a new
identity and re-rendered every user of the shell. useHealth reads it from its own context; only the rail's status,
the offline banner, the setup checklist, the key form and the host settings follow the poll.
2026-10-11 19:29:55 +02:00
feder-cr bd1b5866cc guest: the engine runs nothing by name from a directory the model writes
The engine's unit put dot's ~/.local/bin first on its PATH, only so the guest check could find
invisible-playwright-mcp by name; but the engine holds the OpenRouter key, and a program a library ran by name would
have been dot's, run as dotengine. The unit's PATH is now system directories only, as dot-agentd's is, and it names
the browser's MCP server by its path (INVISIBLE_DOTS_MCP_COMMAND), which the engine checks and starts as dot through
the relay. The smoke reads both from the unit.
2026-10-11 19:29:54 +02:00
Federico 1935f47e5a Merge pull request #1443 from feder-cr/fixes-batch
Fixes, removals and one owner per fact, from a read of the whole repository
golden-cb749f0e73bd
2026-10-11 19:02:23 +02:00
feder-cr 08d6786421 ci: the floors are what this branch's CI measured: vitest 2207 on Linux and 2204 on Windows, PostgreSQL 744 with 295 pg runs 2026-10-11 18:54:30 +02:00
feder-cr b50f09ed40 web tests: the fake control plane answers the two routes of every Dot, and the inbox says what one read cannot
The shell reads the failed tasks and the channels of every Dot with GET /api/tasks and GET /api/channels: the web
tests' control plane answers them as the real one does, and the inbox tests that faked one Dot's list failing now
fail the one read, which the inbox reports as such.
2026-10-11 18:39:45 +02:00
feder-cr 9ee1f32094 e2e: the journal check reads the journal as the Dot's user, without asking the model
A careful model refused to run a command that searches the system journal for sk-or-: it read like a probe for
secrets. The property the check proves is what the model's user can read, so it runs the same command through
dot-agentd's exec route, which the Dot's exec tool uses, as that user; the hash of the count and a nonce still proves
the whole journal was read.
2026-10-11 18:20:38 +02:00
feder-cr 373d837e29 e2e: step l checks that the interrupted command is not run again, not that the model runs nothing else
A careful model looks whether the marker is there after its command was interrupted (cat and pgrep): that is a
second exec call, but not the command again. The step counted every exec of the task and failed on it; it now
requires that no other call runs the interrupted command, and the marker check still proves the command never ran.
2026-10-11 18:12:04 +02:00
feder-cr 2e762e77d5 ci: the pytest floor is 2483, what the engine suite measures without the code no Dot runs
The engine's lean pass removed the MCP reconnect, the provider's reasoning options, the event sink, concurrent tool
batches, spilling results to files and the cron methods only tests used, each with its tests: the whole suite now
passes 2483 on Linux (measured in WSL, where CI runs it), down from 2553. The other floors only rise; they are set
from CI's measured counts once it has run.
2026-10-11 17:58:03 +02:00
feder-cr 528be98890 vm-manager, api: one logger and one pathExists, from shared
vm-manager's own Logger, silentLogger and stderrLogger repeated the shared ones; it now takes them from
packages/shared and logs as the rest of the process does (prefixedStderrLogger). vm-manager and the API's VM driver
each had their own exists(): one pathExists in shared replaces both.
2026-10-11 17:51:54 +02:00
feder-cr ac26cec1bf Merge branch 'guest-fixes' into fixes-batch 2026-10-11 17:50:35 +02:00
feder-cr 5316d43308 guest: dot-install only installs, the images are what their inputs say, and the Go copies of the protocol are held
- dot-install refused nothing apt reads as an action: "foo-" removed foo, as root. A name can no longer end in "-";
  a test runs the real script against stubs.
- The VM proxy's setup is runtime/vmproxy.py, staged on the runtime ISO: a bad value, a host that does not resolve and
  no default route each say so (exit 3, 4, 5) instead of all reading "not socks5"; the firewall still drops all. The
  smoke runs the file itself.
- dot-agentd is built one way everywhere (-trimpath -buildvcs=false -ldflags=-s -w): a commit no longer changes the
  binary and the runtime ISO. go.mod pins the toolchain, setup-go@v6 reads it, a repo test keeps every spelling equal.
- The golden image's apt packages come from an Ubuntu snapshot (pins.json apt_snapshot, in the inputs digest); the
  browser environment installs wheels only, with no cache left in the image.
- tests/repo/agentd-protocol.test.ts compares the Go routes and constants (and the engine's defaults) with
  packages/shared; a Linux test runs the real dot-agentd under the real GuestClient: proof, auth, exec, files, the
  agent proxy and its SSE.
- vm-manager: one assertImages, the VM bounds from CONFIG_BOUNDS, the pid file in its own module; one
  resolveImageVersion and publishImage for the golden, runtime and prebuilt images.
- Fixed-sleep tests wait for their signal; the docs no longer say the images are never published or speak of CD-ROMs.
2026-10-11 17:50:35 +02:00
feder-cr 5a65ada65d api, web: the shell reads the failed tasks and the channels of every Dot in one request each
The web's shell, on every page, read each Dot's task list and each Dot's channels, at load and again for every Dot on
each task.failed and channel event: 2N requests for N Dots, as its own comments said, because no route answered for
every Dot. GET /api/tasks?status=&finished_after= lists the tasks of every Dot that ended in a state after a time, and
GET /api/channels every channel with its Dot's id (never a credential); the SDK has endedTasks and everyChannel, and
the shell makes one request for each.
2026-10-11 17:49:03 +02:00
feder-cr b0514eac62 host: the pieces every package uses live in shared, so a package never depends on an app
Clock, Logger, silentLogger, errorMessage, ControlPlaneError, notFound and the stderr logger move from
apps/scheduler to packages/shared; packages/channels and apps/api import them from there, and channels keeps the
scheduler for its tests only. One abortableSleep replaces four copies (scheduler, CLI, SDK, WhatsApp); the one
shared had slept the whole time on a signal that had already aborted. The API reports its package's version, as the
CLI does, instead of a hard-coded 0.1.0.
2026-10-11 17:44:28 +02:00
feder-cr c8cb603941 Merge branch 'clients-fixes' into fixes-batch 2026-10-11 17:38:05 +02:00
feder-cr 825740351d Merge branch 'engine-lean' into fixes-batch 2026-10-11 17:38:05 +02:00
feder-cr 4e376c1927 clients: one wording for an approval everywhere, the CLI reads what it shows and can stop what it starts
- The tool and permission labels move from the web to packages/shared (labels.ts, still checked against
  permissions.py): Telegram, WhatsApp and the CLI ask "The Dot wants to run a command" with "Run commands (high
  risk)", as the web card does, and the CLI names the Dot.
- The event reader moves from the web to the SDK: `logs` reads only the newest events and follows on; `approvals
  --all` keeps the newest 500 and `status` reads the 10 it shows.
- New CLI commands: cancel, delete --yes, update with the config version (a concurrent save is a 409). The CLI's
  version comes from its package.
- The web's tool and approval steps are one model (steps.ts) for the chat, the task story and the activity; one
  outcome table and one duration format.
- The inbox keys answer only while the approvals list has the focus (WCAG 2.1.4); useResource and the Dot shell's
  context keep their identity across renders.
- setup --all no longer sends the person to a sign-in that does not exist; the MCP panel says the Dot can add a
  server; the e2e contract declares every CLI command run.ts runs, and a test holds it; apps/web's second vitest
  config is gone; docs follow.
2026-10-11 17:37:54 +02:00
feder-cr 1cbf4609d8 engine: a call written in the text runs nothing, a call runs once, and the code no Dot runs is gone
- The provider no longer turns <tool_call> text into calls (architecture 8.3 says such text runs nothing); a test
  holds it.
- The MCP client no longer retries a call after a connection error, which could run it twice on an HTTP server
  (section 8.7): it fails with its error and reports the server's end.
- Removed what the Dot never reaches (3342 lines, with their tests): the MCP reconnect and enabled_tools, the
  provider's reasoning tables and unused options, the event sink, concurrent tool batches, spilling results to files,
  the cron methods only tests used and the protected system jobs, and the identity state `archived`.
- The sentences of invisible-playwright-mcp the browser matches are captured from the pinned package into
  invisible_playwright_mcp.json, so a version bump that changes them fails a test.
- Fixed-sleep tests wait for their signal; architecture.md, UPSTREAM.md and main.py say what the code does.
2026-10-11 17:34:00 +02:00
feder-cr 8c6f3cb010 host: every task end is told, an approval adds one server once, and no state nobody sets
- A task whose task.created could not be delivered is failed with a task.failed event in the same transaction
  (the guest's type: a client follows one), so the web no longer shows it running for good; a commit point covers it.
- markDelivered marks the row and its run in one statement.
- add_mcp_server: the host uses up the approval that allowed the call (approvals.used_at, migration 0013), found in
  one statement with jsonb equality instead of the newest 500 and a hand-made canonical JSON; a call refused for its
  entry keeps its approval. The permission is typed.
- Dot DISABLED and VM IDLE, which nothing ever wrote, are gone from the states, the checks (migration 0014), the
  queries and the web.
- Unused exports removed (latestId, activeForDot, hasPending, isVmState, isDotState, API_TOKEN_ENV, MIN_TOKEN_LENGTH).
- One BackgroundWork for the scheduler and the lifecycle; syncGuest marks a Dot suspect itself when its push fails.
- The scheduler alone checks a message, a note and "always"; /api/stream resolves its Dot with historyDotId.
- The MCP secrets of a push are read in one query; EventLog.stream keeps one id instead of every replayed one.
- The event schemas and the declared event types are held equal by the typecheck, so the parsers need no cast.
- start.test waits for the three requests instead of 200 ms; the pre-push hook says what only CI runs; docs follow.
2026-10-11 17:32:20 +02:00
feder-cr 61e6dc21b4 bench: the tasks are written by their generators before every run, never stored
.gitignore's *.log kept the data of top-ips (access.log) and log-pipeline (app.log) out of the repository, so a clone
had both tasks without their input; the stored output could also drift from author.py and author_long.py, and did
(hand edits, zlib's bytes). run.sh now writes tests/bench/tasks and tasks-long afresh before each run.
2026-10-11 17:32:09 +02:00
Federico 21500338a3 Merge pull request #1442 from feder-cr/dot-mcp-add
A Dot adds an MCP server itself, with the person's approval
2026-10-11 11:16:27 +02:00
feder-cr 0060eee57a ci: the floors are what this branch's CI measured with add_mcp_server's tests: vitest 2148 on Linux and 2151 on Windows, PostgreSQL 738 with 293 pg runs, pytest 2553 2026-10-11 10:59:24 +02:00
feder-cr 0c51c42c60 tests: the real-VM run has the Dot add an MCP server itself and the person approve it; the smoke offers add_mcp_server
Step m queues a task that adds `clock` with add_mcp_server: the approval is under mcp.servers.add with the entry, and
once approved the server is in the config, with its permission left to ask, and connected. The engine smoke's tool
list for a Dot granted everything has add_mcp_server, and its check that a removed server's tools are gone looks at
the servers' tool names.
2026-10-11 10:44:27 +02:00
feder-cr e03edb06d4 docs: a Dot adds an MCP server itself, with the person's approval, and how the agent-hosts compare
Sections 5.4, 7 and 8.3, the guide, and the wiki article, with what NanoClaw, Hermes, OpenClaw, nanobot and Goose let
an agent do, from their source.
2026-10-11 10:44:27 +02:00
feder-cr 4f4cbca2f4 web: the approval of an MCP server the Dot adds shows what it runs, the secrets you will set and why
add_mcp_server's card lists the name, the command line (or the URL), the environment and headers written in it,
the secrets the person will set and the reason; the activity says what was added, or why it was not. "Add MCP
servers" is a row of the MCP servers group of the permissions.
2026-10-11 10:44:27 +02:00
feder-cr bd3cf77274 host: a server the Dot adds is written only when allowed or approved for exactly these arguments
On `mcp.server.added` the host checks, in the transaction that stores it: mcp.servers.add allows it, or an approved
approval of add_mcp_server has the same arguments (compared key by key, in any order); the name is free; the entry
passes the config's schema. Then it writes the server with its own permission left to ask, logs `dot.updated`, and
pushes the config; otherwise it logs `mcp.server.refused` with why.
2026-10-11 10:44:27 +02:00
feder-cr a33c706731 engine: add_mcp_server, a Dot adds an MCP server to its own config, as NanoClaw's agents do
The tool checks at once what it can (a name that is free in the stored config, a command or a URL, no user or
password in the URL) and writes `mcp.server.added` with the call's arguments; the host decides again and writes it.
Its permission is the table's mcp.servers.add, so the gate asks the person with the whole entry. Its description
says how to install a server's program first (uvx, `uv tool install git+https://...`, npx after Node.js) and that a
key is named in secrets, never written. The config check no longer reads mcp.servers.add as the permission of a
server named "servers.add", and the prompt says the Dot adds a server this way.
2026-10-11 10:44:27 +02:00
feder-cr 7067f0357b shared: the permission to add an MCP server, mcp.servers.add, and the events of a server the Dot adds
mcp.servers.add asks by default and is high risk; a server's name holds no `.`, so it is never a server's own
permission. `mcp.server.added {arguments}` is the guest's report of an add_mcp_server call that ran, and
`mcp.server.refused {name, reason}` the host's when it does not write the server. parseMcpServerEntry is the one
check of a server's entry, the config's own schema.
2026-10-11 10:44:26 +02:00
Federico 2c803ef5b5 Merge pull request #1441 from feder-cr/dot-mcp-servers
A Dot uses the MCP servers its person declares
2026-10-11 05:49:34 +02:00
feder-cr 72cd7d4df6 ci: the floors are what this branch's CI measured with the MCP servers' tests: vitest 2143 on Linux and 2146 on Windows, PostgreSQL 732 with 290 pg runs, pytest 2535, Playwright 88 2026-10-11 05:40:51 +02:00