568 Commits
Author SHA1 Message Date
feder-cr 33d82c57d7 Merge remote-tracking branch 'origin/main' into dot-mcp-servers 2026-10-11 05:33:26 +02:00
Federico 440f6837d7 Merge pull request #1440 from feder-cr/browser-host-mcp
The page tools are invisible-playwright-mcp's own, and its instructions are in the prompt
golden-23253ce2ff8f
2026-10-11 05:33:21 +02:00
feder-cr 7e103cc636 Merge remote-tracking branch 'origin/browser-host-mcp' into dot-mcp-servers 2026-10-11 05:29:09 +02:00
feder-cr c5c8c07e66 ci: the pytest floor is 2507, what this branch's CI measured: the page tools are the server's now, so the 88 tests of the engine's own (its 16-tool fixture, browser_scroll, browser_screenshot, the browser argument) gave way to 84 that hold them to the server's capture, with the cases of browser_evaluate, read_html, take_screenshot and upload_files 2026-10-11 05:27:15 +02:00
feder-cr 803d03b0ac wiki: the 30-second Browser.newPage bound is reported as invisible_playwright#321 2026-10-11 05:21:22 +02:00
feder-cr 441c96d4e0 Merge remote-tracking branch 'origin/browser-host-mcp' into dot-mcp-servers 2026-10-11 05:20:27 +02:00
feder-cr 7378d76e19 Merge remote-tracking branch 'origin/main' into browser-host-mcp 2026-10-11 05:18:37 +02:00
Federico ba6227765c Merge pull request #1439 from feder-cr/browser-audit
Browser audit: back, forward and reload removed, and the calls that lead nowhere by cause
2026-10-11 05:18:23 +02:00
feder-cr a6c727a5f1 web test: the Tasks page waits for a new task by the list's length, not by a page-wide text query over two hundred cards that used up waitFor's second on a loaded machine 2026-10-11 05:14:52 +02:00
feder-cr cd7e36f190 ci: the pytest floor is 2511, what this branch's CI measured: the 33 tests that left are the cases of browser_back, browser_forward and browser_reload (24 of the approval matrix, 9 of the page tools), gone with the tools 2026-10-11 05:12:15 +02:00
feder-cr a4472f0394 wiki: MCP servers a person declares, how other hosts do it, and a live Dot that installed a server's program and used it 2026-10-11 04:47:39 +02:00
feder-cr a0c5aefb0e guide: the acceptance run passes its 16 steps, the MCP servers' step with them 2026-10-11 04:43:30 +02:00
feder-cr f542349ce7 smoke: the MCP server's secret compared by hash, per server process, never printed 2026-10-11 04:39:08 +02:00
feder-cr 46ba8330c8 smoke: the MCP server's user, environment and command lines in three checks, each saying what it found 2026-10-11 04:33:54 +02:00
feder-cr 97fea69aee smoke and e2e: look for the MCP secret on the command lines with a pattern that does not match the grep's own 2026-10-11 04:28:31 +02:00
feder-cr 4d5e59fd0d mcp servers: a start bound of their own, startup_timeout_s, and one that timed out holds up no turn after
A server that never answers would be started again at every turn, and every message would wait out the bound for
it. As Codex has `startup_timeout_sec` beside `tool_timeout_sec`, a server's entry has `startup_timeout_s` (1 to 600,
60 by default, MCP_STARTUP_TIMEOUT_BOUNDS) beside `timeout_s`; one that does not start within it is not started again
until its entry or its secrets change, and its error says so. The fork's client tests fake the SDK's stdio_client
and connect_mcp_servers with their real parameters (errlog, the failures and errlogs sinks).
2026-10-11 04:23:32 +02:00
feder-cr 6875851a43 smoke: the MCP checks read the server's environment as dot, give the missing server a permission so the prompt says why, and group the jq test 2026-10-11 04:10:19 +02:00
feder-cr c0cbb552db web e2e: an MCP server added in the settings through the real host, its secret pushed to the guest and never on the page 2026-10-11 04:09:31 +02:00
feder-cr e6ebfc79f8 tests: the MCP servers in the engine smoke and in the real-VM run, and the e2e contract that knows them
The engine smoke declares the engine's fake MCP server and a program that is not installed: through the image's
relay the first connects as dot with its secret in its environment and on no command line, its tool runs under its
permission and its instructions reach the prompt, the second fails saying what the relay found, and a config without
them stops the server. The real-VM run's new step m runs mcp-server-time through uvx: its tool asks, is approved
always and runs as dot; a second server waits for its secret, set through the CLI. Steps n and p look for that secret
as for the key.
2026-10-11 04:02:57 +02:00
feder-cr 2afd73b0fe docs: MCP servers in the architecture and the guide; no longer out of scope
Section 7 declares them and their permission, 8.3 says where they run, when they start and what the model gets, 4.3
how their secrets travel, 5.3 and 9.6 the routes, 8.8 the engine's manager. The guide has a section on them, the two
commands, and the README one row.
2026-10-11 04:02:57 +02:00
feder-cr 71bdf9db85 web: an MCP servers panel in a Dot's settings, and each server's permission among the others
The panel lists each declared server with where it is on the computer and why one is not connected, adds, edits and
removes one in the draft like any setting, and sets or clears its secrets write-only once the config that names them
is saved. Each server is a row of Permissions and tools, asked by default and high risk; a preset leaves the
servers' permissions alone, the review lists a server's change by what it runs, a change put on top of a newer config
keeps both sides' servers, and an approval of a server's tool is the kind to stop and think about.
2026-10-11 04:02:57 +02:00
feder-cr a60ee568e3 host: MCP secrets are set write-only, pushed with the key, and none outlives the config that names it
A secret is stored encrypted under the Dot's scope as mcp/<server>/<NAME>, set and cleared through
PUT and DELETE /api/dots/:id/mcp-secrets/:server/:name (the SDK, `invisible-dots secret mcp`), listed without its
value, and pushed with the key on every READY and agent.started. A config that drops a server or a name drops its
value in the same transaction. `GET /api/dots/:id/tools` answers the engine's whole answer, the servers' states with
the tools, and `invisible-dots mcp <dot>` shows them. Always allowing the permission of a server the config no
longer declares is refused (409 permission_gone).
2026-10-11 04:02:56 +02:00
feder-cr 2b354e2a29 engine: the MCP servers a person declares, started as dot through the relay, their tools behind their permission
nanobot/dots/mcp_servers.py keeps one MCPProvider per declared server, as the BrowserManager keeps one per identity.
A command runs through dot-agentd relay as dot, its secrets by the relay's environment (--env-from); a URL is reached
with its secrets as headers. A server starts when declared, restarts alone when its entry or a secret changes, and
one that failed starts again when the next turn begins, which waits for those starting (60 s each at most). Its
tools join the registry as mcp_<server>_<tool> under mcp.<server>; the prompt carries each server's instructions, or
why it is not connected, with what its stderr said and its secrets masked. The fork's client keeps a server's
initialize instructions, says why a connection failed and takes a stdio server's stderr (UPSTREAM.md). A config
stored by a release without servers declares none.
2026-10-11 04:02:56 +02:00
feder-cr 05bd036027 shared: a Dot's config declares MCP servers, each with its own permission and secrets kept apart
`mcp_servers` takes the entries Claude Code, Codex and nanobot take: a command with its args and env, or a URL with
its headers, plus `secrets`, the names of the variables or headers whose values are set apart from the config, and
`timeout_s`. Each server has a permission, `mcp.<server>`, that asks by default; one of a server the config does not
declare is refused, and denied. The engine's tool table, `approval.requested` and `GET /tools` name such a
permission, and `GET /tools` says where each server is (connecting, connected, failed and why). `POST /secrets`
carries the servers' secrets with the key; a secret's value is one rule, MCP_SECRET_PATTERN, as the key's is. Servers
are listed by name, since the database keeps a config as jsonb.
2026-10-11 04:02:56 +02:00
feder-cr 6791bd1e34 wiki: browser_evaluate measured on 0.71.1, on the same four tasks as on 0.71.0 2026-10-11 03:14:26 +02:00
feder-cr 5c9d964423 image: invisible-playwright-mcp 0.71.1, whose browser_evaluate says it comes last and names the tool for each read
In the four verification tasks on 0.71.0 browser_evaluate was the call the model made most (14 of 72), mostly to
read text, a heading or a link a reading tool returns. 0.71.1 changes only that tool's description and the rung of
the server's instructions that names it: last, and which tool reads text, markup or a control instead.
mcp-requirements.lock is regenerated with the command in its header, and nanobot/dots/invisible_playwright_mcp.json
captured again from the package installed from PyPI, so the engine offers the published wording.
2026-10-11 03:03:43 +02:00
feder-cr 26ed9228e7 smoke: the engine smoke's browser checks for a server started in host mode: no browser argument on any call, the reading tools the server has, and browser_take_screenshot under its own name 2026-10-10 23:07:36 +02:00
feder-cr 540f21b6c5 image: the engine's .json data travels with its source, so the browser server's capture reaches the runtime disk
The engine reads nanobot/dots/invisible_playwright_mcp.json at start, and the runtime disk, the wheel and the smoke's
staging took only the .py files and the .md templates: the browser smoke's engine stopped at import with no such
file. The three readers of what the package holds (pyproject's include list, runtime.ts, prepare-engine.sh) now
take the package's .json too, and runtime.test.ts holds a .json of the package on the disk and the capture among
the real engine's files.
2026-10-10 23:07:35 +02:00
feder-cr fc07373f9d image: invisible-playwright-mcp 0.71.0 and invisible-playwright 0.32.0, the versions that serve a host its page rules and main alone, with the engine's capture taken again from the published package
0.71.0 is the release the engine's page tools were built for (INVISIBLE_MCP_HOST_MANAGED, the page rules, a read
that resolves its selector through the engine); it requires invisible-playwright 0.32.0, which brings
invisible-core 39.35.0. mcp-requirements.lock is regenerated with the command in its header for both, and
nanobot/dots/invisible_playwright_mcp.json is captured again from the package installed from PyPI: only its
version changed from the capture taken from the repository, so the published instructions and tools are the ones
the engine was tested against, and the test that holds the capture to the lock passes. golden.test.ts builds from
the lock and names the new pins.
2026-10-10 23:07:35 +02:00
feder-cr 1d6827065b wiki: the browser server's own tools and words, how other hosts carry a server's tools and instructions, the host mode a Dot needs, and what it costs
The audit article said the consent rule was in the skill and that browser_read_text takes CSS; both moved to the
server (its page rules, and a read that resolves its selector through the engine), and the article says so and
links the new one.
2026-10-10 23:07:35 +02:00
feder-cr f9a21766ad engine: the page tools are invisible-playwright-mcp's own, offered as an MCP host offers a server's tools, and its instructions are in the prompt; the skill keeps what only a Dot knows, the identities
The engine rewrote every page tool of the browser server under names and descriptions of its own, and its skill
copied the server's instructions (the order to try things in, never act by script, say when a task is impossible),
then gained three rules of its own from the audit of the browser calls, all true of any page. Two copies of a rule
drift, and the copy the model read was not the one the server's authors keep. Claude Code and opencode offer a
server's tools with the server's descriptions and put its instructions in the system prompt; nanobot, the engine's
base, offers the descriptions and drops the instructions.

The server could not be passed as it was: it serves one person and a helper, `main` and `support`, and tells its
model to open them with browser_open, which a Dot's model does not have. invisible-playwright-mcp 0.71.0 reads
INVISIBLE_MCP_HOST_MANAGED=1, under which it serves `main` alone, no tool takes `browser`, and its instructions are
the page rules. The BrowserManager starts it that way (BROWSER_ENV["HOST_MANAGED"], in the shared table too) and
stops adding `browser: "main"` to every call; it still opens and closes the browser with browser_open and
browser_close, which a first launch (the engine's download, answered in steps) and a close (Firefox writing its
profile before the client ends the process) need.

The page tools are now built from nanobot/dots/invisible_playwright_mcp.json, the server's instructions and
tools/list as guest/image-builder/builder/capture-mcp-interface.py captures them from the real server started that
way: the server's name, description and input schema (normalized as nanobot's MCP client does), read-only from its
annotation, plus the identity_id of the open identity, described once on that argument. The permission table says
which the model is offered: browser_read_html, browser_evaluate and browser_upload_files come in, and
browser_take_screenshot under its own name; browser_open, browser_close, browser_list and browser_status are the
identity tools' and browser_watch the UI's frame. browser_scroll, which the engine invented over PageDown, goes, and
so does the 214-character cap on browser_type: the server answers within 45 s and goes on typing in the background.
An image the server answers with is shown, so browser_click_at returns the page after the click. The engine still
refuses a URL that is not http or https before it reaches the server. The system prompt carries the server's
instructions whenever a page tool is offered. The capture replaces tests/fixtures/mcp-tools-0.70.2.json: the fake
MCP server and the smoke serve the same file.

The server's click_at takes `number` coordinates, so store.canonical_arguments writes a whole number one way: 10
and 10.0 are one call for an approval, 10.5 stays itself (the test that forbade a number type said to do exactly
this before adding one). A null for an optional page argument is now refused by the server's schema with what it
should be, as nanobot refuses it for any MCP tool, instead of being dropped by the engine.

Measured with tiktoken, every permission allowed: the page tools' definitions went from 1632 tokens (10 tools) to
2885 (12), and the instructions add 765, about 2000 tokens more on each request that offers the browser. The fake
model of the tests published a 32000-token window, under the engine's cautious estimate of a turn with every
permission (about 34000); it now publishes 128000, the window of the smaller models Dots run on.

The capture is of the server at 0.71.0 from its repository; the lock still pins 0.70.2, so the test that holds the
capture to the lock fails until 0.71.0 is published and the lock moves to it, with the capture taken again from the
published package.
2026-10-10 23:07:35 +02:00
feder-cr 7788fbc068 smoke: GET /tools is checked against the permission table's own count, which the removal of back, forward and reload made 27, not the 30 written by hand 2026-10-10 23:04:53 +02:00
feder-cr 669a4f0350 wiki: browser calls that lead nowhere, the audit of 45 browser tasks of real Dots by cause, with what was changed for each and what was not
90 of 773 page calls led nowhere and took 961 of the 2823 seconds spent on pages. Addresses from memory are 58 of
them (27 of the 54 navigations dropped unread answer 404 today, against 1 of 40 that were read), which the
finding-a-page rule of PR #1438 is for, and its measure is written here too. Then two timeouts of the library (the
first page after a launch fails at the 30 seconds invisible_playwright gives Browser.newPage; pages over 45
seconds), reads that fail once right after a navigation, clicks under Google's consent dialog, and back, forward
and reload, which never worked. The snapshot with the navigation is measured and not adopted: a snapshot follows 7%
of the navigations that loaded, a text read 72%.
2026-10-10 18:23:12 +02:00
feder-cr b47df8936b engine: browser_back, browser_forward and browser_reload are gone, since a key pressed on the page never moves the browser's history: back and forward failed every time ("unknown key: 'Left'") and reload answered that it reloaded a page it did not; browser_navigate does both, and the skill says so
An audit of the 45 tasks of the saved runs that drove a page (773 page calls) counted browser_back three times and
three failures, and browser_reload five times and five "reloaded the page". The three tools pressed Alt+Left,
Alt+Right and F5 through browser_press_key. Run on invisible_playwright itself: Alt+Left and Alt+Right are refused
because the library's key names are ArrowLeft and ArrowRight; Alt+ArrowLeft and Alt+ArrowRight are pressed and the
page stays where it is; F5 is pressed and the page is not loaded again (a value set on the window survived it). A
key the server presses reaches the page, never the browser's own shortcuts. PageDown does scroll (0 to 913 pixels),
so browser_scroll stays, and a navigation to the page's own address loads it again (HTTP 200, the value gone).

So the three tools are removed from PAGE_TOOLS, the permission table, the web client's labels, the smoke's list of
offered tools and the architecture's table; an old event naming one is shown as the model called it. The skill says
that going back is a navigation to the address the page came from, and loading a page again a navigation to its own
address.

The same audit gave two more lines to the skill. browser_read_text takes a CSS selector, and its argument now says
so: the server reads with document.querySelector, so a snapshot selector in Playwright's syntax (:nth-match(...),
text=...) fails there, as one of the ten failed reads did. And a cookie or consent dialog is answered first: four
clicks on a Google result waited their 15 seconds under Google's dialog and failed before the model answered it
(66 seconds). On the same Google task, run once with the old skill and once with the new one on a fresh Dot
(z-ai/glm-5.3-flash), both answered the dialog before clicking, so that run shows no difference; the line is for
the four clicks of the runs.

wire_shapes.json is written again for the tool list and the skill's content.
2026-10-10 18:23:12 +02:00
Federico 37d58b616b Merge pull request #1438 from feder-cr/browser-finding
skill: the browser skill says how a page is found, never an address from memory
2026-10-10 14:46:58 +02:00
feder-cr 6815e3a253 skill: the browser skill says how a page is found: a link seen in a snapshot or a search on Brave, never an address from memory, and a 404 sends the Dot back to a page that loaded
The skill said how to act on a page and how to keep identities, and nothing about how to reach a page. A model
left alone fills that gap from memory: it writes an address it remembers, the site answers 404, it writes
another, and a task's steps go on guesses. The new section says where an address comes from (a link in
browser_snapshot, the site's own menu or search, or a search engine's results), that the status browser_navigate
answers is read before the page is trusted (the tool's own words), that after a 404 the next address comes from a
page that loaded or from a search, and that an address from memory is for a site's home page at most, once. The
skill's description names finding a page and searching the web, so a research task is one it covers.

Brave is the search engine named because it is the one that answered the Dot's own Firefox with real results: a
Dot asked to open the three engines reported Brave 200 with usable results, DuckDuckGo's html version 200 (its
result links wrapped in a redirect), and Bing 200 with an empty results area, then results unrelated to the query.
DuckDuckGo is the fallback the skill names.

Measured on one Dot (z-ai/glm-5.3-flash) asked for the exact URL and first heading of three documentation pages
whose addresses are not guessable (Tailscale SSH, Hetzner Cloud firewalls, Fly.io autostop): with the old skill
28 calls, 8 navigations, 2 addresses from memory that answered 404 before it recovered from the sites' indexes,
$0.019; with the new one 29 calls, 7 navigations, 0 addresses from memory (each page came from a Brave result or
a link of the site), $0.037, the difference being the snapshots of the result pages. Both answered correctly.

wire_shapes.json is written again for the new description and content; architecture.md's line on the skill says
what it now covers.
2026-10-10 14:36:50 +02:00
Federico 80f4709da6 Merge pull request #1437 from feder-cr/memory-incremental
engine: a memory pass sends only the messages it has not taken yet
2026-10-10 06:32:48 +02:00
feder-cr 1ece62d3e3 engine: a memory pass sends the summary model only the messages it has not taken yet: a conversation file is written again whole after every turn but only grows at its end, so a long day's chat was read again in full at every pass, at a cost that grew through the day; docs and the event's description said it read only what changed, which is now so 2026-10-10 04:38:59 +02:00
Federico e00898b9f0 Merge pull request #1436 from feder-cr/bench-lock
A LongMemEval run holds its directory; the engine drops the provider-side conversation state it never used
2026-10-10 04:38:55 +02:00
feder-cr 6a324467e7 tests: the exec relay tests no longer bet on a login shell printing within 200 to 1000 ms: a session is read until its command exits, and a command that ends is given the longest yield, which it answers within as soon as it ends (two full runs on an overloaded workstation failed them) 2026-10-10 03:41:10 +02:00
feder-cr a7d48bc94c ci: the pytest floor is 2540: the 22 tests removed with the provider-side conversation state tested only that code (test_conversation_state.py's 11, and 11 runner, consolidator and retry cases), none a live path 2026-10-10 03:07:47 +02:00
feder-cr 7406d05d0c engine: the provider-side conversation state and provider-native compaction are removed: no provider of a Dot keeps state or compacts on its side (OpenAICompatProvider overrides none of it), so every one of its paths was a no-op; request_pressure, which nothing called, goes with them 2026-10-10 03:07:47 +02:00
feder-cr 02a17d5b36 bench: a LongMemEval run or judge holds its directory, so a second one on it stops at once instead of answering and grading the same questions twice 2026-10-10 02:05:02 +02:00
Federico 71e9408975 Merge pull request #1435 from feder-cr/memory
A Dot remembers: conversations as files, MEMORY.md in every prompt, a background memory pass
article-views
2026-10-10 01:55:21 +02:00
feder-cr 9e42bfd8b3 ci: the test floors are what this branch's CI run measured: vitest 2110 on Linux and 2113 on Windows, pytest 2562, PostgreSQL 717 with 283 pg runs 2026-10-10 01:07:47 +02:00
feder-cr 5e163e969a smoke: the summary role is tried with windows a Dot can work in (24000 tokens: its prompt and tools alone outgrew 2880, so every turn failed while the old checks still passed), and every long message must be answered by the model, not with a failure; docs: the product run's numbers (90.4%, $0.025 a pass) 2026-10-10 00:06:29 +02:00
feder-cr e8e3d71d15 engine: the conversation files and MEMORY.md hold no secret the events mask: a decision on a call is its public line (call and target, never the arguments), and every proxy the Dot's identities ever had and the OpenRouter key are masked in any text, since /home/dot is what the host's file routes serve (the smokes found a proxy password there) 2026-10-09 19:41:01 +02:00
feder-cr 3820581c6a docs: the wiki is written in docs/wiki and published on every push to main by publish-wiki, with invisible_playwright_mcp's converter; its first articles are the studies behind a Dot's memory, its token sizing, its compaction and its benchmarks 2026-10-09 19:19:59 +02:00
feder-cr ad4fbf3306 bench: LongMemEval waits for the Dot's own memory pass before each question; the memory-method study is recorded in the README and its adapters removed (kept in de3d9a1b) 2026-10-09 19:15:49 +02:00
feder-cr f0227ede4d engine: once the Dot has been quiet for 5 minutes, a memory pass has the summary model rewrite MEMORY.md from the conversation files that changed, in one request with no tools; its spend is capped like a task's and reported by memory.updated, which the host counts and the web shows (on LongMemEval: preferences 65 to 83%, updates 78 to 94%) 2026-10-09 19:15:49 +02:00