Repository navigation
Hatchling 0.6.0
Install it and leave it on: a service that starts at boot, doctor for what stops it working, and a hosted model's key added in one command. And the post-0.5.0 audit's three High findings, fixed.
Added
-
hatchling-broker secret add: a hosted model's key, typed where nothing keeps it (WO-033). Adding a hosted API's key meant running a script from the docs that wrote it as a plaintext file, then sealing the vault with the broker stopped. Nowsecret add --for openrouter.ai:443reads the key at a prompt that doesn't show it, or from a pipe (a password manager's), and seals it at once under the name the broker's egress reads. It's never in a file, an argument, the environment or the shell's history. A running broker uses it from its next request.secret add <name>does the same for any other secret, such assearch-brave. A name already in the vault is refused unless--replace, before anything is asked. CI's hosted-model checks now seal their key this way. -
The broker's commands take the service's settings (WO-033). Installed as a service, the broker's settings are in
/etc/hatchling/hatchling.env, which systemd gives it, and a command run from a shell had none of them. Now an operator command run withsudofrom a shell with no settings runs again with all of the service's, sosudo hatchling-broker tools listjust works. It never runs with only some, which could, say, take a keyring's absence for a key file. The file is read only while it's root's and nobody else can write it: it says what a root broker runs.doctorholds a settings file to the same rule, and names one that fails it. -
hatchling-broker doctor: what stops Hatchling working here, and the fix (WO-033). A first start fails on a few things, which the broker met one at a time, each in its own words.doctorchecks them all at once:- the settings parse;
- the agent's program is a program;
- this machine can make a cell, as the broker makes one, naming the step the kernel refused and why (AppArmor's user-namespace restriction, a kernel that allows none or only root's, a container's policy);
- the model's server answers, with the model named.
Each problem comes with its fix. It reads the service's settings (
/etc/hatchling/hatchling.env) when the environment has none, and changes nothing.scripts/install.shruns it before starting anything. CI runs it on a stock Ubuntu with the restriction on, where it must name AppArmor, then with the README's profile, where it must find nothing wrong; and on the installed service, where it must name a model that's stopped. -
Install it as a service (WO-033).
scripts/install.shinstalls a release (or your own build) with a systemd service for the broker, a user service for the app's host that runs without a login, Hatchling's users and its state directory, and one configuration file,/etc/hatchling/hatchling.env. It checks every file against the release'sSHA256SUMSbefore installing anything, seals the vault, starts both services and prints the app's address. With systemd 254 or later, the broker gets a cgroup to give each cell. CI installs each commit's own build this way on a stock Ubuntu, and asks the agent before and after a restart. See Install Hatchling as a service. -
A page for security reviewers (docs/security). It covers what leaves the machine and who turns each part on; that Hatchling sends nothing on its own (no telemetry or update checks); what's kept and how it's sealed; each log and what it records; a hardening checklist; and what to do after a suspected compromise. SECURITY.md now gives response and fix targets, a disclosure deadline, the supported versions, and a CVE for every confirmed vulnerability in a release. The distribution page is current: it was written before the first release.
-
The app comes built with each release (
hatchling-ui.tar.gz). Running Hatchling from the release's programs needed Node 22 and a clone of the repository, only to build the app the host serves. The release workflow now builds it twice from different paths, releases it only if the two builds are byte-identical, and attests it with the rest: extract it and pointHATCHLING_HOST_UIat it.scripts/release-artifacts.sh uibuilds it the same way.
Changed
- The cells' user by name, and one setting fewer.
HATCHLING_AGENT_UIDtakes a local account's name as well as a uid (HATCHLING_AGENT_UID=hatchling-cell), so a configuration file needs noid -u. A cell on the broker's own map now takes that uid as its id-map base whenHATCHLING_CELL_UID_BASEis unset: the one value the broker accepted. The README's quickstart drops both theid -uand the second setting.
Fixed
doctor's cell probe stages under a name nobody can guess. It made its directory in the shared temp directory under a name from its pid, anddoctorruns as root on a service install. Another local user could make that name first and stop the probe, or change what it reported. The name is random now, as the broker's other temp directories are. Found by the 0.6.0 release checklist.- The quickstart's first task waits for the broker. The README starts the broker in the background and gives the agent its first task straight after, and
docs/examples/first-run.pyconnected at once. When the broker took a moment longer to make its socket, the first task failed withFileNotFoundError, in CI and for anyone following the README. It now waits up to 30 seconds for the socket. - Two hardenings of a cell's view. A cell's read-only binds (its program, libraries, tools, code) are now
nosuidandnodev, as its writable ones were: a set-id program or a device node in a host directory is nothing in the cell. And when a harness's work tree is given to its cell and taken back, a file with another name (a hard link) is left as it is: its other name may be outside the tree, and on a host that lets users link to others' files (fs.protected_hardlinks=0) a link the cell made would have had a host file's owner changed. Found by the post-0.5.0 audit. - Every broker thread is confined. The broker confines itself once its start is done (WO-017): no-new-privileges, and Landlock to what it keeps and reads. Both hold the thread that asks and the threads it starts after, not those already running. The broker started its agent cell first, and the two threads that watch and reap that cell went on unconfined. Now the cell is started after the confinement, and the broker refuses to confine itself, and so to serve, with more than one thread. Found by the post-0.5.0 audit.
- A run cell runs the agent's program, and only it. Run as root, the spawner held a run cell's program to the paths a cell may be given, which include the broker's own run directories. A run cell holds the vault channel, on which the keeper gives it the secrets its grants name (WO-031). So a compromised broker could put a program of its own there, have it run as the agent, and be given the secrets. Now a run cell's program must be the configured agent's (
HATCHLING_AGENT_BIN, resolved), and with none configured no run cell starts. Found by the post-0.5.0 audit. - The path the spawner checked is the path a cell gets. Run as root, the spawner checked a cell's host paths by their names, and the helper then followed whatever links were in them when it bound them. So a compromised broker could put a link in the way after the check, in a directory it writes, and have the cell get somewhere else. Now the spawner resolves every path, links and all, before it checks it, so a link is checked as where it leads. It hands the helper the resolved paths, marked so (
CellSpec::resolved): the helper refuses one that leads anywhere else by then. Each source is opened with no link allowed on its way (openat2,RESOLVE_NO_SYMLINKS), and bound by that descriptor. Breaking forhatchling-isolation:CellSpechas a new field,resolved. Found by the post-0.5.0 audit. - A cell's stdio is a stream, and nothing else. Run as root, the broker asks the spawner to start each cell's helper on three descriptors it sends, which become the cell's stdin, stdout and stderr. The spawner didn't look at them. A compromised broker could send a directory opened
O_PATHas stdout, and the cell could open files under it (openat(1, ".ssh/...")) from outside its own root; or a socket connected to another service's. The spawner now takes only a pipe,/dev/nullor one end of an unnamed socket pair, and for the cell's output also a file opened to be written only, a terminal, or the journal's stream socket (hatchling_sys::stdio_kind); anything else, and it starts nothing. Found by the post-0.5.0 audit. - A link in a configured directory takes a cell nowhere. Run as root, the spawner gives a cell only paths the operator configured, and the broker's own run directories. Below a configured directory, such as the scheduler's run root, it checked the path by its prefix alone, though the broker writes there. A compromised broker could make a link there to the owner's home and have it bound into a cell, writable. Now nothing on the way from a configured directory to the path may be a link, as with the run directories. Found by the post-0.5.0 audit.
- A skill or a build no longer fails, now and then, with "Text file busy". The broker writes each skill's verified copy, then a cell runs it. A process the broker started meanwhile could hold the copy open for a moment, and the kernel runs no program that's open for writing, so the cell failed to start. A cell now tries again for up to a second (
hatchling-sys'sexec). Seen in CI's build tests. - One session can't stop you being asked. A denial paused every new request on the broker for ten minutes, and a session's requests went on waiting after it ended, holding places in the queue of five: content the agent read could have sent a few subagents to fill it. Now a denial pauses only the task it denied, and a session that ends takes back the site visits and signed-in uses it had waiting. A build it proposed stays, since the worker makes it. Found by the post-0.5.0 audit.
- A secret the broker keeps never leaves in a page's address or a search. Typed text was checked for one, but the address of a page the agent read or opened, and its search queries, weren't. A held secret the model had seen, on a page or echoed by a service, could have left in a URL, to the site and its logs, or in a query, to the search provider. Each is now refused before the owner is asked, a page is read or the provider is asked, in every form the output gate looks for. Found by the post-0.5.0 audit.
- Signed in, the agent asks you before it browses, and before anything it does could send. In a chat, a site covered by a signed-in profile you kept opened with no question, and a click, a choice or Enter reached the page with nothing in between: an email the agent read could steer it to forward your mail, or post, as you. Now the first page signed in, in each session, is put to you with the profile's sites and the agent's reason, and nothing reaches the browser until you say yes. After that, pressing Enter, clicking anything but a link and choosing an option each wait for you, on a card that shows the page, the control and everything the agent typed on that page. Your yes is for that act and that text; more typed is asked again. Typing, scrolling, reading and following links aren't asked. Breaking for
hatchling-broker:Approvedhas a new variant,Signed. Found by the post-0.5.0 audit. - Allow once is the page you were shown. A site's approval named the site alone, and a request for another page on it, made while you decided, joined the one waiting without a new card. So the address you approved once could be swapped for another on the same site, one carrying what the agent had read. Once now approves the page shown: another page is refused while one waits, then asked about itself. In the agent's browser, once covers the visit, and the card now says so. Found by the post-0.5.0 audit. Breaking for
hatchling-broker:site_actiontakes the page, andSiteAskhas a new field,visit. - No dependency's build code runs where the crates.io token is. The release workflow published with
cargo publish, which builds each crate and its dependencies first, so every dependency's build script and macro ran in the job holding the publish token and the job's means to mint another. Every crate is now packaged and built in a job with no token, the GitHub release and crates.io both wait for it, and the token's job publishes with--no-verify, building nothing (scripts/publish-crates.sh --no-verify). Found by the post-0.5.0 audit. - A build's card shows what will be built. The owner was asked to approve a build from each input's name, size and hash, and the agent's own sentence about it. An idea the agent captured from something it read could go to a builder on that sentence. The card now shows each input's text (its first 1000 characters, and how many more), as words the agent or the owner wrote. Found by the post-0.5.0 audit.