Repository navigation
Releases: undergroundrap/hatchling
Release list
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...
Hatchling 0.5.0
Added
- Release binaries you can check, and rebuild to the same bytes (WO-013). A version tag on
mainnow releases it: thereleaseworkflow buildshatchling-broker,hatchling-agent,hatchling-host,hatchling-browserandhatchlingfor x86_64 and aarch64 Linux, with the pinned toolchain andCargo.lock's dependencies, each target twice on separate runners from different paths, and releases only if the two builds are byte-identical. Each program comes with a CycloneDX SBOM, and its dependency list embedded bycargo auditable; every file,SHA256SUMSincluded, has a build-provenance attestation, sogh attestation verify <file> --repo undergroundrap/hatchlingchecks it was built by this repository's release workflow from the tag. The release notes are the version's section of this file, and a tag without one, or whose commit isn't onmain, is refused. crates.io publishing moved into the same workflow, still through trusted publishing. Releasing says how to cut a release, verify one, and reproduce its builds. - The cell suites run as root in CI (WO-015). The README runs the broker as root, and a root broker takes paths an unprivileged one never does: its cells run as a user of their own, and it hands its sockets and run directories to other users and drops to its own. A new required job, "cells as root", runs every cell suite that way with the README's users and the pinned headless Chromium, and none may skip.
HATCHLING_ROOT_PASS=1 bash scripts/check.shruns the same pass locally: see CONTRIBUTING. Every failure the first root runs found was the tests' own assumption, each fixed: a root test's cells were givennobody, which the broker refuses, and its own directories and sockets weren't handed over as a root broker's are. - Add a coding CLI by a manifest, OpenCode first (WO-018). Beside Claude Code and the Codex CLI, the broker runs any CLI that speaks Anthropic's or OpenAI's API, defined by a manifest in
HATCHLING_HARNESS_MANIFESTS: its binary and companion files, each pinned by SHA-256; its API and its one destination; its command line, with{task}; and the variables that point it at its relay ({relay}) and carry its surrogate key ({key}). It runs as the built-in harnesses do: in a CLI cell, through the relay with a surrogate, its key the vault secretharness-<name>, its patch computed by Hatchling. The directory and each manifest must be the broker's user's or root's and writable by no one else, and a manifest that's wrong in any way refuses the start, naming the field. OpenCode 1.19.5 is the worked example, tested end to end in a real cell. For it, a CLI cell now allowsepoll_waitandpwritev2(the same asepoll_pwaitand thepwrite64it had), and answerssendmmsg, which a name's lookup sends with, with an error instead of killing the CLI. See Harness manifests. - Approved tools: the programs a coding CLI's cell runs, listed and pinned (WO-019, part 2). The broker keeps a list of them,
approved.jsonin<vault dir>.tools/, each with its SHA-256 and when it was approved.hatchling-broker tools add <path>approves one (a regular file at an absolute path),tools listshows each and whether it's still that program, andtools remove <path>takes one away; a change takes effect at the broker's next start. A cell's tools are pinned to the hashes approved: a tool that changed doesn't stop the start, and every run that would get it is refused, naming it, until it's that program again or approved again. The list must be the user's the broker starts as (root, for a root broker) and writable by no one else, or the broker refuses to start. An existing deployment sees no change: its first start approves the tools it has,HATCHLING_HARNESS_TOOLS's or the host's defaults. After that,HATCHLING_HARNESS_TOOLSmust name the list's tools or be unset. See Approved tools. - What you said earlier stays with the agent (WO-026, part 4). Past the conversation's bounds (8 exchanges, 24 KiB), an exchange's answer goes, and what you said in it is still given to each chat run, word for word: up to 32 of your messages and 8 KiB, newest kept. A decision or a correction you made is never summarized, so nothing a model wrote, or a page it read, can change it. The model sees them as one message, labeled as Hatchling's record, before the exchanges (
HATCHLING_RUN_EARLIER, which the agent refuses past the bounds or for any run that isn't a chat turn). With a conversation store they survive a restart, and the app shows them under a notice of their own. Breaking forhatchling-scheduler,hatchling-brokerandhatchling-agent:RunRequest,ScheduledRunand the agent'sConfighave new fields;Chat::resuming,KeepsandResumed::Kepttake aConversation. - The conversation survives a restart (WO-026, part 3). Set
HATCHLING_CONVERSATION_DIR(absolute, withHATCHLING_SCHEDULER_DIR, apart from every other store), and the chat keeps the conversation sealed there, in a store of its own: what a run is given, nothing more. After a restart the chat goes on with it, and the app shows each exchange under Hatchling's notice, so you see what the agent is still given. New conversation removes it before you're told it started. A store that can't be read is removed, and the app says a new conversation started. Unset, nothing changes: a restart starts a new conversation, and nothing is written. Breaking forhatchling-broker:SchedulerConfighas a new field,conversation. - A new conversation, from the app (WO-026, part 2). The chat's header has New conversation. The owner's alone (
new_conversationin the local API): the next run is given nothing said before it, and Hatchling's notice shows where it starts. The messages stay on screen. A message sent before it, and the answer to one the agent was working on, aren't part of the new conversation. Breaking forhatchling-broker:Chat::steptakes each message with the conversation it was sent in, and the conversation now. - The agent proposes builds from the chat, and the owner decides (WO-032). A chat turn now has five tools:
idea_capture,idea_list,idea_refineandidea_notekeep ideas in the broker's build store, andbuild_proposeasks the owner, in the app, to build one, with the builder the operator named for its kind (HATCHLING_ARTIFACT_BUILDERS,kind=name@version, comma-separated, besideHATCHLING_ARTIFACT_DIR). A chat run's session has ideas alone: it can't build, deliver or describe a build, or record an idea as the owner's, and it proposes only with the operator's builder; the owner's approval is what builds. Scheduled runs and subagents still have no build store. The broker tells a chat run the builders (HATCHLING_RUN_BUILDERS), and the tools are offered only then. Breaking forhatchling-brokerandhatchling-agent:ArtifactConfig,ScheduledRunand the agent'sConfighave new fields. - A sign-in goes from the vault keeper to the browser's controller, and never through the broker (WO-031, part 2c-ii). The broker asks the keeper to fill a sign-in into a profile's browser, and names the fill to the browser's controller by its nonce (
BrowserCommand::SignIn). The controller reads the sign-in from its own vault channel (HATCHLING_VAULT_FD) and types it on the origin the keeper sent, hidden from the agent as before. The broker can no longer ask the keeper for a sign-in's secrets: a bug that discloses the broker's memory or log finds no sign-in, kept or filled. A broker without a vault keeper fills none. A browser whose profile the owner kept while it ran gets no fill until it's opened again. - A browser started for a signed-in profile gets a vault channel to the keeper, which fills its sign-ins on it (WO-031, part 2c-i). The broker names the profile at the browser's start, and the spawner hands the cell's vault channel to the keeper named with it, never to the broker. Asked to fill a sign-in into that browser, the keeper checks it's the profile's and kept for the page's site, records the fill in the vault's access log (
fill,browser-fill), and sends the sign-in on the cell's channel, telling the broker only the fill's nonce; a fill it refuses is recorded as denied. Nothing asks for one until part 2c-ii, when the browser's controller types it. - The sign-ins are the vault keeper's (WO-031, part 2a). The store of the owner's site sign-ins (
HATCHLING_BROWSER_CREDENTIALS_DIR) is now the keeper's, as the vault is: the broker is handed no key to it, and run as root, its directory ishatchling-keeper's, outside the broker's user and its Landlock rules. An installation from before is handed over at the next start. The broker still checks that a new sign-in is for one of its profile's sites, then asks the keeper to keep it, list them or delete one. Until the browser's cell is given a fill itself (part 2b), the broker asks for one sign-in's secrets to fill, and the keeper records each such read in the vault's access log (browser-fill) before sending it. - A cell's vault ops go straight to the keeper, and the broker relays none (WO-031, part 1c). The agent sends its vault ops on its vault channel (
HATCHLING_VAULT_FD) when its cell was given one, after checking the channel's peer as it checks its connection's. With a keeper running, the broker denies a vault op sent to it, and the keeper no longer takes a cell's op from the broker at all (Request::Cellis gone): a compromised broker reads no secret a cell fetches or stores, and can't ask for one in a cell's name. - The spawner hands each agent cell's vault channel to the keeper (WO-031, part 1b). The broker gives the spawner the keeper's channel at start and keeps no copy. A connected agent cell, the broker's own or a scheduled run's, a subagent's or a chat's, now gets a vault channel, named with t...
Hatchling 0.4.0
Added
-
hatchling-broker clean [--dry-run]: removes a dead broker's leftovers, a cell's staging directory ($TMPDIR/hatchling-staging-<pid>-…, whose broker is gone) and a run's directory (<scheduler dir>.runs/run-…), as each start now does too, and logs each.--dry-runlists them and removes nothing. It refuses while a broker serves the socket. It never touches your data: no symlink is followed, no mount is entered, and every configured state directory is kept whole, wherever it is. What Hatchling keeps on disk says which files are your data and which are disposable, and how to back up, restore and uninstall (WO-012). -
internal:host:portinHATCHLING_EGRESS_ALLOWLIST(and a coding CLI's vendor API): a destination the operator means to be on their own network, allowed to resolve to a private or loopback address; otherwise an ordinary credentialed HTTPS destination (WO-023). -
The Machine view, for whoever runs the machine:
hatchling-host, givenHATCHLING_OPERATOR_SOCKET, also servesmachine.htmland carries its channel to the broker's operator socket, printing a second address with the operator token. It shows the broker's version and uptime, the cells running, the connections open and the recent egress decisions, refreshed every two seconds, and metadata only. CI opens it in Chromium beside the app (WO-022, part 6). -
HATCHLING_OPERATOR_SOCKET: a socket for the people who run the machine, apart from its users. It speaks its own frames, so a user's socket can't answer an operator's query and the operator's can't take a user's action; it has its own token in<socket>.token; and its one query so far,broker, answers with the broker's version and uptime (WO-022, part 1). -
The operator's
cellsquery: every cell running, oldest first, with its id, kind (agent, run, subagent, skill or harness), start and limits, and never its task, input or output (WO-022, part 2). -
The operator's
egressquery: the broker's last 200 egress decisions, with when, which cell, the destination and a verdict from a closed set, never a request's path, headers or body, or its answer (WO-022, part 4). -
The operator's
sessionsquery: every API and UI connection open, oldest first, with its id, socket and when it was admitted, never what it said (WO-022, part 3). -
The README's "Using the app": build the UI, start
hatchling-host, open the address it prints, and chat with the agent. CI runs it as written and checks the agent's answer on the page in Chromium (WO-021, closed). -
Chat: the broker answers each message sent on the local channel, through the API or the ingress, with an agent run in a fresh cell, given the message as its task, one at a time. Its grants are the API caller's, and it runs only when they include
channel-send:local(WO-021, part 4). -
HATCHLING_UI_SOCKET: a second local API socket, for the frontend host to carry a browser's channel to. It serves the same protocol asHATCHLING_API_SOCKET, and everyhelloon it must also carry the UI token, which the broker mints at each start into<socket>.token, readable by its owner alone (WO-021, part 1). -
The frontend starts live when
hatchling-hostserved it: the address the host prints carries the UI token as#hatchling-ui=<token>, which the app takes and clears from the address before its own routing reads it, then speaks to the broker over a WebSocket to the host's/ui, its hello carrying the token. Opened any other way, it runs its scripted demonstration as before. The app's main chat is the broker'slocalchannel on the wire (WO-021, part 3). -
The local API reports the agent's status on the stream: working once a message is queued for it, with the message's first line as the detail, and idle when its reply arrives (WO-021, part 4).
-
hatchling-host, the frontend host: it serves the built UI on127.0.0.1(HATCHLING_HOST_PORT, default 7340;HATCHLING_HOST_UInames the build) and carries the browser's WebSocket to the broker's UI socket (HATCHLING_UI_SOCKET), frame for frame. It prints the address to open, with the UI token in the URL's fragment. Its WebSocket frames are read strictly: whole, masked, bounded by the broker's own frame limit, text, close, ping and pong only. Its files are named only by plain segments and served only when regular, never through a symlink. A new crate (WO-021, part 2). -
Testing code that deletes: how a mutant of the new removal code deleted a test machine's WSL home during WO-012, and the rules since: two independent guards per escape, tests that delete only what they made, a bubblewrap sandbox for the tests and mutation runs of such code, and canary files (WO-012).
Changed
- One broker per socket, and its helpers end with it. A serving broker holds a lock beside its socket (
<socket>.lock) for its life; a second broker on the same socket refuses to start. A run's or a skill's helper process now watches the broker that started it, by pidfd, and ends, with its cell, the moment that broker does: a broker killed withSIGKILLused to leave its helpers and their cells running until they ended on their own (WO-012). - The redaction audit log confirms no guesses. A redaction is recorded by tags, HMAC-SHA-256 under a random key the broker makes at its first start with the gate on and seals under the master key in the redaction store (
raw/audit-tag-key), instead of unkeyed SHA-256 hashes, which let anyone holding the log test a guess at a short secret or phrase.redactions showprints the tags, and an override names its flagged form by tag. Records and overrides from before keep their unkeyed hashes, and those overrides still apply;showmarks such a record.rekey-vaultreseals the key with the rest of the store, so tags stay stable. Breaking,hatchling-broker:JudgedandOverridecarrytag/flagged_tag, with their hash fields optional;output_gate::buildtakes the master key. (WO-011) - The broker wipes its copies of held secrets. The output and artifact gates read every held secret for each message or artifact they judge; each secret, and each form they look for (hex, base64, normalized), is now wiped when the gate is done with it. A fetched secret's reply, and each request frame (a stored secret's), are made at their final size and wiped once sent or handled. A test allocator scans every block these free for a canary secret in any of its forms. Breaking,
hatchling-core:VaultOp::StoreandVaultResult::Fetchedcarry theirsecret_b64as aSecretB64, which is wiped when dropped and never shown byDebug; the wire format is unchanged. Breaking,hatchling-broker:held_secret_valuesreturnsZeroizingvalues. (WO-011) - Security keys can be made to ask for their PIN. With
HATCHLING_REQUIRE_KEY_PIN=true,vault-keyring enroll-keysandadd-keyenroll each key to ask for its PIN, as well as a touch, each time it opens the vault, and a hardware-key vault serves only if every key does (one that asks only for a touch is named; remove it and add it again). The key enforces it: under its PIN it answers from its verified-user secret (CredRandomWithUV), so a client that skips the PIN derives nothing that opens the vault, and the setting is bound into the key's wrap, so it can't be edited out of the keyring.vault-keyring statusshows what each key asks. Breaking,hatchling-vault:Factor::Fido2andKeyInfohave a field more; theAuthenticatortrait hashmac_secret_verified(with a default), andAuthenticatorErrora variant more. (WO-011) - Keyring edits are locked, and none rests on a stale read. Every save takes an exclusive lock beside the keyring (
<keyring>.lock) and writes only if the file is still what that keyring read, so two processes editing at once can't write back a spent backup code or TOTP step: the second is refused with "changed since it was read", and nothing is written. A failed save leaves the keyring as it was. (WO-011) - The vault wipes the copies of secrets it frees. File reads are sized once, so a growing buffer never frees a partial copy; the master key file is read into a fixed buffer;
checkandholds_envelopesread only a file's first four bytes; the TOTP seed, the keyring's text, and typed and shown backup codes are wiped when dropped. A test allocator scans every block the vault frees for a canary secret while it migrates, checks, unlocks and recovers. A TOTP factor'sDebugnever shows its seed. (WO-011) - A cell killed by seccomp hands the host no core dump. Every cell program is held to a core limit of one byte, soft and hard, before its filter goes on: the kernel aborts a dump to a crash handler on a pipe (apport, systemd-coredump) at exactly that limit, and writes no core file under a page. The filter refuses any change to the core limit from inside (reads of every limit still pass), and raising it takes a capability no cell holds. Before, a seccomp kill handed apport the cell's memory, any secret it fetched included, and hundreds of kills could exhaust a small machine. A broker started with a hard core limit of 0 now refuses to start cells, since 0 doesn't stop a pipe handler; raise it to at least 1. (WO-011)
rekey-vaultrotates a keyring vault's key inside its keyring, and takes no key file there. Every way in at the vault's tier is wrapped around a fresh key: the file tier's KEK, the TOTP tier's seed, and every enrolled security key, each plugged in and touched (one that doesn't answer is named, and nothing changes). Backup codes are replaced by a new set, printed once, and every store is re-sealed. A crash part-way leaves the vault refusing to serve untilrekey-vaultis run again, which finishes it. Without a keyring, a key file is still needed. (WO-003)- Raising the tier gives the vault a new key.
vault-keyring enroll-totpandenroll-keysrotate the key asrekey-vaultdoes...
Hatchling 0.3.0
The local-attacker release, first part. It closes what another local user could do to a running broker: a root broker that followed symlinks when it handed a cell its socket, set modes or made directories (WO-006), and agent sessions admitted by uid alone, so any process with the agent's uid could open one (WO-007). Every agent cell, the broker's own and each scheduled run's and subagent's, now reaches the broker over a connection made for it alone, and in cell mode no agent socket is bound. The broker and the agent must both be 0.3.0: a 0.2.0 agent can't use the connection a 0.3.0 cell gives it; the one hatchling binary is both. The broker also refuses nobody as the agent uid unless told otherwise. Rust API changes are marked Breaking below.
Added
HATCHLING_AGENT_ALLOW_NOBODY: the broker now refuses to start withnobody(65534) asHATCHLING_AGENT_UID, since every process running as nobody could act as the agent; this setting accepts it anyway (WO-007).
Changed
- Breaking: the broker's agent cell now reaches the broker over a connection made for it alone, inherited at the descriptor
HATCHLING_CONNECTION_FDnames, and in cell mode the broker binds no agent socket at all. Before, any process with the agent's uid that could reachHATCHLING_BROKER_SOCKETcould open a full agent session, and name any cell id in its hello; now the cell's session is the only one, and runs under the cell id the broker gave it.HATCHLING_BROKER_SOCKETis still read, and used when the broker serves without a cell. A scheduled run's and a subagent's cell get a connection of their own the same way: the helper that spawns the cell sends the broker its end over a channel given as the helper's stdin (taken off stdin first, so the cell inherits/dev/null), and no run socket is bound. With no run socket to fit a unix socket address, a scheduler directory may now be any length (WO-007, parts 2 and 3). - Breaking, Rust API:
hatchling-isolation'sCellSpechas aconnectionfield andCellaconnectionmethod, withCONNECTION_FD_VAR;hatchling-agent'sConfignames its broker withbroker: BrokerAddressinstead ofbroker_socket;hatchling-broker'sCellConfighasconnected, andserve_cell_connection,serve_cell_foreverandAgentCellare new;hatchling-sysaddskeep_across_exec,take_stdinandnet::inherited_unix_stream;hatchling-brokerdropsrun_launcher::RUN_SOCKET_NAME;hatchling-coreaddsPeerCredentials::of_spawned. - Breaking,
hatchling-sys:chown_ids, which changed ownership by path, is removed.hand_over_socketandshare_dir_with_cellchange ownership through handles opened without following symlinks, withdir_shareandowner_traversesdeciding what they do;create_private_dirmakes a directory exclusively with mode 0700,ensure_private_dirmakes or pins one, andchmod_socketsets a socket's mode through a handle. The workspace version is 0.3.0.
Fixed
- A connection to the broker that stays silent for 10 seconds before its hello is complete is closed, freeing its connection slot; before, it held the slot until the peer went away (WO-007).
- The broker sets its listener's mode, and a scheduled run's directories and the vault's, without looking a path up again. The listener's mode goes through a handle opened without following symlinks; private directories are made exclusively and set to 0700 through a handle, and an existing run root is fixed through a handle. The ingress and device endpoints take their owner from the broker's own uid rather than reading it back from the socket's path. A skill's verified-code copy gets a name no one can guess, so another local user can't pre-create it to block the skill (WO-006).
- A root broker no longer gives its socket's directory to the cell's user. That user could swap the socket for a symlink between spawns, and the broker's next handover, which went by path, would give the cell's user whatever file the link named. The directory now stays the broker's, with the cell's group allowed to traverse it and nothing more (mode 0710); a directory an earlier version handed over is taken back. The socket and the directory are each changed through a handle opened without following symlinks, and the socket must be a socket itself (WO-006).
- A cell's staging directory can no longer be pre-created by another local user to block or redirect a spawn. Each broker process makes one private staging root in the temp directory (mode 0700, a random name, created exclusively), and each cell's directory inside it gets a random name too; a name already taken, by a directory or a symlink, is skipped and never followed (WO-006).
Hatchling 0.2.0
The hostile-cell release. It closes the work orders the pre-launch review of the trust boundary found for a cell acting against its host: credential echoes and numeric allowlist forms (WO-009), vendor-side fetching (WO-008), schedule ownership (WO-010), socket families and inherited groups (WO-005), and per-cell resource limits (WO-004). It also makes the first five minutes tested: --help everywhere, a quickstart CI runs, and a hosted-model guide (WO-001), with an operator thinking-mode setting for models that need it (WO-020). The session protocol is v13, so the broker and the agent must both be 0.2.0; the one hatchling binary is both. Rust API changes are marked Breaking below.
Added
- A hosted-model guide covers vault import, HTTPS destinations and grants, with OpenRouter as an OpenAI-compatible example; the first-run client also accepts a hosted
host:portdestination (WO-001, part 3). - CI runs the README quickstart as written with the stand-in model and checks that the agent's answer arrives, so documentation changes cannot silently break the first run (WO-001, part 2).
- Every command now supports
--helpand-hbefore startup, with generated environment-variable inventories, including cell limits and cgroups. This covershatchling,hatchling-broker,hatchling-agentandhatchling-dev; unknown arguments print usage to stderr and exit 64 without starting work (WO-001, part 1).
Changed
- Breaking, hatchling-agent and hatchling-broker: operators can select
disabledthinking withHATCHLING_MODEL_REASONING(direct runs) orHATCHLING_SCHEDULER_MODEL_REASONING(scheduled and child runs). Unset orprovider-defaultpreserves requests; endpoints may refuse the extension. Rust struct literals for agentConfig, brokerSchedulerConfigandScheduledRunmust supplymodel_reasoning: ModelReasoning::ProviderDefaultto retain previous behavior; older serialized runs default only when the field is absent (WO-020). - An operator-delegated
HATCHLING_CELL_CGROUPnow gives every cell its own cgroup v2 leaf, holding the whole cell topids.max,memory.max(half the host's memory by default, configurable withHATCHLING_CELL_MAX_MEMORY) andcpu.max(HATCHLING_CELL_MAX_CPUS, fractional cores, unlimited by default). The first child enters before release, and cleanup kills all tasks and removes the leaf. Unset uses the documented rlimit fallback with a startup warning; configured but unusable fails closed.CellSpecandCellConfiggaincgroup;LimitsandCellLimitsgainmemoryandcpu_millis;hatchling_sys::ErrorgainsCgroup(breaking Rust struct construction and exhaustive matches; serialized configs retain defaults). See cell limits (WO-004). - Breaking,
hatchling-isolation,hatchling-sysandhatchling-broker:CellSpecgainslimits: Limits, andCellConfiggainslimits: CellLimits.hatchling_sys::exectakes a last step to run just beforeexecve, and addshold_toandLimit. - A root broker maps one uid for a cell.
HATCHLING_CELL_UID_COUNTabove 1 is refused at startup: a wider map would let the cell act as every host account it covered. An unprivileged broker could only ever map one (WO-005). - Breaking,
hatchling-brokerandhatchling-scheduler:handle_schedule_opandlist_schedulestake the caller'sCreator, andSchedulegainscreator. The schedule store reads an entry without one as the owner's. - The session protocol is v13:
EgressOp::StageandEgressOp::Read,EgressResult::StagedandEgressResult::Part,stagedonEgressOp::EgressandmoreonEgressResult::Ok(see the session protocol). The broker and the agent must speak the same version, as always; the onehatchlingbinary is both.hatchling-coreaddsfits_one_frameandEGRESS_PART_BYTES. - Breaking,
hatchling-broker:EgressConfig::allowlist_raw: Stringis nowEgressConfig::allowlist: EgressAllowlist, which holds the operator's text and its one shared resolution: build it withEgressAllowlist::new(text)(ordefault()for none), and read the text withraw(). The workspace version is 0.2.0.hatchling-egressaddsProxy::shared, a proxy over an allowlist already resolved, andProxy::allowlist. - The AGPL license gains three additional terms under its section 7, in
NOTICE.md: a work built on Hatchling keeps the credit "Built on Hatchling by Ocean Bennett", a modified version is marked as modified and doesn't present itself as Hatchling, and the name and logo aren't licensed.TRADEMARKS.mdexplains, in plain words, what forks can and can't call themselves. Every crate's docs and crates.io page point to the terms. The license tag staysAGPL-3.0-or-later.
Fixed
- TLS uses rustls 0.23.46 or newer, fixing four upstream security advisories and preventing selection of an older affected version. The crypto provider and TLS configuration are unchanged.
- A cell is held to hard limits: by default 4,096 processes (threads included), 8,192 open files and an 8 GiB largest file, set as both its soft and its hard limits just before the program starts, so it can't lift them. Set each with
HATCHLING_CELL_MAX_PROCESSES,HATCHLING_CELL_MAX_OPEN_FILES,HATCHLING_CELL_MAX_FILE_SIZEandHATCHLING_CELL_MAX_ADDRESS_SPACE(bytes, unlimited by default), orunlimitedto leave the broker's own; a value above the broker's own hard limit is held at the broker's. See cell limits (WO-004). - A cell no longer outlives its supervisor when its program clears its own parent-death signal. Every cell's pid 1 is now Hatchling's init: it holds the death signal against the supervisor, never runs the program's code, reaps every child that exits, and ends with the program's exit code; the program runs as pid 2. When the supervisor dies, so does the init, and the kernel kills everything left in the cell. The init holds no capability, runs under
no_new_privs, closes what it inherited, and can't be dumped or traced by the program it shares a uid with (WO-004). - A coding CLI's cell may make a socket pair only of Unix sockets:
socketpairwith any other family is answered "address family not supported", assocketalready was. It had been allowed whatever its family (WO-005). - A root broker's cell drops its supplementary groups before entering its user namespace. It had carried the broker's groups in, where it couldn't drop them, so any file a group of the broker's could reach stayed reachable from the cell (WO-005).
- A cell can no longer see or cancel the owner's schedules. Each schedule records who made it: the owner, through the local API, or the agent, through a session. A cell lists and cancels only the agent's, and its cancel of the owner's is answered as for an id that doesn't exist. A schedule stored before this change counts as the owner's (WO-010).
- A local API caller with no grants is refused every action. Sending a message, invoking a skill and cancelling a schedule needed the owner's whole set of grants, which a caller with none held whenever the owner held none (WO-010).
- A cell can no longer have a model vendor fetch on its behalf. The egress proxy and the harness relay read every model request (chat completions, responses, messages, embeddings) before it leaves, and refuse content given by URL, a tool the vendor runs (web search, web fetch, code execution, remote tool servers, and any tool type not yet reviewed), and the vendor's own search; a body that isn't JSON, or names a key twice, is refused too. A known vendor is open to its model endpoints only, not its batch, file or assistant APIs. Ordinary requests pass unchanged. A harness set to use its vendor's web search gets those requests refused with a 403 that says why. See the model request policy (WO-008).
- A request or an answer through the egress proxy is no longer limited to what fits one 64 KiB session frame, about 48 KiB of body. A model conversation grows every turn, so a long one stopped working there. A body that doesn't fit now moves between the agent and the broker in 32 KiB parts, up to the proxy's 1 MiB bound each way; a 512 KiB request and a 512 KiB answer cross a session whole. Each message still fits one frame, and a session holds at most one request and one answer in transit (WO-009).
- The harness relay holds at most 64 MiB of request bodies per run, across all its connections. Before, a cell could open 16 connections, each declaring a 32 MiB body, and the relay set aside each body before reading it and copied it again for the vendor: about 1 GiB of the broker's memory per run. A request past the budget is refused with a 503, the body is sent to the vendor without a second copy, a chunk size large enough to wrap the running total is refused, and a connection's place among the 16 is given back however its thread ends (WO-009).
- The broker resolves the egress allowlist once, and every session shares it. Before, each new agent session parsed the allowlist and looked up every destination's addresses again, so a destination whose DNS changed while the broker ran could be reached at addresses the broker had never checked. Now the first resolution is pinned for the broker's life; one that fails pins nothing, and the next session tries again (WO-009).
- A credential an upstream echoes back (an error quoting the request, a debugging endpoint) no longer reaches the cell. The egress proxy and the harness relay overwrite the real key with
*wherever it appears in a response's headers or body, keeping its length; the relay does it as the answer streams, through chunked framing, so a key split across reads or chunks is still found. Responses are requested uncompressed, the cell's ownAccept-EncodingandTEare dropped, and a compressed answer is refused, since compression would hide an echo (WO-009). - Egress dest...
Hatchling 0.1.2
Publishing, documentation and metadata. No change to the broker's or the agent's behavior, or to the wire protocol.
Added
- The module map shows how the crates connect:
hatchling-runtimeat the front, the trusted side (broker, vault, egress, isolation) and the cell side (agent, loop, model), and which crates are published. Every crate's crates.io page links to it. hatchling-dev auditfails the gate if a crate has no row in the module map, if the map names a crate that doesn't exist, or ifhatchling-agentdepends on the trusted side, directly or through another crate.- Releases publish to crates.io from CI when a GitHub release is published, through trusted publishing: no crates.io token is stored anywhere. The workflow refuses a release whose tag, workspace version and changelog disagree.
cargo semver-checksruns on every pull request against the crates on crates.io.- crates.io and docs.rs badges, and
cargo install hatchling-runtimeas an install option in the quickstart. - CONTRIBUTING says when new code earns its own crate, and what each published crate costs.
docs/releasing.md: how a version is cut and published, the trusted-publishing settings, and a new crate's first publish.scripts/publish-crates.shpublishes in dependency order for both CI and the maintainer, skips crates already on crates.io at the version (so a failed run is simply re-run), and in CI skips and names crates new to crates.io.scripts/setup-trusted-publishing.shmakes every published crate trust the release workflow through crates.io's API, with a token scoped totrusted-publishingonly, read without echoing; it skips crates already set up.- crates.io pages:
hatchling-runtimegets a README written for crates.io (the logo, why Hatchling is different, install, the security model in brief, links), and every other crate's page links to it and to the repository.
Changed
- Keywords and categories are per crate, each describing what that crate does:
hatchling-runtimecarries the headline terms, and every other crate its own (model providers, tool-calling, cron, signed skills, seccomp, FIDO2 and so on), so each reaches a different search and leads back to Hatchling.
Install
cargo install hatchling-runtime
hatchling --helpOne hatchling binary is the broker (hatchling broker) and the agent (point HATCHLING_AGENT_BIN at it). The quickstart takes you to an agent answering from inside a cell. How the crates fit together: the module map.
This is the first release published by CI through trusted publishing: no crates.io token was involved. The known limitations from 0.1.0 still apply; v0.2.0 starts closing them.
Hatchling 0.1.1
The first release on crates.io. No change to the broker, the agent or the wire protocol.
Added
hatchling-runtime, the crate that installs onehatchlingcommand:hatchling brokerandhatchling agentrun the broker and the agent with their existing command lines. Started as a cell's/agent, it is the agent, soHATCHLING_AGENT_BINcan point at it too (WO-016).- The shipped crates are published to crates.io; development and test crates are not.
- CI packages and verifies every published crate on each pull request, and runs the real-cell suites for the broker's spawner helpers through
hatchlingas well ashatchling-broker. - This changelog, and a milestone per planned version: v0.2.0 (what a hostile cell can do), v0.3.0 (what a local attacker can do), v0.4.0 (what a security review asks for next).
Changed
- Every crate shares the workspace version, and depends on the others through
[workspace.dependencies], so a release bumps one line. cargo denyexempts our own published crates from the license allowlist by name; a third-party AGPL dependency still fails it.
Install
cargo install hatchling-runtime
hatchling --helpThen follow the quickstart, with hatchling broker in place of hatchling-broker, and HATCHLING_AGENT_BIN set to the hatchling binary's path. Linux 5.11+ with unprivileged user namespaces, or WSL2.
The known limitations from 0.1.0 still apply; v0.2.0 starts closing them.
Hatchling 0.1.0
The first public release of Hatchling: personal AI agents you actually own.
Hatchling is an always-on agent that lives on your machine, thinks with the model you choose, and works while you're away. It's built so it cannot see your keys, read your files, or reach the internet unless you granted exactly that. Everything the model drives runs in a kernel-isolated cell; the broker outside it holds the keys, the network and the schedule.
What's in 0.1.0
- Cells. Every agent runs in its own Linux user, mount, PID, network and IPC namespaces, under a seccomp-BPF allowlist, with every capability dropped. There is no unsandboxed mode: if the kernel can't build a cell, Hatchling refuses to start.
- A vault the agent never reads. Credentials are sealed at rest (XChaCha20-Poly1305) and unlocked by key file, authenticator codes or a FIDO2 security key, with backup codes. The egress proxy swaps a surrogate for the real key at the boundary, so the agent can use a service without holding its key.
- No network by default. A cell reaches the outside only through the egress proxy, per grant, against an exact-match allowlist, over TLS for remote hosts.
- Your model. Any OpenAI-compatible endpoint: Ollama, llama.cpp, LM Studio, vLLM, or a hosted API.
- Coding harnesses. Claude Code and Codex CLI, pinned to a SHA-256, run inside cells.
- Scheduling, memory and goals. Cron and one-off tasks in explicit timezones, each with exactly the grants it was created with; broker-owned memory, goals and a digest feed.
- Skills, artifacts and spaces. Signed skill packages in their own cells, artifacts inspected at the boundary, and shared spaces with members and devices.
- A local API. A Unix-socket JSON API with a generated JSON Schema: docs/local-api.md.
Get started
Linux 5.11+ with unprivileged user namespaces, or WSL2 on Windows. Build from source; the quickstart takes you from git clone to an agent answering from inside a cell, with a stand-in model, so no API key is needed.
git clone --branch v0.1.0 https://github.com/undergroundrap/hatchling
cd hatchling
cargo build --releaseStatus
Hatchling is pre-1.0: the wire protocol and configuration can still change between minor versions.
- Known limitations. Our own pre-launch review of the trust boundary found gaps, and they're published, not hidden: Known limitations. Each one is a work order with an open issue.
- Root. The broker runs as root so each cell gets a uid of its own; running without root works with reduced features.
- Frontend. The TypeScript UI runs on scripted data; wiring its live adapter to the local API is open work.
- macOS. No backend yet; the isolation layer is a platform-neutral trait.
Held to a high bar
1,983 Rust tests and 397 frontend tests, with real-kernel cell tests that fail rather than skip in CI. Mutation testing with a kill floor of 1.0. unsafe confined to one crate. Strict clippy, cargo deny, a written review of every dependency, and CI on Rust 1.98.1 and the 1.85 MSRV.
Get involved
- Work orders: WO-001 to WO-012 are open issues with full specs, most of them security hardening. Comment on one to claim it.
- First PR: good first issues.
- Questions and ideas: Discussions.
- Security reports: privately, per SECURITY.md.
License
AGPL-3.0-or-later, with a commercial license for organizations that need other terms: see oceanbennett.com/hatchling.