Repository navigation
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, re-seal every store, delete the file tier's KEK file and replace the backup codes, so a copy of the keyring, KEK file or codes from before opens nothing. Breaking,hatchling-vault:confirm_totp,enroll_hardware_keysandenroll_single_hardware_keyreturn aRotatedfor the caller to finish (rekeyeach store, thenKeyring::finish_rotation); the keyring format is 3, which older builds refuse. Breaking,hatchling-broker:run_rekey_vaulttakes an optional key file,run_keyringthe broker'sConfig, andenroll_totp,enroll_keysandcommandthe store list. (WO-003) rekey-vaultre-seals every sealed store (the secrets, schedules, artifacts, spaces and redaction records), where before it left all but the secrets under the old key, and prints a line per store. It checks every store first, so an old envelope or a plaintext file stops it before anything is touched. Breaking,hatchling-broker:rekey_vaulttakes the store list and returns a report per store. (WO-003)- A keyring vault started without its keyring refuses to serve, where before it minted a fresh plain key and served with it. Adopting a keyring, and each keyring unlock, records it beside the key file; a start without
HATCHLING_VAULT_KEYRINGthat finds that record, or finds sealed files and no key, stops and says to set it. A new vault still gets its key.hatchling-vaulthasholds_envelopes. (WO-003) - Breaking, the vault's files: run
hatchling-broker migrate-vaultafter upgrading. Every sealed file (the secrets, schedules, artifacts, spaces and redaction records) is now a version-2 envelope that authenticates its store and its name, so a file renamed or moved into another store fails to open, as a tampered one does. The broker refuses to start while any store holds an old envelope, naming the files;migrate-vaultnow re-seals every store, one file at a time (re-run it if it's interrupted), where before it sealed only the secrets' plaintext. Breaking,hatchling-vault:migrate_plaintextismigrate, and it,rekeyandSealedVault::open_intake aStore. (WO-002) - The egress allowlist refuses internal addresses. A destination (other than
local:) that resolves at startup to a loopback, private, shared, link-local, unique-local, unspecified, multicast or cloud-metadata address now fails the allowlist, naming the entry and the address; an IPv4 address mapped into IPv6 counts as the IPv4 one. If one of yours is meant to be internal, write itinternal:host:port. (WO-023) - Breaking,
hatchling-broker:ApiConfighas aui_socketfield. The workspace version is 0.4.0.
Fixed
- The frontend host serves only its own pages. It refuses a request not addressed to
127.0.0.1:<port>orlocalhost:<port>, and a WebSocket upgrade whoseOriginisn't one of its own pages. Before, another site open in the browser could open the UI or operator channel (it never had the token, but it reached the broker's decoder and its connection limits), and a rebound name could fetch the UI's files. - A security key's PIN isn't shown as it's typed, and is read into memory that's wiped. So are the broker's UI and operator tokens.
- What a client or cell sends can't forge a line in the broker's log. An error quoting a frame the broker couldn't parse is logged escaped.
- A skill run's directory in the temp dir has a random name, so another user can't make it first and block the run.
- The local API takes its socket's owner from the broker's own uid, rather than reading it back from the socket's path after binding (WO-021, part 1).
- What the broker tells a cell stays generic. A coding-CLI run that fails on the broker's side (its binary unreadable or not the pinned one, a file that can't be staged, the Codex host copy, the network set-up, the work tree unreadable) now reports a fixed sentence; the host paths, the broker's process id and the OS error stay in the broker's log. A malformed hello is rejected with a fixed sentence rather than the parser's message.
- Strings a cell or an API client chose are logged escaped, so a newline in one can't forge a log line: a refused egress destination, a malformed skill id, a refused delivery target. A skill id holding a control character names no skill.
- The operator's
redactionsreview shows control characters, terminal escape sequences among them, as escapes: the text it prints was chosen outside. - Memory names read back from the store are checked as names given are.
- A coding-CLI run's directory gets a name no one can guess, so another local user can't pre-create it to block the run.
- The coding-CLI relay's policy refusal says
403 Forbidden, not403 Service Unavailable.