158 Commits
Author SHA1 Message Date
Federico EliaandClaude Fable 5.1 f8d7093c5a Job database: rebuilt every five hours instead of once a day
The biggest source, jobhive, publishes around 15:00 UTC and the new-grad
lists commit every hour; the 04:23 build read them up to a day late. Cron
23 */5 * * *, off the top of the hour; timeout 200 minutes so a hung run
does not take the next slot; six dated releases kept, about thirty hours.
Texts say five hours, and the search log gives the build's hour.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-07 03:34:57 +01:00
feder-cr 69d93438cd The repository's new name 2026-11-05 14:48:34 +01:00
feder-cr 1c054dc6d3 Data sources: list all 24, their licences and how to remove one 2026-11-05 04:56:20 +01:00
feder-cr f7ef2564bb First run: a refused key, quitting, 80 columns, plain errors, BOM files, install and CI
A key that OrcaRouter refuses is forgotten when it came from the key file, and asked for again; a key from the
environment is named by its variable and not forgotten. Ctrl+C leaves at every prompt, and leaving while a prompt
is open no longer raises NoMatches from the heading. Log lines wrap to the whole width, and Quit stays on the
footer at 80 columns. Database and model failures say what happened in plain words, with the detail after them.
Files saved with a byte-order mark are read. The README gives the install commands checked in new venvs, the
index size from the manifest, and how to change the key. CI runs the unit job on Python 3.11 and 3.12 on Ubuntu,
Windows and macOS; gate still needs all of it. The classifiers name the three systems.
2026-11-04 22:52:08 +01:00
feder-cr d2f93b5850 Texts: the one sentence about AIHawk everywhere, the README for a one-minute read, and true statements about models, mail and the database 2026-11-04 22:24:51 +01:00
feder-cr 9094020f7f Open-source basics: CONTRIBUTING, the plugin guide, SECURITY, issue and pull request templates, CHANGELOG 2026-11-04 22:15:46 +01:00
feder-cr da5517bd4f aihawk: matching from a weighted plan graph, more sites, and the browser as its server says
The search plan is a graph of nodes and weighted forms with conditional arcs, after the owner's harrier engine, kept in
aihawk.yaml so it can be tuned. Search scores titles in Python from it, by rarity among the postings of the place, and a
town's name no longer matches where the country column says otherwise.

Judging asks closed questions and computes the score from the answers. Plugins now describe how each site's pages are
built; SmartRecruiters, Teamtailor, JazzHR, BambooHR and Personio are new, from rendered pages.

The browser window is visible. The model gets the server's own instructions, its rungs of coordinates and screenshots,
and any tool it marks read-only; a click at a point goes through the same check as a click on a selector. The form rules
now send certifications and unstated facts back as questions, after a real test ticked them. A daily run tries the
newest invisible-playwright-mcp.
2026-11-04 16:53:34 +01:00
feder-cr 8cead59b7b AIHawk: find the jobs that fit your CV and apply to them
This repository is AIHawk again. Dots moved to feder-cr/invisible_dots with its history.

Every day a job merges the public job databases into one (pipeline/), one row per posting, and publishes it as a
release. `aihawk scan` queries it in place, so a user scrapes nothing and downloads only what the search reads. `score`
judges jobs against the CV through OrcaRouter, `apply` tailors the CV and fills the form through invisible-playwright-mcp.

The model never sends an application: it fills the form and names the submit control, and AIHawk clicks it after the
person's yes, or by itself with --auto-submit. While the model works, a click on what would send the form and the Enter
key are refused. What the CV and the answers file do not say comes back as a question, never as a guess.
2026-11-04 11:47:28 +01: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
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 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 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 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 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 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 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
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 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 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 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 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 bcf03a0eb2 Community files: how to contribute, the code of conduct and the security policy, and issue and PR templates that ask for what a report of this project needs (doctor, logs, commit, host) 2026-10-07 21:33:52 +02:00
feder-cr 48299b2772 web: a Dot's page open while its delete finishes says the Dot does not exist
The header reloaded the Dot on dot.updated, computer and agent events, not on dot.deleted: a delete finishes after its request (the computer stops first, longer with a task running), and the page open on the Dot stayed on "Computer: Deleting" with its tabs until reloaded by hand (seen through the real product). dot.deleted reloads it, and the 404 draws "This Dot does not exist". vitest floors +1.
2026-10-07 09:43:57 +02:00
feder-cr a10343ff70 vm-manager: a seed that changes gets an instance-id cloud-init has never seen, so a cleared VM proxy is cleared
The instance-id was the digest of the seed's content, and cloud-init runs write_files once per id it has seen. A VM proxy set and then cleared gave the first boot's content, so the first boot's id: cloud-init skipped write_files, config.json kept the proxy, and the Dot stayed behind a proxy it no longer had (seen through the real product: after the proxy was removed and the computer stopped and started, every message answered "Connection error"). A seed equal to the last one written keeps its id, as before; any other gets the content's digest and a digest of the id before it. The last one is recorded in seed-instance.json beside seed.iso. vitest floors +1.
2026-10-07 09:16:05 +02:00
feder-cr ca781d96b2 engine: a key the provider refuses is said to be the key
With a wrong OpenRouter key set in the host's Settings the chat said "I could not answer: Error: User not found.", OpenRouter's words for it, which do not say that the key is what is wrong. A 401 now reads "Error: the API key was refused (401): User not found." pytest floor 2222 to 2223.
2026-10-07 08:51:09 +02:00
feder-cr c75bca3ef2 cli: the bundle keeps class names, so Telegram works from the installed command
In the bundle esbuild renamed AbortSignal to AbortSignal2, and node-fetch, which grammY uses on Node, recognizes a signal by its constructor's name: every Telegram call failed there as "Network request for 'getMe' failed!". Connecting a bot answered "could not reach Telegram (getMe)" for any token, and a connected one could neither poll nor send. The suites import the sources, so none saw it; it was found through the real product (a wrong token said Telegram was unreachable while Telegram answered 401). keepNames is set, the options live in scripts/bundle-options.mjs, and tests/repo/cli-bundle.test.ts bundles a real grammY call with them against the fake Bot API (without keepNames it fails as the product did). vitest floors +1.
2026-10-07 08:37:54 +02:00
feder-cr 1194e0a576 web: an address with nothing behind it has a page in the app's look, with the way back to Home
Next's default 404 drew its own black or white page in the system font, with no link: a person who followed an old link, mistyped, or opened a Dot that was deleted was left there. app/not-found.tsx says there is nothing at this address, why that can be, and links to Home. Seen through the real product in every theme and width; shell.spec.ts checks the 404, the font and the link. Playwright floor 86 to 87.
2026-10-07 08:21:15 +02:00
feder-cr d8bb8901f6 engine: a context window too small for the request is said in words, with what to change
With limits.context_tokens at 4000, the schema's floor, a Dot of the Balanced preset could answer nothing: its prompt and tools alone are 6371 tokens and the window less the answer's room is nothing. Each message got "I could not answer: Model input exceeds the local context budget for chat: 6371/0 via tiktoken" (seen through the web client). The turn now fails with the request's size, the window and what it leaves, and says to raise Context tokens in the Dot's settings. Whether the floor itself should rise is the owner's decision; the smoke's summary check relies on a small window. pytest floor 2221 to 2222.
2026-10-07 08:12:44 +02:00
feder-cr ac5e93ca93 engine: a provider's error reaches the chat as its message, not as a Python dict
The OpenAI client hands failure_text the body it already parsed, a dict, and str() of it reached the person as "I could not answer: Error: {'message': 'no-such-vendor/no-such-model-9 is not a valid model ID', 'code': 400}" (seen through the web client with a model id that does not exist). A parsed body is now read for its message, {message} or {error: {message}}, and shown as JSON when it has none. pytest floor 2217 to 2221 for the four tests added.
2026-10-07 07:59:44 +02:00
feder-cr b69184e1e2 tests: the vitest floors are what the pre-push gate measured, 2099 on Windows and 2096 on Linux
The floors lowered with the Memory tab, the browser's view and the goal were worked out from the lines of vitest list, which shows an it.each once: the deleted files (memory, memory-view, notes, automations, identity, browser-activity, login, session) took more runs than that. The gate's run on Windows: 2099 passed, 0 failed, 0 skipped, in 149 files. Linux stays three below, the generated PowerShell tests it skips.
2026-10-07 04:03:24 +02:00
feder-cr bb37a3a105 settings: a Dot has no goal and no browser settings; it is asked for what it does, and it manages its identities itself
The goal is gone from the schema, the engine's protocol and prompt, the header, the cards, the search, the
forms and the CLI sample; the first messages offered no longer draw on it. Migration 0010 takes it out of
stored configs and moves their version.

The Browser panel is gone with what it set: managed_by_dot, max_identities and max_open. The engine keeps
its own limits (3 open, 20 in all, DEFAULT_MAX_OPEN and DEFAULT_MAX_IDENTITIES in main.py), fixed when the
manager is made, and offers every identity tool its permission allows. Migration 0011 takes the section out
of stored configs. The limit message says "the most it may keep" instead of "the most its configuration
allows". The real e2e's eviction step launches a fourth identity instead of lowering max_open to 2.

Test floors are lowered by what went with the features: pytest 2231 to 2217 (the set_limits, managed_by_dot
and goal tests), vitest 2125 to 2124 on Windows and 2122 to 2121 on Linux (three first-message tests about the goal
became one and the max_open rule's test went; the two migration tests came).
2026-10-07 03:21:44 +02:00
feder-cr fefd892aef skills: the Dot learns how to do things as Claude Code does, and the browser has no view of its own
A skill is a folder with a SKILL.md, the Agent Skills layout Claude Code and upstream nanobot read: a frontmatter
with its name and a one-line description of when it applies, then the steps. Built-in skills ship beside the
engine's package (invisible_engine_dots/skills/, on the runtime disk at /opt/invisible-dots/engine/skills/); the Dot
writes its own under /home/dot/skills with its file tools, and one of its own replaces a built-in one of the same
name. Each turn the prompt names every skill with its description and the path of its file, says to read the file
before a task it covers and how to write one; a file that does not parse is left out and logged
(nanobot/dots/skills.py). The first built-in skill, invisible-playwright, is invisible-playwright-mcp's own
instructions to agents brought to the Dot's tools: identities, a selector before coordinates before a screenshot,
the real pointer and keyboard only, a second identity for a throwaway mailbox, and saying a task is impossible
rather than getting blocked.

The engine serves the list (GET /skills), the host passes it through as it does the tools (GET
/api/dots/:id/skills, the SDK's listSkills), the engine pins its answer in the wire shapes the host parses with
skillSchema, and the web client has a read-only Skills tab: the list, whether each is built in or the Dot's own, and
the open one as Markdown.

The Computer page's Browser view, its identity cards and the chat panel's browser chips are gone: an open browser
is a window of the desktop the Screen and the panel show, and the Dot makes, opens and closes its identities with its
tools. The engine, its tools and the host's identity routes are unchanged. The old /identities address goes.

The summary tests' estimator counts the thread they size, not the engine's own prompt, which grows with what it
teaches the model (a skills section had broken them a second time); the test that no engine key reaches the model
looks for the keys as keys, since a prompt's path may hold the same letters. The vitest floors go down by the 29
tests of the Browser view, net of the new ones; the pytest floor goes up to the 2231 it now has.
2026-10-07 02:33:07 +02:00
feder-cr 5289e0b8d3 no telemetry: Next's is off in the web client's own config, and the Dots' Ubuntu loses pollinate
Next.js posts anonymous telemetry from every `next build` and `next dev` unless NEXT_TELEMETRY_DISABLED is set. It
was set by `setup --all` and by CI only, so a build by hand sent it; now next.config.ts sets it, which Next reads
before it creates its telemetry, so no way of building the web client sends any, whatever the person's own Next
settings say (measured: a build started without the variable finds it set and its telemetry disabled, and posts
nothing to telemetry.nextjs.org). The copies in setup and CI go.

The minimal cloud image ships pollinate, which at the first boot of every machine, so of every new Dot, tells
Canonical's entropy server the system's version, its cloud and its processor; it is purged with the other services
a Dot never uses. The README's privacy section says what is left, and that the browser layer's launch counter is
that library's.
2026-10-07 02:01:09 +02:00
feder-cr 07cb456028 memory: the Dot keeps it itself, as Claude Code does; the Memory tab and everything it set are gone
The notes stay what they were, files in /home/dot/memory, but nothing about them is a setting any more. The prompt
always says where they are, that the Dot saves there what a later conversation will need and changes a note that is
no longer true, and names the newest; the Dot reads and writes them with its file tools (grep, find_files,
read_file, write_file, edit_file). Gone: the memory_search and memory_get tools and their memory.read permission,
the memory.enabled switch, the memory.written event and the chat's "Remembered" chip, the Memory page with its notes
and its automations views, and the routes that listed, paused and deleted automations (engine, guest client,
scheduler, API, SDK). The automations themselves stay the Dot's: it makes and removes them with its cron tool, and
the host still hears when the next one is due.

Migration 0009 takes memory and memory.read out of the configs already saved (moving their version on) and the
memory.written rows out of the log; an engine file made before keeps its old intents column, which its default fills.

Two defects found on the way: dot-agentd not answering reached the engine as an httpx error instead of a
ComputerError, so a turn crashed over the list of notes for its prompt (now status 0, and the turn goes on); and the
sample `invisible-dots init` writes would have been refused by the schema (now tested to parse).

The test floors go down by exactly the tests removed with the feature: vitest 73, postgres 2, pytest 78
(2299 to 2221), playwright 10 (memory.spec.ts).
2026-10-07 01:49:38 +02:00
feder-cr 8033cf48d0 web: Vercel's look, states in words, the rail's foot as icons with tooltips
Geist Sans and Mono (bundled), black on white and white on black, Vercel's grays
and 8px corners; a state is a colored dot and a word everywhere (the computer's
says Running, not RUNNING: one label map for every VM state), and the cost is
plain text. The rail's foot is one line of icons whose words are in a tooltip
and for screen readers; Tooltip brings its own provider, and the phone menu
opens on the current page instead of focusing (and opening) a status tooltip.

Tests: the e2e harness has no sign-in any more, the rail test reads the favicon
color from its module, and the Tasks paging test counts the queue's items from
the DOM instead of a role query over 200 cards repeated by waitFor (5s -> 2s).
Playwright floor 99 -> 96: the three login tests went with the login.
2026-10-07 00:20:39 +02:00
feder-cr 51385e45a5 image: the golden image is built once by CI and downloaded, built here only when none is published for these inputs
CI (golden-image.yml) builds it compressed and publishes the release
golden-<inputs digest> with its manifest, the image in parts under 2 GiB and
release.json. image build looks for the release of the checkout's own digest,
checks each part and the whole image against their SHA-256, and builds the
image itself when there is none (--no-download always builds).
2026-10-06 22:17:56 +02:00
feder-cr 1cb4a2194e tests: floors set to what the suites measure after the cuts
Lowered on purpose: the tests of the removed layers went with them (GeoIP pin,
command masking, proxy scrub and stderr filter, engine start-up guards) and the
web sessions/CSP revert took two browser tests. Measured: vitest 2227 on
Windows (2224 on Linux, three PowerShell tests skip there), pytest 2299,
postgres 701, playwright 99.
2026-10-06 21:59:22 +02:00
feder-cr 1102fd058d image: drop the pinned GeoIP database; the browser library keeps its own (owner: not needed)
The pin, its weekly CI check, the cache pruning, the notices file and the
engine's start check go; the engine no longer sets STEALTHFOX_GEOIP_MMDB.
Tests and smoke checks of that layer are removed with it.
2026-10-06 21:28:01 +02:00
feder-cr d9194a3149 tests: floors set to what the merged suites measure (M7)
Every floor is the measured count of the merged tree (HEAD 07812b36 for
the guest and container suites, run from a git archive; the Windows host
suites on this checkout):

  vitest      linux 2269, win32 2272   (was 2255 / 2258)
  postgres    linux 697, pg runs 272   (was 711 / 281, lowered: see below)
  whatsapp    linux 25 (9 pg), win32 16 (was 24 / 15)
  go          linux 120, win32 70      (unchanged since lane-sec)
  pytest      linux 2799               (was 2791)
  playwright  linux 101, win32 101     (unchanged)

The postgres floor is lowered, from 711 to 697 tests and from 281 to 272
runs on pg, and the reason is a move, not a loss. lane-lic took the tests
that need the real WhatsApp client out of the default suites because the
client is an opt-in install: the auth-state tests (18 tests, 9 of them on
pg) and the reading and connector tests of baileys.test.ts, plus the
final review's test that Baileys fetches no link of a text, now
packages/channels/test-optin/ (npm run test:whatsapp, the whatsapp floor,
which the whatsapp-client job counts with PostgreSQL: 25 passed, 9 of
them pg). The default suites gained the tests of the client loader
(whatsapp-client.test.ts) and of the API start in their place. The
whatsapp floor is raised by the one test that moved there in this merge.

Suites run on the merged tree, all green: typecheck; vitest 2272 on
win32 and 2269 (3 skipped, the real-PowerShell tests) on linux; postgres
697; test:whatsapp 16 on win32 and 25 on linux with pg, typecheck:whatsapp;
go 70 on win32 and 120 on linux, gofmt, vet, vet GOOS=windows, the static
build, and 131 with -tags privileged as root; pytest 2799; next build;
Playwright with CI=1, 101; engine smoke 252 passed, 0 failed, 0 skipped;
browser smoke 75 passed, 0 failed, 0 skipped; english and claims gates.

Not run: the iso kernel-check job (needs a loop mount, nothing here
touches packages/iso) and tests/e2e (a real golden image under QEMU).

No test removed.
2026-10-06 20:22:34 +02:00
feder-cr 07812b3651 docs: the user split as it is, and the GPL statement only where it is true
Both merged branches left statements that no longer matched the product:

- README.md: the architecture diagram names dotagentd as the computer
  daemon's user. The list of what is not protected now says what is still
  true after the split: it rests on the guest operating system (a kernel
  flaw or an unknown way to root ends it), the daemon holds CAP_SETUID,
  CAP_SETGID and CAP_KILL and one root command (its one door, the token
  port, is closed to dot), and nothing limits the disk, memory or
  processor time of the model's commands inside their VM. The statement
  that dot could replace the computer daemon on its port was already
  removed by the split and stays removed.
- docs/architecture.md 4.1: after the merge the section describes three
  users; its closing paragraph "What stays" is now the list above with the
  reasons and the section that covers each. The older statements that dot
  can read the Dot token and stop dot-agentd are gone with the old unit
  text (the security split wins).
- "Nothing under the GPL is installed by default" was written for the npm
  install and read as a statement about the product. It is now stated for
  the default npm install (README license section, architecture 9.8 and
  11.3, THIRD_PARTY_NOTICES.md, the tests.yml comment and two test
  headers), and architecture 11.3 and the README say the VM image carries
  the Ubuntu guest operating system and the apt packages of the golden
  image, mostly GPL and LGPL software under its own licenses, downloaded on
  the machine that builds the image and not published here.

Comment and prose changes only; no test removed, no floor changed.
2026-10-06 20:06:25 +02:00
feder-cr dab32a4a47 merge lane-lic: Baileys and libsignal opt in, true GeoIP licences
Brings in S3 (lane-lic, tip a39316c2): the WhatsApp client (Baileys, which
depends on libsignal, GPL-3.0) is no longer in the default npm install; it
lives under optional/whatsapp with its own lock file and is installed by
npm run whatsapp:install. A test asserts that a default install holds no
GPL package; the GeoIP data is described with its true licences.

Conflicts and resolutions:
- .github/test-floors.json: kept the final review's playwright floor and
  its description, added this lane's whatsapp floor and its description,
  and the counts of the previous merge. Measured values replace them in
  the floors commit that follows.
- .github/scripts/test-guard.mjs, tests/repo/test-guard.test.ts: both
  sides added a suite name to the usage text, the comment and the list
  CI must run; the result accepts whatsapp and playwright both.
- package-lock.json: regenerated from the final review's lock file with
  npm install --package-lock-only after packages/channels/package.json
  dropped baileys, so the lock holds neither baileys nor libsignal (the
  opt-in client has its own lock under optional/whatsapp).
- packages/channels/src/whatsapp-baileys/baileys.ts: the final review
  added textContent (linkPreview: null, so Baileys fetches no link of a
  text from the control plane) and exported BaileysConnection; this lane
  moved the library behind client.ts and added socketConfig. Both stay;
  WhatsAppSocket.sendMessage in client.ts now types linkPreview, so the
  typecheck made with the client installed holds textContent against the
  adapter's view of the socket.
- packages/channels/test/baileys.test.ts: the final review's two tests of
  what is sent. The one that runs Baileys' own generateWAMessageContent
  needs the library, so it moved to packages/channels/test-optin/
  sending.test.ts (npm run test:whatsapp); the one with a fake socket
  needs no library and stays in the default suite.

No test removed. Verified on this merge: typecheck, vitest 2272 passed,
typecheck:whatsapp, test:whatsapp 16 passed on a Windows host.
2026-10-06 20:04:28 +02:00
feder-cr 17f2106daa merge lane-sec: dot-agentd runs as its own user, the model's work as dot
Brings in the security split (lane-sec, tip d7dc34b1): dot-agentd runs as
dotagentd and holds the Dot's token; the model's commands, files and
browser servers run as dot through credentials; the engine socket refuses
dot (SO_PEERCRED); programs the daemon runs as itself are named by
absolute path; its ambient capabilities are emptied before it starts
anything.

Conflicts and resolutions:
- .github/test-floors.json: both sides raised the vitest and pytest
  floors from the same base, and the final review added the playwright
  floor. Kept the playwright floor and the final review's counts and
  added this lane's increments (vitest +3, pytest +8, go 120/70 which
  only this lane changed). These are provisional sums; the measured
  numbers replace them in the floors commit that follows the second merge.
- No other textual conflict. README.md, docs/architecture.md and
  apps/vm-manager/src/guest-client.ts merged without a marker: the
  security text of this lane replaces the older unit and install text
  (one owner per fact), and the statements the final review flagged as
  stale are rewritten in the docs commit at the end of M7.

No test removed.
2026-10-06 20:00:13 +02:00
feder-cr 9b1fd2fe6b tests: floors raised to what the suites reach now (final review)
Measured on this Windows host on the tree of d27601ef, after the work of the final
review; nothing is lowered.

- vitest: win32 2160 -> 2258 passed, 0 skipped; linux 2157 -> 2255 (the same three
  PowerShell tests skip there).
- postgres (the pg adapter, against postgres 18.4): 687 -> 711 passed, the "pg"
  tests 279 -> 281.
- pytest (engine, docker, python 3.12): 2778 -> 2791 passed, nothing skipped.
- playwright: 95 -> 101 passed.
- go (dot-agentd): unchanged, 64 passed on win32 (110 on linux).
- the engine smoke 238/238 and the browser smoke 75/75 passed on the git archive
  of d27601ef.
2026-10-06 19:39:35 +02:00
feder-cr d7dc34b1d9 guest: nothing dot-agentd runs as itself is found through a directory the model writes, and its ambient capabilities are emptied whatever --run-as says (sec review)
Finding 1 (high), confirmed and fixed. The unit gave the daemon
PATH=/home/dot/.local/bin:..., DefaultPowerOff started "sudo" through
os/exec.LookPath in that PATH as dotagentd, and dot owns /home/dot (it can
chmod o+x ~ ~/.local ~/.local/bin and plant ~/.local/bin/sudo), so the next
stop or sleep of the VM ran the model's program as dotagentd: it reads the
token and, being in group dotagentd, enters the engine's socket.
- dot-agentd.service: the daemon's PATH is system directories only.
- The PATH of the model's commands is built by the one owner of the model's
  environment, Server.execEnv (modelPath: the Dot's ~/.local/bin, then
  SystemPath), and no longer inherited from the daemon. A PATH a request
  names (POST /v1/proc env) still wins, as before.
- DefaultPowerOff is /usr/bin/sudo -n /usr/bin/systemctl poweroff (still
  exactly what the sudo rule names); Bash defaults to /bin/bash and
  ImportBin (and --import-bin) to /usr/bin/import, so no program the daemon
  starts is looked up in any PATH.
- The engine's own unit keeps /home/dot/.local/bin: it only checks that
  invisible-playwright-mcp exists there, and starts every command through
  `dot-agentd relay` by absolute path with a system-only PATH, so nothing
  the engine runs is found there.
Tests: guest-files (the dot-agentd unit's PATH has no entry outside the
system directories), Go (TestTheProgramsTheDaemonStartsAreNamedByPath,
TestExecEnvPathDoesNotDependOnTheDaemonsPath, the poweroff contract test
and TestExecEnvIsTheAccountsNotTheDaemons updated to the new argv and PATH),
and a smoke check that opens dot's home, plants ~/.local/bin/sudo, asks for
the poweroff and asserts it never ran. The smoke now starts the daemon with
the PATH read from the unit itself.

Finding 2 (medium), confirmed and fixed. ResolveRunAs returned before
emptying the ambient set when the daemon already was the user to run as, and
main skipped ResolveRunAs for an empty --run-as, so the three ambient
capabilities of the unit reached every command of the model (CapAmb e0:
setuid(0) works). serve now calls ForgetAmbientCapabilities first, before
anything starts, whatever --run-as says; ResolveRunAs keeps only the check
that the daemon holds the capabilities. An ambient set that is empty already
is left alone, so a cgo build (the unit tests on CI) runs where nothing is
granted. The empty/self mode stays: tests and development use it, and with
the set emptied it no longer carries the capability, only the uid (which the
daemon says in its log). Test: main_privileged_test.go starts the daemon
under setpriv with the unit's three ambient capabilities and an empty
--run-as and asserts CapAmb is empty (it failed before the fix with e0);
run.sh now runs the privileged tests of every package, not only agentd's.

Floors: go linux 118 to 120 and win32 68 to 70 (two tests added that run on
both). No test of kept behaviour was weakened; the poweroff contract test
and the unit's PATH test changed because the contract they pin changed.
2026-10-06 17:37:01 +02:00
feder-cr a39316c217 channels: the typecheck with the WhatsApp client installed checks the socket options and the auth state, and the loader refuses a client that is not the pinned release (lic review)
What and why

Finding 1 (typecheck no longer checked the socket config or the auth state):
confirmed. makeWASocket took `object` and the credentials were `{ me?: unknown }`,
so a renamed or mistyped option (markOnlineOnConnect, syncFullHistory,
shouldIgnoreJid) and an auth state the library would reject both compiled. The
earlier commit's sentence that the new typecheck did what the old compile-time
check did overstated it. Fixed at the origin:

- client.ts declares WhatsAppSocketConfig, exactly the options the adapter
  passes, and WhatsAppLogger; baileys.ts builds the object in one exported
  function, socketConfig, whose declared return type makes a typo in the adapter a
  compile error even without the library.
- The library's credentials and key data come in as type parameters (Creds,
  KeyData) of WhatsAppAuthState, WhatsAppClient, BaileysAuthLib and AuthStore,
  defaulting to the adapter's loose view, so the typecheck can instantiate them
  with the library's own AuthenticationCreds and SignalDataTypeMap.
- test-optin/shape.ts asserts, one way and strictly (methods are compared both
  ways, so the module-fits line alone could not): socketConfig's result is
  assignable to the parameter of makeWASocket; no key of WhatsAppSocketConfig is
  absent from the library's options; AuthStore's state is assignable to
  AuthenticationState. Checked by mutation, each making npm run typecheck:whatsapp
  fail: an option renamed, an option given the wrong type, a parameter type
  changed, keys.get returning another type, creds typed loosely.
- The sentences in architecture.md (WhatsApp, licenses) and in client.ts now say
  what is proved and what the method comparison does not.

Finding 2 (the loader did not check the installed release against the pin):
confirmed. entryOf only looked for node_modules/baileys, so after a pull that moved
the pin in optional/whatsapp/package.json the old release loaded silently.
Fixed: client.ts reads the one version package.json declares (it stays the one
owner of the pin) and the version in node_modules/baileys/package.json; any
difference, or either missing, is WhatsAppClientVersionError (names both versions
and `npm run whatsapp:install`). One function, whatsappClientProblem, replaces
whatsappClientInstalled and is what loadWhatsAppClient and the start.ts warning
both use, so the server warns at start with the exact reason. Tests that fail
without the check (verified by disabling it): whatsapp-client.test.ts (release
differs and is fixed by installing again; pin or installed version missing) and
start.test.ts (the warning names both releases). The stand-in client of the tests
now declares its pin like the real install does. tests/repo/whatsapp-optin.test.ts
follows the renamed variable in the loader's one import.

Tests: no test removed or weakened. whatsappClientInstalled's assertions became
whatsappClientProblem's (null or the error). Floors raised for 3 added vitest
tests: vitest linux 1196 to 1199, win32 1199 to 1202 (measured 1202). The
whatsapp suite is unchanged (15, shape.ts is a typecheck).

Verified: npm run typecheck, npm run typecheck:whatsapp, npm run test:whatsapp
(15), npx vitest run (1202 passed), go test ./... in guest/dot-agentd.
2026-10-06 17:22:40 +02:00
feder-cr 60ac1787de guest: dot-agentd runs as a user of its own and starts the model's work as dot; the engine's socket refuses dot's processes (S1)
What
- dot-agentd runs as the new user dotagentd (builder seed, per-Dot seed, unit,
  install.sh, provision.sh). Every command (POST /v1/exec, the engine's relay),
  the browser's MCP servers, the screenshot and the pseudo-terminal of a
  command run as dot through SysProcAttr.Credential; a file route acts as dot
  for the length of the request (the serving thread's file-system user and
  groups are dot's), so files are opened, created and owned as dot's and the
  home confinement of the TCP listener is unchanged on top of it. A program is
  looked up in the PATH it is started with, as dot sees it. The environment of
  those programs has HOME, USER, LOGNAME and SHELL of dot's account.
- The unit gives it AmbientCapabilities=CAP_SETUID CAP_SETGID CAP_KILL and
  nothing else; at start it checks it holds them and empties its ambient set
  in every thread, so no program it starts holds one (a command of the model
  has CapPrm, CapEff and CapAmb zero). --run-as (default dot) names the user;
  an empty value or the daemon's own user leaves the work with the daemon's uid
  and is logged as a warning.
- The token file is dotagentd's, 0600. /run/invisible-dots is dotagentd:dotengine
  and /run/invisible-dots-agent is dotengine:dotagentd (2750), so only the two
  daemons reach either socket and dot reaches neither. The poweroff sudo rule
  moved from dot to dotagentd (the user that runs it): the model has no sudo
  rule now.
- The engine's API socket refuses a peer whose uid is the model's user
  (SO_PEERCRED, 403 forbidden_peer, also when the kernel cannot name the peer),
  as a second wall under the directory modes; INVISIBLE_DOTS_MODEL_USER names
  the user (default dot) and the engine does not start when it does not exist.

Why
The model's commands ran with dot-agentd's uid, the engine's socket directory
admitted group dot and the engine's API has no authentication, so one command
could PUT /config with every permission allow, POST /events an approval of its
own parked call, and read the Dot token, which also opened /v1/agent/* on port
1024. Architecture 4.1 already named the fix (a user of its own for
dot-agentd); this is it, and architecture 4.1, 4.2, 4.3, 5.2 and 1.1, the
READMEs and the unit and seed comments say it.

Tests
- Go: untagged tests of the account lookup, the environment and the no-account
  path; privileged tests (build tag privileged, run as root in the smoke's Go
  container, where a skipped one fails the run) for uid, groups, environment,
  file ownership and permission, home confinement, the terminal's owner, the
  kill of another user's process group, the screenshot and the PATH lookup.
- Engine: SO_PEERCRED refusals (PUT /config, POST /events, POST /secrets,
  /health, the event stream), the model user at start.
- Smoke (engine suite, dot-agentd started through setpriv with the unit's three
  capabilities): as an exec command the model cannot enter either socket
  directory (connection refused), is refused 403 forbidden_peer by the engine
  even with directory and socket opened to everyone (PUT /config, POST /events,
  POST /secrets, GET /health), cannot read the token (its file, the daemon's
  environment, memory map, command line, a sweep of the readable files), gets
  401 on 127.0.0.1:1024 for the daemon's routes and the engine's behind it,
  cannot signal the daemon, has no sudo rule and holds no capability, also
  through the engine's relay and on a terminal it owns. Engine smoke 251
  passed, browser smoke 75 passed.
- vitest: unit users, the daemon's capabilities, tmpfiles groups, the seed's
  users and token owner, the platform-branch list.

Removed tests: none. Changed expectations of kept behavior: the token file and
the two socket directories are no longer dot's (smoke, seed test, install.sh
test); the engine's socket still reads through a link out of home, now checked
on /etc/hostname because a link to the token file is a 403 for dot (smoke); the
poweroff rule belongs to dotagentd (seed test).

Floors: vitest 1187/1190 to 1190/1193 (linux/win32), go 110/64 to 118/68,
pytest 2778 to 2786. None lowered.

Existing Dots: the seed's user list makes dotagentd on a golden image from
before this change, so the new runtime disk starts there too; install.sh and
provision.sh still refuse an image that has no such user.
2026-10-06 16:43:54 +02:00
feder-cr ee7b80cf76 licenses: nothing GPL is installed by default; the WhatsApp client is an opt-in install, and the GeoIP notices name the data licenses (S3)
What and why

npm ci installed Baileys and its GPL-3.0 dependency libsignal for everyone. Now
no package of the default install needs a GPL license:

- Baileys is declared by no workspace (not regular, dev, optional or peer) and is
  out of the root package-lock.json (49 packages fewer: libsignal, curve25519-js,
  whatsapp-rust-bridge and what only the client needed).
- It lives in optional/whatsapp/: its own package.json (Baileys 7.0.0-rc14, one
  exact version) and package-lock.json (integrity hash of every package),
  outside the workspaces. One command installs it: npm run whatsapp:install
  (npm ci --prefix optional/whatsapp --ignore-scripts).
- packages/channels/src/whatsapp-baileys/client.ts is the one file that names the
  library. It looks for it in that folder when a connection opens, loads it by
  path only if it is there, and otherwise throws WhatsAppClientMissingError, whose
  message is the command and INVISIBLE_DOTS_WHATSAPP=1. That text is the channel
  status, is logged at server start when the variable is set without the client,
  and is what linking answers when WhatsApp is off (WHATSAPP_ENABLE_HELP).
- Nothing imports the library by name, not even as types: the adapter writes down
  what it uses of it (WhatsAppClient) and compiles without it. A typecheck with the
  client installed (npm run typecheck:whatsapp, tsconfig.whatsapp.json, test-optin/
  shape.ts) proves the real module has that shape and that the key groups are the
  library's own, which the old compile-time check did. The tests that need the real
  library moved to packages/channels/test-optin and run with npm run test:whatsapp
  (vitest.whatsapp.config.ts); a new CI job, whatsapp-client, installs the client the
  way a person does and runs both, with PostgreSQL. The command bundle no longer
  lists baileys as external: it has no import of it.
- tests/repo/default-install-licenses.test.ts scans the root lock for licenses: no
  GPL or AGPL (SPDX expressions read with OR/AND), a license stated for every
  package, LGPL only among @img/sharp-* (Next.js's optional image binaries, named in
  the notices), and the same scan flags libsignal in the opt-in lock.
  tests/repo/whatsapp-optin.test.ts now proves the opt-in shape: no workspace
  declares it, it is outside the workspaces, pinned with integrity, one documented
  command, one loader, no static import, not in the bundle.

GeoIP notices

The first entry said the merged file is GPL-3.0, which is the license GitHub shows on
daijro/geoip-all-in-one: the license of its scripts. Read at the pinned release
(2026.09.30): its README states no license for the merged data and only credits
IP2Location LITE, GeoLite2 and DB-IP. geoip-notices.ts now lists the actual data
licenses of the sources (CC BY-SA 4.0 for IP2Location LITE, IPinfo and IPLocate.io;
the GeoLite2 EULA; CC BY 4.0 for DB-IP Lite; CC0 1.0 for the tdulcet GeoFeed + Whois
+ ASN database, which sapics lists as PDDL 1.0; ODbL 1.0 for the OpenStreetMap-derived
time zone boundaries), with GEOIP_STATEMENT: the image is built by the person on their
own machine, invisible_dots does not publish or redistribute it, and whoever shares it
meets those terms. Every golden manifest records it as notices_statement beside
notices. THIRD_PARTY_NOTICES.md, the README, the image builder README and the
architecture document say the same; the third-party notices test pins the source list
and license of each source and keeps the notices, the statement and the manifest equal.

Tests removed or moved (none of a feature that went away)

- packages/channels/test/whatsapp-auth.test.ts (9 tests) moved to
  packages/channels/test-optin/auth-state.test.ts, unchanged in what it asserts: it
  needs the real BufferJSON, initAuthCreds and protobuf, which are not in the default
  install. It runs in the whatsapp suite on PGlite and PostgreSQL.
- packages/channels/test/baileys.test.ts: "reading a Baileys message" (5 tests) and
  "the connector on a socket that cannot connect" (1) moved to
  test-optin/reading.test.ts for the same reason; endOf and the addresses (4 tests)
  stay in the default suite.
- hub-whatsapp.test.ts "goes with the Dot" used the real library only to give the hub a
  session whose writes must not come back; it now uses a JSON stand-in for the
  library's three helpers (the real ones are in test-optin/auth-state.test.ts).
- tests/repo/whatsapp-optin.test.ts: the five old checks are replaced by checks of the
  new shape; "pinned to one exact version in the channels package and the lock file",
  "declared by no other package" and "imported by the glue file alone" no longer apply
  because the dependency is not in the channels package or the root lock and no source
  imports it.

Floors

- vitest: linux 1187 -> 1196, win32 1190 -> 1199 (measured 1199 on win32).
- postgres: min_passed 675 -> 659 and pg 273 -> 264, LOWERED: the 9 pg runs and 9 PGlite
  runs of the auth-state tests moved to the whatsapp suite, and the 6 moved glue tests
  (measured 659 passed, 264 pg, against a real PostgreSQL 18.4).
- whatsapp (new): linux 24 with 9 pg (CI, PostgreSQL), win32 15 (no PostgreSQL).

Suites: typecheck, typecheck:whatsapp, vitest 1199 passed (win32), postgres subset 659
passed with 264 pg, whatsapp suite 24 passed with PostgreSQL and 15 without, go test,
engine 2778 passed, web build, english and claims gates clean.
2026-10-06 16:42:01 +02:00
feder-cr 24ab868bbe tests: every floor is the measured count of the merged branch (M6)
Both sides of the merge had raised the floors of their own suites; the numbers
are now what the merged tree reaches, each measured on the host it is
floored for (no floor goes down):

- vitest: linux 2157 passed with its 3 PowerShell skips (was 1187 on this
  branch, 1998 on lane-web), win32 2160 (was 1190 and 2001);
- postgres (the CI job's postgres 18.4 service, the same five workspaces):
  687 passed, 279 of them named pg (was 675 and 633, 273 and 255);
- go: linux 110 in a golang:1.25 container, win32 64 with 2 skips, both
  unchanged by the merge (lane-web had 108 for linux);
- pytest: 2778 passed, nothing skipped, in python:3.12-slim (was 2778 and 2421);
- playwright: 95 passed on linux (node:24 container, chromium installed as the
  web job does) and 95 on win32.

Also run on a git archive of this commit from WSL: the engine smoke, 238 checks
passed, and the browser smoke, 75 passed, none skipped.
2026-10-06 15:58:31 +02:00
feder-cr a4435fac4d merge: lane-web (the new web UI, W1-W12) into the merged branch (M6)
Merges caf63b9d: the whole new web UI with its own merges of older browser and
host work. 16 files conflicted; each is resolved keeping both sides' behavior
with one owner per fact:

- .github/test-floors.json: both sides raised the floors of their own suites;
  the file takes lane-web's shape (the playwright entry) and the numbers are
  set to the measured counts in the commit that ends the merge (M6 step 4).
- THIRD_PARTY_NOTICES.md: the GeoIP section of this branch and the notices of
  the web client's derived files (shadcn/ui, OpenDots, AI Elements, ...) are
  both kept; the nanobot web UI paragraph stays next to the nanobot license it
  refers to.
- packages/shared/src/states.ts: STOP_REASONS (this branch) and
  COMPUTER_STOPPED / computerIsUp (lane-web) are different facts, both kept.
- apps/scheduler/src/scheduler.ts: the ENGINE_BROWSER_ANSWERS table that
  lane-web still had is the one this branch deleted when the identity error
  statuses got one owner (shared, engine); deleted, nothing used it.
- apps/vm-manager/src/guest-client.ts and its test: lane-web's refusal of a
  frame that is not image/jpeg (502 frame_failed) is kept; this branch only
  had the doc line.
- docs/architecture.md: this branch's GET /state (pending_approval), GET /tools
  (offered with managed identities) and the identity proxy validation; lane-web's
  events filter text (tools, order, before), with the migration named by this
  branch's clean sequence (0006_events_task.sql).
- invisible_engine_dots/nanobot/dots/server.py: this branch's version (the
  shared IDENTITY_ERROR_STATUS owner; lane-web's own table is the older copy).
- invisible_engine_dots/tests/dots/test_browser_manager.py: this branch's more
  robust waits for the cancelled-launch test; lane-web's until_recorded helper
  is kept for the slow-launch helper.
- invisible_engine_dots/tests/dots/test_permissions.py, wire_shapes.json,
  tests/fakes/engine_harness.py: this branch's (the navigation approval tests,
  the managed-identities cases of the tool table, the manager made in
  new_engine); lane-web's wire_shapes is a subset of it, checked as parsed JSON.
- apps/web ComputerTab.tsx, lib/timeline.ts, test/timeline.test.ts: deleted, as
  lane-web deleted them. What this branch had put in them (L13: the automation
  next-run line of the timeline, the stop confirmation) lives where the new UI
  owns it: lib/events/view.ts describes automation.next_run (family computer,
  with its test in event-view.test.ts); the stop confirmation and the Computer
  views are the next commits.
- apps/web/test/format.test.ts: both sides' tests; confirmText kept.

Migrations stay the one clean sequence 0001..0010 of this branch (lane-web did
not change a migration after the point both branches share).

Checked after the merge: npm ci, typecheck (root, web, e2e), vitest 2150 passed.
2026-10-06 15:15:29 +02:00
feder-cr f1107a9f86 channels: the Telegram adapter is connected only after a poll was answered, and a 429 on a poll is waited out (merge1 review)
Finding 1 (real, fixed). The adapter reported connected right after getMe and
deleteWebhook, and the supervisor starts its backoff over on that report, so a
poll that failed every time (a 409 from a second server on the same bot, a 429,
a network fault) restarted at the initial delay on each cycle: about two
channel.status events and three Bot API calls per second for ever, and the UI
showed connected for a bot that never polled. A 429 on a poll lost its
retry_after, because only sends honoured it.

At the origin: the first getUpdates is a short poll (timeout 0) and connected is
reported after it is answered, so the backoff grows across cycles that never got
an answer; a 429 on any guarded call becomes ChannelRateLimitedError carrying
retry_after, and the supervisor waits at least that long. @grammyjs/auto-retry
named in the design is not adopted: the hub owns retries and backoff (the
adapter header says so), and a second retry layer under it would hide the
failures it reports. FakeBotApi forgets the failures set for a token when that
bot is added again, so a persistent failure of one test does not leak into the
next.

New tests (both fail on the previous code): a persistent 409 on getUpdates keeps
a growing delay between tries (at most 20 in 1.5 s where the old code made 39),
never reports connected and writes at most 3 channel.status events; a 429 with
retry_after 1 on a poll is waited out although the backoff is a few
milliseconds. The wait for the next poll in the redelivery test of telegram.test.ts
counts from the polls made before the second run, because a run now makes one
more (short) poll before its long ones; what it checks is unchanged.

Floors raised for the two tests: vitest 1187 linux, 1190 win32; postgres 675,
pg 273.
2026-10-06 14:51:05 +02:00