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]>
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.
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.
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.
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.
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.
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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).
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.