Skip to content

Releases: undergroundrap/hatchling

Hatchling 0.6.0

Choose a tag to compare

@github-actions github-actions released this 03 Nov 03:11
20598e9

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. Now secret add --for openrouter.ai:443 reads 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 as search-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 with sudo from a shell with no settings runs again with all of the service's, so sudo hatchling-broker tools list just 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. doctor holds 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. doctor checks 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.sh runs 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.sh installs 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's SHA256SUMS before 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 point HATCHLING_HOST_UI at it. scripts/release-artifacts.sh ui builds it the same way.

Changed

  • The cells' user by name, and one setting fewer. HATCHLING_AGENT_UID takes a local account's name as well as a uid (HATCHLING_AGENT_UID=hatchling-cell), so a configuration file needs no id -u. A cell on the broker's own map now takes that uid as its id-map base when HATCHLING_CELL_UID_BASE is unset: the one value the broker accepted. The README's quickstart drops both the id -u and 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, and doctor runs 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.py connected at once. When the broker took a moment longer to make its socket, the first task failed with FileNotFoundError, 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 nosuid and nodev, 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 for hatchling-isolation: CellSpec has 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_PATH as 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/null or 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's exec). 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...
Read more

Hatchling 0.5.0

Choose a tag to compare

@github-actions github-actions released this 02 Nov 17:30
76003bc

Added

  • Release binaries you can check, and rebuild to the same bytes (WO-013). A version tag on main now releases it: the release workflow builds hatchling-broker, hatchling-agent, hatchling-host, hatchling-browser and hatchling for x86_64 and aarch64 Linux, with the pinned toolchain and Cargo.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 by cargo auditable; every file, SHA256SUMS included, has a build-provenance attestation, so gh attestation verify <file> --repo undergroundrap/hatchling checks 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 on main, 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.sh runs 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 given nobody, 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 secret harness-<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 allows epoll_wait and pwritev2 (the same as epoll_pwait and the pwrite64 it had), and answers sendmmsg, 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.json in <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 list shows each and whether it's still that program, and tools 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_TOOLS must 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 for hatchling-scheduler, hatchling-broker and hatchling-agent: RunRequest, ScheduledRun and the agent's Config have new fields; Chat::resuming, Keeps and Resumed::Kept take a Conversation.
  • The conversation survives a restart (WO-026, part 3). Set HATCHLING_CONVERSATION_DIR (absolute, with HATCHLING_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 for hatchling-broker: SchedulerConfig has 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_conversation in 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 for hatchling-broker: Chat::step takes 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_refine and idea_note keep ideas in the broker's build store, and build_propose asks 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, beside HATCHLING_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 for hatchling-broker and hatchling-agent: ArtifactConfig, ScheduledRun and the agent's Config have 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 is hatchling-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::Cell is 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...
Read more

Hatchling 0.4.0

Choose a tag to compare

@undergroundrap undergroundrap released this 18 Oct 17:46
22a5fd0

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-run lists 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:port in HATCHLING_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, given HATCHLING_OPERATOR_SOCKET, also serves machine.html and 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 cells query: 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 egress query: 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 sessions query: 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 as HATCHLING_API_SOCKET, and every hello on 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-host served 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's local channel 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 on 127.0.0.1 (HATCHLING_HOST_PORT, default 7340; HATCHLING_HOST_UI names 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 with SIGKILL used 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 show prints 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; show marks such a record. rekey-vault reseals the key with the rest of the store, so tags stay stable. Breaking, hatchling-broker: Judged and Override carry tag/flagged_tag, with their hash fields optional; output_gate::build takes 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::Store and VaultResult::Fetched carry their secret_b64 as a SecretB64, which is wiped when dropped and never shown by Debug; the wire format is unchanged. Breaking, hatchling-broker: held_secret_values returns Zeroizing values. (WO-011)
  • Security keys can be made to ask for their PIN. With HATCHLING_REQUIRE_KEY_PIN=true, vault-keyring enroll-keys and add-key enroll 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 status shows what each key asks. Breaking, hatchling-vault: Factor::Fido2 and KeyInfo have a field more; the Authenticator trait has hmac_secret_verified (with a default), and AuthenticatorError a 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; check and holds_envelopes read 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's Debug never 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-vault rotates 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 until rekey-vault is 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-totp and enroll-keys rotate the key as rekey-vault does...
Read more

Hatchling 0.3.0

Choose a tag to compare

@undergroundrap undergroundrap released this 16 Oct 05:51
51187ea

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 with nobody (65534) as HATCHLING_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_FD names, and in cell mode the broker binds no agent socket at all. Before, any process with the agent's uid that could reach HATCHLING_BROKER_SOCKET could 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_SOCKET is 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's CellSpec has a connection field and Cell a connection method, with CONNECTION_FD_VAR; hatchling-agent's Config names its broker with broker: BrokerAddress instead of broker_socket; hatchling-broker's CellConfig has connected, and serve_cell_connection, serve_cell_forever and AgentCell are new; hatchling-sys adds keep_across_exec, take_stdin and net::inherited_unix_stream; hatchling-broker drops run_launcher::RUN_SOCKET_NAME; hatchling-core adds PeerCredentials::of_spawned.
  • Breaking, hatchling-sys: chown_ids, which changed ownership by path, is removed. hand_over_socket and share_dir_with_cell change ownership through handles opened without following symlinks, with dir_share and owner_traverses deciding what they do; create_private_dir makes a directory exclusively with mode 0700, ensure_private_dir makes or pins one, and chmod_socket sets 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

Choose a tag to compare

@undergroundrap undergroundrap released this 15 Oct 16:02
9c1d6b3

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:port destination (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 --help and -h before startup, with generated environment-variable inventories, including cell limits and cgroups. This covers hatchling, hatchling-broker, hatchling-agent and hatchling-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 disabled thinking with HATCHLING_MODEL_REASONING (direct runs) or HATCHLING_SCHEDULER_MODEL_REASONING (scheduled and child runs). Unset or provider-default preserves requests; endpoints may refuse the extension. Rust struct literals for agent Config, broker SchedulerConfig and ScheduledRun must supply model_reasoning: ModelReasoning::ProviderDefault to retain previous behavior; older serialized runs default only when the field is absent (WO-020).
  • An operator-delegated HATCHLING_CELL_CGROUP now gives every cell its own cgroup v2 leaf, holding the whole cell to pids.max, memory.max (half the host's memory by default, configurable with HATCHLING_CELL_MAX_MEMORY) and cpu.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. CellSpec and CellConfig gain cgroup; Limits and CellLimits gain memory and cpu_millis; hatchling_sys::Error gains Cgroup (breaking Rust struct construction and exhaustive matches; serialized configs retain defaults). See cell limits (WO-004).
  • Breaking, hatchling-isolation, hatchling-sys and hatchling-broker: CellSpec gains limits: Limits, and CellConfig gains limits: CellLimits. hatchling_sys::exec takes a last step to run just before execve, and adds hold_to and Limit.
  • A root broker maps one uid for a cell. HATCHLING_CELL_UID_COUNT above 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-broker and hatchling-scheduler: handle_schedule_op and list_schedules take the caller's Creator, and Schedule gains creator. The schedule store reads an entry without one as the owner's.
  • The session protocol is v13: EgressOp::Stage and EgressOp::Read, EgressResult::Staged and EgressResult::Part, staged on EgressOp::Egress and more on EgressResult::Ok (see the session protocol). The broker and the agent must speak the same version, as always; the one hatchling binary is both. hatchling-core adds fits_one_frame and EGRESS_PART_BYTES.
  • Breaking, hatchling-broker: EgressConfig::allowlist_raw: String is now EgressConfig::allowlist: EgressAllowlist, which holds the operator's text and its one shared resolution: build it with EgressAllowlist::new(text) (or default() for none), and read the text with raw(). The workspace version is 0.2.0. hatchling-egress adds Proxy::shared, a proxy over an allowlist already resolved, and Proxy::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.md explains, 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 stays AGPL-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_SIZE and HATCHLING_CELL_MAX_ADDRESS_SPACE (bytes, unlimited by default), or unlimited to 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: socketpair with any other family is answered "address family not supported", as socket already 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 own Accept-Encoding and TE are dropped, and a compressed answer is refused, since compression would hide an echo (WO-009).
  • Egress dest...
Read more

Hatchling 0.1.2

Choose a tag to compare

@undergroundrap undergroundrap released this 12 Oct 02:12
e51927c

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-runtime at 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 audit fails the gate if a crate has no row in the module map, if the map names a crate that doesn't exist, or if hatchling-agent depends 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-checks runs on every pull request against the crates on crates.io.
  • crates.io and docs.rs badges, and cargo install hatchling-runtime as 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.sh publishes 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.sh makes every published crate trust the release workflow through crates.io's API, with a token scoped to trusted-publishing only, read without echoing; it skips crates already set up.
  • crates.io pages: hatchling-runtime gets 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-runtime carries 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 --help

One 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

Choose a tag to compare

@undergroundrap undergroundrap released this 11 Oct 19:57
758fdb4

The first release on crates.io. No change to the broker, the agent or the wire protocol.

Added

  • hatchling-runtime, the crate that installs one hatchling command: hatchling broker and hatchling agent run the broker and the agent with their existing command lines. Started as a cell's /agent, it is the agent, so HATCHLING_AGENT_BIN can 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 hatchling as well as hatchling-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 deny exempts our own published crates from the license allowlist by name; a third-party AGPL dependency still fails it.

Install

cargo install hatchling-runtime
hatchling --help

Then 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

Choose a tag to compare

@undergroundrap undergroundrap released this 11 Oct 18:57
730ae04

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 --release

Status

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

License

AGPL-3.0-or-later, with a commercial license for organizations that need other terms: see oceanbennett.com/hatchling.