ADI v1.0.0
The first release under a stable version number. Two things are in it: a browser tab can now be
paired with a machine and render its control panel with nothing listening in between, and five
security fixes — one of which closed a path from any web page the operator happened to visit to
code running as root on their machine.
One thing to do after updating. This release replaces the local certificate authority (see
below), so https://app.adi will warn until you trust the new one. On macOS:
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/.adi/mono/hive/tls/ca.pem. The old root is inert from here and worth removing from your trust
store. Plain http:// is unaffected, and the front door logs the instruction on every start
until it is done.
Security
-
A web page you visit can no longer drive this machine's control panel. The panel has no
login: it listens on loopback and treats everything that reaches it as the operator. But.adi
resolves machine-wide, sohttp://app.adi/api/*was an address any page open in the operator's
browser could post to — and because the panel reads JSON out of a body whatever its type claims,
atext/plainPOST was a CORS simple request: no preflight, nothing to consent to, the side
effect just happens. The end of that chain was root,POST /api/fs/writebeing jailed to a
directory that holds thehive.yamlthe root front door re-reads every three seconds and runs.
Every/api/*request is now refused unless itsOriginis absent or names the host it was
sent to. This is not authentication and does not pretend to be — it stops a web page driving the
panel; a process already on this machine can still send whatever it likes. -
The root front door launches nothing at all. The rule that strips service runners from a
hive running as root was applied only to imported hives, leaving the top-level file's own
run:exactly where it was — and the file that got the exemption is the one that needed the
rule, since the root daemon is pointed at a store file that is user-owned by design. Arun:
written there was executing as root within three seconds, no restart needed. The decision moved
to the accessor the supervisor actually calls, which as root now returns nothing and names the
services it dropped. -
A root daemon will not run a program an ordinary user can rewrite. Installing the front door
resolved its program as a sibling of whichever binary was doing the installing, soadi enable
from a repo build was enough to put atarget/releasepath into a root plist — after which a
plaincargo build --releasebecame root within about a minute, with no prompt. Installation
now refuses if that program, or any directory above it, is owned by a non-root user or is
group- or other-writable, and names the component at fault before anything is written. -
The local certificate authority may only vouch for this machine's own names. The CA you are
asked to install as a system trust root was bounded only in path length — nothing constrained
names, so it could sign a certificate forgoogle.com, for a bank, or for your own SSO, and
every browser on the machine would have accepted it. It is now constrained to theadiDNS
subtree,localhostand127.0.0.0/8, which is exactly what the front door serves. Because the
copy in your trust store is not the copy on disk, an existing CA is replaced outright rather
than quietly rebuilt — hence the re-trust above. -
The store is written owner-only. Every file it held landed at whatever the umask said, which
is0644on a stock macOS account. That included this node's long-term mesh identity key, and
the invite nonce and ticket that are together a complete invitation to join. macOS puts every
local account instaff, so a second account on the machine could read all of it, and every
backup and sync carried the same bytes. New files are opened0600and directories0700;
existing ones are repaired as they are read, keeping the owner's own bits so a tool script stays
executable.
Added
-
A browser tab is its own peer, and it pairs by dialling.
mono-mesh-client.withadi.devis a
page that holds its own key, dials the machines it has been paired with, and renders each one's
control panel — no server, no open port, nothing listening behind it. Everything on the screen
came over QUIC from a machine that is not reachable from the internet. Long-lived streams work
both ways, so the panel's live channel, its event streams and its websockets all run through it.
Pair by runningadi-mono mesh inviteon the machine you want to reach and giving the token
to the page. -
Pairing a phone is pointing it at a code. An invite is around a thousand characters, and
getting that onto a phone was the whole friction.adi-mono mesh invitenow draws it as a QR at
a terminal; the Fleet page has a Show pairing QR button that mints one and counts down its
ten minutes; and the browser client has a Scan button that reads one. The code carries the
token rather than a URL, so a phone's own camera app will only offer to copy it — the page says
so, because that failure is silent and looks like a broken code. Redirected output is unchanged,
so scripts that read this command keep working. -
A phone sees what each machine runs, not just its panel. Under every paired machine are the
dashboards that machine is running, read from the node itself. A row your pairing did not grant
says Allow, and the first tap asks the node for it. -
⌘K answers on the page it just took you to. The palette was mounted only inside one shell,
so using it, jumping somewhere, and pressing it again did nothing — it stopped working exactly
where you had started trusting it. Both shells now take their rows from one list, so they cannot
come to disagree about what the app can do, and Pair new device is one of those rows: it
lands on Fleet and raises the QR. -
The chat's right rail says who you are talking to, and where. Chat Analytics counted what a
conversation cost and what went wrong in it, and never named the agent having it — the name was
on the session row you clicked to get here and nowhere on the screen you landed on. Above it
there is now an Agent block of its own: the agent's name and what it is doing right now
(waiting on you, answering, running, awaiting a wake, idle), the backend and model behind it and
the project it is filed under; the conversation's working directory, when it started and
when it last said something; and the settings that explain the behaviour in front of you — its
permission mode, how many tools, knowledge bases and secrets it carries, whether it keeps a
memory, and whether it runs unattended. It appears before the first turn lands, which is when
the counts below it have nothing to show yet. -
Analytics opens with what is running right now. The page led with a fortnight of history,
so it could tell you what this machine had done and nothing about what it was doing. A panel
above the totals now names every run in flight — the agent, the project it is filed under, the
task it was given, how long it has been at it and when it last said something — and the live
sessions of interactive agents beside them, since those keep no run history to appear in.
The times climb while you watch, and when nothing is running the panel says so rather than
vanishing, which reads the same as a page that hasn't loaded. -
An agent is edited on its own page, at
/agents/newand/agents/<name>/edit, rather than
in a form that opened under the list. The page paints filled, survives a refresh, a deep link
and Back, and a rename leaves the URL pointing at what is in the form. Settings in a chat's
Agent panel opens that agent's editor rather than the list you were already past.
Changed
- The
tcp:andctl:grant families are withdrawn from the fleet page and the CLI. Both were
offered as examples and neither was ever enforced: what actually gates the raw-forward path is a
different list entirely, and nothing anywhere consumedctl:. This failed closed, so it was
never exploitable — but an operator who addedtcp:127.0.0.1:22believed they had opened
something and had not, and one who removed it believed they had closed something and had not.
Both beliefs are worth correcting. They still parse and load, so existingfleet.tomlfiles are
unaffected;ctl:is explicitly reserved.
Fixed
-
A Linux node stops taking its whole session down with one runner.
killon procps-ng keeps
only the first digit of a bare negative pid, so stopping one hive runner was aSIGTERM
broadcast to every process the caller owned — control panel, dashboards, DNS and the session
manager itself. Linux pids reach seven figures and a node's all begin with1, so this fired
every time. It read for weeks as "the node keeps losing its linger and dying"; losing the linger
was the aftermath, not the cause. macOS parses the same argument correctly, which is why it only
ever surfaced on Linux. -
A fronted app keeps its own
Authorizationheader. The stored mesh password rode
Authorization, attached whenever the client had sent none — so a page sending its own bearer
token to its own API suppressed the password, drew a challenge from the node, and popped the
browser's native password prompt on an ordinaryfetch. The document itself carried no such
header, so the page always loaded and only its AJAX calls prompted, which looked like a site
asking for a password at random. The password now ridesX-Adi-Authorizationand is stripped at
the gate, so it never reaches the service and the app's own header arrives untouched. A node
from before the split still works. -
A release will not ship a panel with no layout. The dev server writes the same directory the
release build embeds, so its partial output could land between the two and ship anindex.html
from one build beside the assets of another. Nothing failed — the missing stylesheet hit the
single-page fallback and was served as HTML with a200— and the panel came up with its markup
intact and no styling at all. Every asset the page asks for is now checked to be present, after
the UI build and again after the binary has embedded it.
Performance
- The store no longer flushes every write to disk. Making the store owner-only had replaced a
plain write with one that also fsynced, which was a durability change smuggled in under a
permissions one and paid on every task, manifest and run record the app saved.