Skip to content

levain 0.11.1

Choose a tag to compare

@phillipclapham phillipclapham released this 16 Oct 21:32
· 1638 commits to main since this release

Added

  • Answer an agent's consent request from your phone, with a passkey. levain consent phone serves a small consent page on its own loopback port (default 7428) for an https proxy you run to put on a name your phone reaches (--tailscale finds this machine's tailnet name and prints the tailscale serve line; --name names any other proxy's). From the page you read each waiting request (the agent, the tool, the request as filed, where) and answer it with "allow with passkey" or "deny with passkey". The phone signs the exact answer record; the phone port turns the WebAuthn signature into an OpenSSH one and the queue verifies it with ssh-keygen against the cockpit's authority log, as for levain consent answer. The page also makes the phone's passkey and shows the laptop commands that enrol it; a passkey never enrols itself. A new custody word, passkey, is for a display-phone key only: it allows up to T2 and denies any request, so a T3 row offers deny only and a T3 allow is refused by the server. The phone port serves nothing else (not the cockpit, not the install), and its link carries its own token, which opens nothing on the cockpit.
  • Known edges, phone answers: (1) the passkey prompt shows no request text; the page shows it and the signature covers exactly that request. (2) A passkey is made for one https name; a renamed machine or tailnet needs a new one. (3) Anyone holding the phone link who reaches the name can read the waiting requests until the phone port stops. (4) The page is served by your own user account, and levain does not confine a Claude Code or Codex session, so answer only on a phone port you started. (5) When more than one passkey is enrolled on a name and this browser made none of them, the page asks which one, and the phone's own prompt may then offer another; that answer is refused at verify, never recorded. (6) An iCloud or Google passkey syncs with its account and reports no signature counter, so a cloned one cannot be detected. (7) The phone port answers every connection that reaches it before the token is checked, so a device on your tailnet can hold connections open (each times out after 30 seconds); it reads and answers nothing without the token. (8) Cancelling the phone's passkey prompt reads to the page the same as a passkey the phone no longer holds, so the page forgets which passkey this browser made; with more than one passkey enrolled, the next answer asks which one.

Fixed

  • A deleted or moved Codex install no longer blocks every Codex prompt on the machine. Codex reads its hooks from one machine-wide hooks.json, and levain's commands there ran python <install>/activation/hooks/<hook>.py. When that install was deleted or moved, python exited 2 on the missing script, and Codex treats a hook's exit 2 as a block: every prompt in every Codex session showed "Blocked by hook" and never reached the model (measured on codex-cli 0.162.1). The commands now run python -EP -m levain.hookrun <script>, which exits 0 and prints nothing when the script is gone and turns any other failure of the hook into exit 1, which Codex shows as "Hook failed" and carries on. A missing interpreter (exit 127) or a venv without levain (exit 1) carries on too. The hooks now need levain itself importable from the interpreter they name, and they ignore PYTHON* environment variables such as PYTHONPATH (-E); levain doctor warns when the hooks name an interpreter other than its own (it never runs one to check), and levain verify-hooks runs each hook the way Codex does. Run levain update in the install Codex uses to rewrite its hooks.json; if that install is already gone, remove or replace the levain entries in ~/.codex/hooks.json (or run levain init --adapter codex from the install Codex should use).
  • Known edges, Codex hooks: (1) an interpreter of Python 3.10 or older at the path the hooks name (a venv rebuilt in place on an old Python) exits 2 on -P and blocks Codex; levain itself needs 3.12, and levain doctor warns that the hooks name an interpreter other than its own. (2) The hook pins cover the install's hook scripts and hooks.json, not the levain and anneal-memory packages the hooks import; a process running as you can change those, as it can any installed package.
  • A spore action refused because the spore moved now refreshes the view. When a Tray, Keep or loop row was moved to another list on another surface (the TUI, a second browser tab, the agent) after you saw it, compost, remove, promote, re-tier and re-route were refused (nothing was written), but the cockpit page and the TUI kept showing the old row, so the same click was refused again until a manual reload. The refusal is now "stale" on every path, and both surfaces re-read on it: the TUI rebuilds at once, the page re-reads after showing the refusal for a moment. Every row-based promote and re-route now sends the row you saw and is checked against it before anything else, so a spore moved elsewhere is refused as stale rather than promoted or moved over the other change; a re-read the page could not run at once (another editor open, a job running) is kept and runs as soon as it can.
  • Known edge: a spore's surface-at date (next) is written last-writer-wins. Two surfaces setting the date of one spore at the same moment keep the later one; nothing else on the spore is affected.
  • The cockpit keeps your place when it reloads. A board reload (the refresh button, coming back to the tab, a stream reload) emptied the page and left it at the top; it now puts the page back where you were. Re-focusing a panel's filter box after a reload, or the / shortcut on a box already on screen, no longer moves the page; / on a box scrolled out of view scrolls to it. A panel read that started before a reload can no longer repaint the newer board with its older read; when that happens the board reloads instead.
  • The hero weather tile has a refresh button. It re-reads only the weather panel; the source decides how fresh that read is, and the tile's "read ... ago" line says.
  • Known edge: a panel whose body scrolls inside itself (a long list clamped to the card's height) goes back to the top of that list on a reload; the page itself keeps its place.