Repository navigation
levain 0.10.4
The consent queue's core and Claude Code adapter as a library, signed standing policy for the cockpit, doctor's hook pins, and the cockpit's phone layout, project hues and Decisions tile. No levain command installs the consent queue yet.
Added
- The consent queue's core and its Claude Code adapter, as a library; no
levaincommand installs or shows them yet.levain.consentfiles a request for a Claude CodePermissionRequestgate, keeps it in a store under<levain home>/consent, expires it, and delivers a decision only when it verifies as a signed answer against the cockpit's authority log (the row is never the authority; a signed answer uses the reserved SSHSIG namespacelevain-consent-answer). With no verified answer the hook decides nothing and Claude Code shows its own dialog.levain.consent.adapters.claude_code.install(workspace, ...)writes the hook into a workspace's.claude/settings.json. Any request that would edit agent configuration or hooks (Claude Code and Codex settings and hook files,.mcp.json,~/.claude.json,.claude/agents|skills|commands|plugins, plugin hook files, levain's own installed code) is never answered by a policy; it waits for you. Policy answers, the Codex adapter and the cockpit's queue view are not built.
Known edges
- The cockpit's
flowconnectsurface-token id keeps its name. It is a wire contract (cockpit/verbs.py,web_server.py), so a rename needs a compat alias and is left for a later slice. - Installing the Claude Code consent hook replaces a
PermissionRequesthandler it takes for its own. A reinstall removes the earlier levain handler even when it was written for another Python, by its shape: a command of exactly two words whose second is levain's installed hook script path. A handler of your own written exactly that way (any program, then levain's script path, nothing else) would be replaced too; a handler with any other command (a third word, another script) is always kept. - An edit of levain's own installed code always goes to you. The consent hook runs levain's package, so an Edit or Write into that package (or into the interpreter's
.pth,sitecustomize.pyorusercustomize.py, which can redirect it) is treated as agent config and never answered by policy. With an editable install the package is your levain checkout, so a session working on levain itself asks you about each such edit when Claude Code would ask at all. - A plugin's own scripts are agent config only inside
~/.claude/plugins. For a plugin outside it (a local marketplace,--plugin-dir), its.claude-plugin/directory and itshooks/are treated as agent config, but a script itsplugin.jsonnames elsewhere in the plugin (such asscripts/x.sh) is not, so an edit of that script can be answered by policy.
Changed
- The cockpit is tidier on a phone and carries one hue per project. On a phone the header leads with the nav, the sky shows only rows that had events, and an empty card folds into a slim bar at the end of its zone instead of a void beside tall neighbours. The projects are your own repos: the names your install's data carries (a row's
repofacet, an ambient event'srepo), and levain ships no list of its own, so an install whose data names no repo shows no tags. Everywhere a repo shows (the sky's marks and row swatches, a tag on rows that name it) it takes one of five hues, checked for contrast and for colour-blind pairs; up to five repos never share one. A 2px stripe tops each panel of one levain subsystem (memory, decisions, the work lists). A Decisions tile glows while something waits on you, and a failed review glows red in the sky. The clock stays plain at every hour. - Known edge: a row's repo tag is read from the row's own words, matched against your repo names, not from a structured repo field on the row, so a row that mentions two repos takes the longest name it finds, and a repo named like an ordinary word tags rows that use the word. Hues go to the repos in name order, so a repo seen for the first time can move another repo's hue (the row tags follow within two seconds, the sky's marks at its next redraw, up to 30 seconds); past five repos, two share a hue. A repo stays known until the page reloads, even after it leaves the data. Word boundaries are ASCII: a name next to an accented letter still matches.
- Decisions waiting is a tile in At a glance, not a Work card. It shows how many decisions wait on you, or "Nothing is waiting on you", and a click (or Enter) opens the list in the focus view, grouped as Waiting on you, Waiting on a signature, Resolved and Outcomes, with the filter box once the list is long. An embedder that registers its own decisions source still chooses its panel's place.
- The cockpit's standing policy is now the signed authority log's. A verb's tier floor comes from the policy links in the log, replayed for each request, and no longer from a constant in the code.
levain cockpit policy raise <verb> <T2|T3> --signing-key <key>appends a raise, signed by a display-phone or laptop-presence key; a raise only ever lifts a tier, and lowering one is a loosen that only a laptop-presence key signs. After a raise, the cockpit shows and enforces the new tier on the next request, every page and manifest etag changes, a fire previewed before it is refused as moved, and a pending issued before it goes stale. A verb the broker has no adapter for (spore_touch) cannot fire once raised above C1: it is refused, naming why. - A cockpit whose authority log is refused shows every verb at T3. A log it cannot read or verify may hold a raise it cannot see, so nothing fires at a lower tier until the log is readable again. The same holds for a server whose broker is not active (a second server on the same data root, one started without
--install-class, or one whose broker failed to start) wherever a cockpit journal or anchor already exists: it cannot read the log, so it shows every verb at T3 and refuses the write. Only a log known to be empty (on an inactive server: no journal and no anchor on the data root), or a server with no broker at all, uses the shipped tiers. - Cancelling a pending honours a raise of
pending_cancel. Cancelling your own pending is C1 and another's is that pending's tier, each lifted to the floor a signed raise ofpending_cancelsets, in the decisions panel and when the cancel is decided; with the log refused, a cancel is refused.levain cockpit policy raiserefuses a verb the cockpit does not know, or one that honours no floor (token_rotate), instead of signing a raise that protects nothing. - Known edge: a request admitted before a raise commits may finish at the old tier. A captured gate finishes (DESIGN §4); closing this needs admission serialized against policy appends.
- Known edge: a direct C1 action rendered before a raise and clicked after it carries no seen tier, so it is not refused as moved: it becomes a pending at the new tier (the safe direction), or is refused
not_builtfor a verb with no adapter. - A downstream's own verb can be raised. When the cockpit server is built it records the names of the verbs it serves, kernel and downstream, with the time, in
served_verbs.jsonbesidecockpit.db.levain cockpit policy raise deploy T3 --signing-key <key>checks a verb that is not the kernel's against that record by exact membership, so a misspelling such asdeplyois refused before anything is signed; with no record it refuses and says to start the deployment's cockpit server once; a server that cannot write its record removes an earlier one, and where the data root is read-only the removal fails as well and the earlier record stays. An embedder callingpolicy_raisepasses theVerbRegistryit serves asregistry=, which is checked instead of the record. - Known edge: the record is a file under the data root, and a name planted in it can be raised. Raising only tightens, so a planted name is at worst a raise that protects nothing. A record left by an earlier server that served a verb the current one does not is the same case, and so is a second server on the same data root that waits on the lock (STANDBY): it writes its own record while the first one serves. So does a server whose broker is disabled (an install-disabled or network-filesystem broker): it records verbs it will not serve.
Security
levain doctorpins the hook files the agents run, and says what changed. It records a pin over Claude Code's hook entries (.claude/settings.jsonandsettings.local.json), the scripts under.claude/hooks/andactivation/hooks/, and Codex'shooks.json, in.levain/agent_hook_pins.json. The first run trusts what is there (a warning says so). A later run that finds any of them changed fails, naming the file and the old and new digest. The script contents are pinned, not only the settings entry, because Claude Code does not notice an edited hook script. If you made the change,levain doctor --repin-hooksrecords it. Doctor never edits your settings.levain initandlevain updatere-pin the hook files they write. After a legitimate init or update,levain doctorno longer fails "changed since pinned" until you run--repin-hooks. Each re-pins as its last act through the same pin writer doctor uses. If the pin was already broken by an edit that is not levain's, the re-pin is skipped and doctor keeps failing.- Doctor's hook pin is stricter (L3 round 1 on the pin). A pin file that exists but is unreadable, not valid JSON, has no
pinsmap or belongs to another install is no longer treated as "first use": doctor FAILS until--repin-hooks, andinit/updatenever re-pin over it. Only a missing pin file is first use.init --forceandupdateon an install that has hook files but no pin leave it unpinned (doctor's own first-run warning speaks); a first init into an empty directory is still pinned. Doctor now also pins the git repo root's.claude/settings.jsonandsettings.local.jsonwhen the install sits below a repo root, and the files a hook command names under the install (see the known edge below for which operands are followed). A command that names no file doctor can pin is named "not pinned". A component that cannot be read or hashed safely (an unreadable script, a refused activation tree, a settings file that is not JSON) fails doctor by name, and--repin-hooksrefuses instead of pinning a placeholder. The sandbox line says "configured enabled", warns unlesssandbox.failIfUnavailableis true too, and names the settings files it read. - Doctor's hook pin, L3 round 2 on the pin. An interpreter is no longer pinned: a hook command pins every file it names under the install (or
${CLAUDE_PROJECT_DIR}), so a Python upgrade is not a change andpython3 ${CLAUDE_PROJECT_DIR}/x.py(or an absolute path, as levain's own hook commands are written) pins the script; a relative operand does not (see the known edge). Settings pins now also coverdisableAllHooks,statusLineandapiKeyHelper(and the commands those name). A symlinked hook script is pinned by its target's content and its link string. The workspace's.codex/config.toml[hooks]table is pinned. A Codexhooks.jsonthat was already on disk before the firstinitis unvetted: doctor's first-run warning now lists what it pinned. The Claude Code sandbox line is no longer printed for a Codex install. Pins written by earlier builds of this branch re-pin withlevain doctor --repin-hooks(nothing shipped before this). - Known edge (doctor pin): sources it does not read. A non-boolean
sandbox.enabledstops the precedence walk at that layer. A git worktree's main-checkout settings root is not followed. A relative bare-interpreter command (python3 tools/x.py) leaves its script unpinned, and doctor warns. argv[0] is never pinned when it is a known interpreter name or a native binary and later tokens exist (a copied venv python included); every later existing file or directory operand outside the install (lexically) is named "not pinned", even when another operand was pinned; an in-root symlink is pinned by link string and target content wherever the target lives. Doctor follows an operand only when it is itself a path: absolute, or starting with~or$CLAUDE_PROJECT_DIR(braced or not), or a relative path with a slash as the command's first token. A relative operand after the first (python3 hook.py,python3 .claude/hooks/x.py), a path inside an option (--script=${CLAUDE_PROJECT_DIR}/x.py), one behind another variable ($PAYLOAD) and a directory under the install are neither pinned nor named, so a command made only of such operands is named "not pinned" while one that also names a pinnable file reads clean. With arguments after it, a first token named like an interpreter (python,node,sh,uv,npx,envand the like) is skipped as one even when it is a script under the install.envin settings is not pinned (ordinary variables change for benign reasons), nor are the files astatusLineorapiKeyHelpercommand runs beyond what the pin of that command's own file covers. - Only a Codex install pins Codex's global
hooks.json. Codex has no per-project hooks file, solevain init --adapter codexin any directory rewrites~/.codex/hooks.json(or$CODEX_HOME/hooks.json). Pinning it from every install turned a Claude Code install'slevain doctorred, with the "an agent may have edited code that runs as you" hint, as soon as a Codex install was made elsewhere. A Claude Code install now pins only its own hook files; a Codex install still pins the global file and still fails on a change to it, and a fresh Codex init still treats ahooks.jsonit did not write as unvetted. - Known edge (doctor pin): one global Codex file, several installs. A Claude Code install does not watch Codex's global
hooks.json, so a hook planted there from a Claude Code session is caught only by a Codex install's doctor. Two Codex installs on one machine share that file, and eachinitrewrites it for its own install, so the other Codex install's doctor fails until--repin-hooks. The pin follows the adapter the install attests (CLAUDE.mdwins overAGENTS.md):init --force --adapter codexover a Claude Code install leavesCLAUDE.md, so that install stays a Claude Code one to levain and does not pin the global file, and aCLAUDE.mdadded by hand to a pinned Codex install reads as a changed pin until--repin-hooks. - Known edge (doctor pin): a write racing init or update. An outside edit landing between
init/updatetaking the pre-write pin status and its re-pin is blessed by that re-pin. The window is the length of the install. - Known edge (doctor sandbox line): sources it cannot see. It reads managed, project and user settings files; it cannot see
CLAUDE_CONFIG_DIRrelocation,--settingsor server-managed settings, and the line says so. A worktree whose Claude Code settings live in the main checkout's root is not followed. levain doctorwarns when Claude Code's own sandbox is off for the workspace. It readssandbox.enabledthe way the host guard reads managed settings (managed, then the workspace's local and shared settings, then yours); none set means off..codexis now write-denied to hosted Claude Code's Bash, like.claude. A sandboxed model's Bash could create<workspace>/.codex/config.toml.- Known edge: in your own mode (skip-permissions, no Claude Code sandbox) a model's Bash edit of hook files is not stopped; doctor finds it afterwards.