Repository navigation
levain 0.10.0
Added
-
The roster kernel computes health per kind and the Fleet card draws it grouped. Each shown roster row carries
fleet_group(resident/on_call/roundtable, the roundtable's seats fold into one line) andhealth: {tone, text, at, memory}: only a resident member whose silence is a signal can be analarm(no episodes in 24 h, write path unavailable, or adigest.last_wrap_atolder thanwrap_every, resident + stateful only); an on-call member's last call, failed or not, is a neutral fact. A roster is no longer cut at the list default of 10 rows. Health is derived at render against the cockpit's clock, never stored; a resident whose evidence is missing or unreadable (no wrap time, no liveness data, a badepisodes_24h) readsunknownwith the reason, neverok; an unreadable or zerowrap_everyand a non-on-call roundtable member are refused. -
The cockpit contract gains
hero,briefandwidgetsregions, a zone grid and reading tokens (stilllevain.cockpit/2; a current provider registers and renders unchanged). The hero holds onereadingand oneambient; the brief holdslines andbriefs; a widget is one answer, so alistthere must declareface="count". A zone panel takessize,spanandtabs(same-zone panels sharing atabstitle are one card), and the manifest'sregionscarriescardsbeside the flatpanels. The legacyheaderregion is normalised at registration (a reading or ambient to the hero, a line or brief to the brief, anything else tooperate) andregions.headerstays as a derived compat key. Areadingmay send asymbolfrom a fixed token set and a sub-reading adetail; the band tokens are named in the contract. -
A list panel may carry
compose: {verb, fields}, and the inbox compose draws on the list's own card.ProviderSpec(compose=...)names a verb the panel offers and typed fields of the form kind (text,recipient,date); registration refuses a verb the panel does not offer (or an empty one), an unknown field type, a recipient without itsenumor with a blank or repeated id, more than one recipient field, a recipient whose facet orsourcethe panel does not declare, or a compose on anything but a list or on a count face; once the verb registry is attached (at attach, at every later registration and on rediscovery) it also refuses a verb the registry does not hold, a row-bound or job verb, and a field outside the verb'sfields. The declaration is stored detached, covered by the panel etag, and replaced (or refused, offering nothing) on rediscovery. The panel head carries the declaration. The page draws recipient fields as chips (none selected sends no value for the field: "none selected = everyone", said in words on the card; a chosen value that trims to nothing refuses the send), sends through the panel's own verb to that panel only (a panel that no longer offers it refuses; another panel's offer of the same verb is never used), reads unavailable on a writable surface whose panel does not offer the compose verb, refuses a field type it cannot draw visibly (the panel reads unavailable with the reason, never a dropped field), and draws every row of a list that composes to recipients assource -> for(everyonewhen the row names no reader), its meta after the address. The facetforis registered (readers, comma-joined). -
GateHandle(levain.autonomic.handle). A process holds one handle over its currentEfferentGate.swap(manifest, binding_catalog, aggregate)builds a new gate on a frozen copy of the catalog and publishes it in one step; pending holds, receipts and the journal carry across.FireDispatcherandChainExecutorread the handle once per admission, so an admission in flight completes on the gate it started with. -
Every cockpit signing key states its custody, and
levain cockpit key createmakes a laptop-presence key on the hardware you already have. Levain never requires bought hardware; a FIDO key is an optional upgrade. Eachenrollink of the authority log carriescustody, one oftouch-id,passphrase,tpm,fido,windows-hello,fido-attestedorunstated, signed with the rest of the record;levain cockpit keysprints it beside the class on every row, andenroll/ledger inittake--custody(default: the key's sidecar). It is a declaration by the enrolling key, never a proof, and nothing here proves it: the verifier cannot tell a key made to require a touch from one made without (the Secure Enclave provider macOS ships echoes the presence flag it is handed), so a touch rests on how the key was made. The replay holds each declaration to the key types it can be true of:touch-id,fido,windows-helloandfido-attestedneed ansk-key,tpma plainecdsa-sha2-nistp256,passphrasea plainssh-ed25519orecdsa-sha2-nistp256. A laptop-presence key must state a real custody;unstatedis for display-phone and presence keys and for a keyledger initcarries over from 0.9.x.fido-attestedis refused until attestation at enrol is built, with the governed install;windows-hellois a reserved label (levain runs on macOS and Linux). What each custody guarantees:touch-id: a fingerprint per signature on a key that cannot leave the Secure Enclave, as created bylevain cockpit key create. Declared, not proven: a Mac key carries no attestation.passphrase: a typed passphrase per signature, and no defence against same-uid malware, which can keylog it or copy the key file. The lowest class, and the one that works on any machine.tpm: a non-exportable key, and no presence.fido: a touch on a key you plugged in, custody declared.fido-attested(later): the only class proven.
levain cockpit key create --custody touch-id(macOS) makes a Secure Enclave identity with Touch ID required (sc_auth create-ctk-identity -k p-256-ne -t bio; levain offers no way to make one without), confirms the identity is listed with protectionbiobefore anything else happens, and fetches its key reference withssh-keygen -K -w /usr/lib/ssh-keychain.dylib; any failure or interrupt once the create has started deletes the identity that run made (told apart by listing the identities before and after, deleted by its hash, never by label, so another run's identity is never touched).--custody passphrasemakes an ed25519 key with a passphrasessh-keygenasks for on the terminal, and refuses the key if the file is not encrypted (an empty passphrase). With no--custody,key createchoosestouch-idon a Mac wherebioutil -rshows Touch ID configured for unlock andpassphraseotherwise, and prints which it chose and why. Keys go in~/.levain/keys/($LEVAIN_HOME/keys, made 0700, or--dir), each beside a sidecar naming its custody and provider, so signing with a touch-id key needs noSSH_SK_PROVIDER(one set in the environment still wins). A passphrase key is always signed from its file, never through an agent, so every signature asks the passphrase. Every signature is checked against the signing key the moment it is made, so a cancelled Touch ID prompt (which letsssh-keygenexit 0 without a signature by the key) is refused asTouch ID was cancelled or did not complete, and nothing is signed; a link that fails to verify when the log is replayed still readsthe signature does not verify. The same check refuses, at signing, ansk-signature without the user-presence flag, which the replay already refused. A hand-made key enrolled with--custody passphrasegets the same encrypted-file check; atouch-idkey whose identity is on this Mac must still bebio, andlevain cockpit keysre-checks that on its row. -
levain cockpit ledger initwrites the authority log's first links, each signed by--signing-key, a laptop-presence key of any custody (--custody, default the key's sidecar; a touch-id or fido key asks a touch per link, a passphrase key its passphrase). On a new store it enrols that key, signed by itself: trust on first use. On a store upgraded from 0.9.x it lists the keys the store held (fingerprint, class, key type, when enrolled) and re-enrols each one you confirm, the signing key included; a key that was revoked is enrolled and then revoked again, a key whose type its class does not accept (a laptop-presence key of a type no custody admits) is listed and skipped, and the rest are dropped; a carried key is re-enrolled with custodyunstated, and the signing key with the custody you state. At a terminal it asks per key; otherwise pass--confirm <fingerprint>for each key to keep and--drop-unlistedfor the rest, and with neither it refuses. The store reads initialised only once every link has landed: a run cut off part way leaves every signed act still refused, and running it again finishes the job. Ctrl-C at a prompt or during a touch ends it withledger init cancelled, saying where the log stands once a link has landed. -
levain doctorwarns while only one laptop-presence key is enrolled in the install's cockpit. Losing that key leaves nothing that can enrol another, so enrol a second laptop-presence key withlevain cockpit enrollwhile you still hold the first. Recovery after losing the only one (levain cockpit ledger re-genesis, a new authority log) is not in this build. It is a warning, not a failure: the exit code is unchanged. A cockpit journal of a format this levain neither reads nor migrates (missing, unknown, or newer thanlevain-cockpit/3) is a failing row.
Changed
-
The cockpit page draws the KE regions. A manifest that declares
hero,briefandwidgetsis drawn as the hero first (the clock column with the weather under it, the sky beside it, the mode / unit / live / jump strip above the sky), the brief, one equal-width row of 138px widgets, then each zone as a fixed 3-column grid whose rows are as tall as their tallest card (tall 540, medium 460, short 190 px) withspanandtabshonoured; side gutters are 64px and the gap 24px, one token each. Verbs are 12px neutral buttons shown on the row you are on, and tags are plain meta text, so the accent is left to the now-line, failed reviews, overdue tags and a compose's one primary button. A reading draws an inline icon for itssymboland a sub-reading with abandas an AQI pill. Theoperatezone's title is now "Work" (its id is unchanged). A manifest with no KE regions renders as before. Every panel and widget carriesdata-ck-panelanddata-ck-kind. -
The KE page draws a region's alerts, notes and every contract shape, keeps tabs and panels in step, and clears when the data goes. The hero, brief and widget regions draw a panel's banner (stale, partial, an alert), its note and its markdown, line or typed content; an alert suppresses the empty message. A status-dots widget draws up to 4 dots and 4 figures (a figure may be a whole, a fractional or a written value) and counts the rest in a "+N more" line. A card's tab row and its panels come from the same list, so a panel that cannot be drawn keeps its tab. Each region builds its content off the page and attaches it last, and a region that fails shows one error line in its own place and hands the masthead and the state line back to the static page (a failed hero also empties its weather and sky, so no stale reading sits beside the error), so one failing region no longer blanks the page or strands those nodes (the next good render recovers with no reload), and a null render clears the regions. Row verbs (spore, episode and pending-cancel rows alike) stay in the tab order and show on focus (and always on a touch screen); the edit chip and the expand control show on the card you are on.
-
The
levain runREPL no longer reads your standard credential stores by default. They are the stores the README lists under Chat with a sovereign entity from the cockpit (~/.config/gh,~/.aws/credentials,~/.netrcand the rest, and on macOS the Keychain). Chat,--taskruns and scheduled seats already denied them; the REPL was the one drive that let the entity's shell and file editor read them, on the grounds that you read every turn. You read a turn only after it ran, so a read a prompt injection asked for was already in the model's context. This is the default changing, not a new prohibition: an entity that needsgh,awsor the Keychain in the REPL sets"deny_standard_creds": falsein its.levain/confinement.json, which reopens them in every drive, as it did before for the others. -
The cockpit schema is
levain.cockpit/2. The/1kindstriage-list,metric,visualandproseare accepted as aliases oflist,status-dots,ambientandbrief, and the manifest and every panel carry the new names. Field names are kept, and v2 validates them strictly: a metric needs a non-empty label and, when it sends a value, a finite number or a string; prose needs markdown or lines, markdown must be a string, and every prose line must be an object with a string text; avisualvalue's text must be a list. A provider whose/1value was looser is refused until it sends well-formed values. -
Cockpit signing keys live in a signed authority log. Who may sign a cockpit write is a verified replay of
authority_logincockpit.db, read once per request; thekeysandpolicytables are gone.levain cockpit enrollandrevokeappend a signed link and take--signing-key: an enrolment, which records the key's class and custody (above), is signed by a laptop-presence key, a revocation by a key of at least the revoked key's class.--compromised-sincetakes an authority seq, whichlevain cockpit keyslists. A pending's fence names the log head it was issued under, so a pending still waiting when any link is appended ends STALE at its decision. -
Breaking: the journal format is
levain-cockpit/3. A 0.9.x journal migrates at first open: its keys are set aside, waiting pendings end STALE, and every signed act is refused, naminglevain cockpit ledger init, untillevain cockpit ledger initenrols the keys again, each link signed by a laptop-presence key. A 0.9.x binary refuses a migrated journal. The migration runs only under the cockpit'scockpit.lock: while alevain serve(an old one included) holds it, anylevain cockpitcommand, a read-onlylevain cockpit keysincluded, refuses and leaves the journal untouched, so stoplevain serve, upgrade, and restart it; the restarted server migrates. Alevain cockpitcommand migrates only a journal started asreported, and checks that before it writes anything. -
The Claude Code host's self-test refuses an entity whose
activation/is a symlink. The self-test checks that bash cannot renameactivation/, and puts the directory back if it was moved, by its own identity read without following a link, so a symlinkedactivation/(a link to a directory included, which was accepted before) is refused as not a directory. If the self-test cannot put the directory back, it now fails with an error naming where the directory was left, instead of reporting it only as a failed check. -
The kernel's default cockpit layout is mock-e's. Operate bands: Tray 10, Open loops 15, Recent episodes 40, Keep 50, Recent edits 60, Recent jobs 70 (16-39 are left for an embedder's inbox, fleet and stream). Mind: the Neocortex tabs card (one tab per heading, ranked from 0), Crystallized patterns 100, and a Cognition card (trace and projection history as tabs, 200-210). Health moved to
widgetsand the state line tobrief;regions.headerkeeps its shape and now carries Health. An embedder that wants its old position re-ranks against the new bands. Until the KE2 page lands, the current page draws Health in Operate.
Removed
ChainExecutor.gate. The executor now holds aGateHandle, exposed asChainExecutor.handle; read the gate withhandle.current(), once per admission. A gate kept across a policy swap goes stale, so the executor no longer hands one out.EfferentGate.sweep_timeouts. The silence-default sweep isGateHandle.sweep_timeouts, which decides each pending on the gate current for it; a sweep held on one gate decided pendings on that gate's policy after a swap had superseded it. A bare gate sweeps withas_handle(gate).sweep_timeouts(now).
Fixed
- A shell command that fails for want of a credential the floor denied now says it may have been the floor. When the standard credential stores are denied (by default in every drive, the interactive REPL included) and, this turn, a shell command that failed printed a line naming one of those stores with "operation not permitted" (as gh, docker, kubectl, aws, twine and npm report a denied read), or git printed
could not read Username for 'https://(orPassword), the turn's activity ends with one note: this may have been denied by the standard credential floor; for git, use an ssh remote if your key is in the ssh agent; otherwise set"deny_standard_creds"tofalse, which opens every listed store and the Keychain in every drive, a scheduled seat's included. A private or mistyped HTTPS URL makes git print the same, hence "may". The Keychain is denied by its services rather than a path, and a denied read looks like a missing item, so it shows up only through git's prompt. The streamed activity oflevain runshows the note too, and the chat panel lists it apart from the actions that ran. The run banner's line for the denied stores now ends "(set deny_standard_creds false to allow)". - An unreadable tool call is no longer hidden or reported as an answer. A reply holding a tool call as text and then a
finishcall ({"name":"terminal",...}then{"name":"finish","arguments":{"message":"Created x"}}) was shown as "Created x", though the terminal call never ran; such a text is now unwrapped to its finish message only when every call in it isthinkorfinish, and otherwise kept whole and flagged. A reply that is nothing but one fenced block holding call markup (<tool_call><function=terminal>...) is now flagged; markup in a fence beside prose is still an answer. So is Qwen3-Coder's<function=name>tag followed by a<parameter=...>tag or its own close, without the<tool_call>wrapper, which glm-5.2:cloud via Ollama sent once the wrapper had been consumed upstream, MiniMax's<invoke name="...">call, GLM's<tool_call>name</tool_call>for one of the entity's own tools, a GLM call cut after its first<arg_key>tag, and Kimi'sfunctions.name:0{...}once its special tokens are stripped. A function-call JSON object naming one of the entity's own tools now counts among other text too, outside code (it counted only as the whole reply), and an indented code block alone is read like a lone fence. Each agent message of a turn is now checked on its own as well as their join, so a call that is a whole message after a plan (for example after the act-now nudge) is caught, as is a call split across two messages, and so is the text a model sends beside a parsedfinish, which was never shown, even when the finish carries no message; the reply now shows that text before the finish message, or as the reply when there is none. Text beside an action that ran is not checked, since it often restates that call. A failure of the check itself now flags the reply instead of passing it, with its own notice (levain could not check the reply, so it is not treated as an answer) rather than one about the model, and the turn result and the chat API carryunreadable_unchecked: true. Afinishcall whose arguments are a JSON string (the OpenAI wire form) raised out of the turn; it is now unwrapped to its message, as are a finish in the OpenAI{"type": "function", ...}wrapper and an array of think and finish calls. A reply over 200 kB, which was not checked at all, is now searched without the Markdown parser or a JSON decode, so nothing in it counts as code and a large array of calls is not expanded in memory; such a reply that quotes call markup or call JSON in a code block is flagged too, so a headless run exits 7 for it. A JSON call is recognised whatever the order of its keys.levain run --taskexits 7 for such a reply (it exited 0) and leaves stdout empty, with--quiettoo, where the markup used to be printed as the payload; the notice and the text go to stderr. A turn result with an unreadable call is no longerok, and the chat API'sexit_codefor it is 7. The REPL's gate no longer asksapprove?over a hold that lists nothing: it can only be rejected, as in the chat panel. - The chat panel no longer offers Approve on a hold that can only be rejected. A held action whose bytes the server cannot bind an approval to was shown with an Approve button, though every call in it was displayed in full; clicking it was refused as undecidable and left the turn's outcome to be checked by hand. The server now says whether a hold is approvable (
approvablein the turn result and in/chat/session.json), and the panel shows Approve only when that istrue; otherwise it shows Reject alone and says why. - On Linux, bash can no longer reach your gnupg dirmngr by renaming one of your directories (the 0.7.1 known limit). Requires bubblewrap 0.5.0 or later (its
--permsoption); an older bubblewrap now reads unavailable, andlevain doctornames the version. Your user runtime directory (/run/user/<uid>, and$XDG_RUNTIME_DIR) is now a fresh, empty directory inside bash, holding only your gpg-agent, keyboxd and ssh-agent sockets, bound back as single files. The rename that reached a running dirmngr from the next session now finds nothing to move: measured 2026-10-12 on an Ubuntu host with a packet capture on port 53, the attack put a fresh name on the wire 63 times before this change and 0 times with it, and the host directory was untouched. The same view hides your user service manager and session bus (which bash could ask to run a command outside the sandbox), replacing the masks that did that, and every other daemon socket in that directory, including one started after bash. gpg signing andssh-add -lthrough the agents keep working. New setting,runtime_socketsin.levain/confinement.json: sockets of that directory to put back, relative to it (["podman/podman.sock"]), each of which hands bash the daemon behind it.systemd/,busandS.dirmngrare refused there. A dirmngr whose socket sits in its GNUPGHOME instead (one started where/run/user/<uid>did not exist, such as from cron before linger, and still running) gets the same treatment in bash without network: that GNUPGHOME is a fresh directory holding its files and the agent and keyboxd sockets, never dirmngr's, and the directories above it cannot be renamed. Measured the same day, same host: renaming that GNUPGHOME, or its parent, reached dirmngr from the next session (60 names on the wire each) before, and failed with "Device or resource busy" (0 on the wire) after; gpg listed keys and signed through the host's agent and keyboxd. gpg inside bash cannot--importinto such a home, and its lock files there are not shared with gpg outside bash. A GNUPGHOME that holds the entity's workspace, or sits inside~/.kube,~/.docker,~/.awsor~/.config/gitwhen the standard credentials are denied, is refused with a message naming the dirmngr to stop. A runtime directory that holds the workspace, the entity or your home (XDG_RUNTIME_DIRpointed at one of your own directories) is refused too. On GnuPG 2.4 withuse-keyboxd, bash now reaches your keyboxd (it used to start a second one on the same keyring).GNUPGHOMEnow surviveslevain run's restart, so these sockets are found for a non-default home. Known limit: an agent restarted during a session is unreachable from that session's bash.