Changelog · Changelogs
lockhub and lox changelog
lockhub has no stability promise before v1: pin an exact version (by image digest).
Unreleased #
Breaking #
lox runrefuses a lock whose profile has aproxy_command(the one it names, or any ofconfig.tomlwhen it names none), before the review, naming the profile and the alternative: the built-in ssh bastion ([profiles.<p>.ssh]) or a published port (#193). It only showed anunreachablerow before, and the console failed at connection time: the gateway cannot follow the program's destination, and the signed image has nossh,dockerorkubectlclient to run it.
v0.11.0 (2026-11-13) #
Supported locks: lockssh joins lox run (lockssh v0.1.0 or later).
lox run upgrades older images to locksql v0.18.1, lockmq v0.9.0,
lockfile v0.9.1, lockhttp v0.9.1, locklog v0.3.0 and lockssh v0.2.0
(family.ShippedLocks; v0.10.1 still pinned lockhttp v0.8.1 and the
others' previous releases, DavidGodefroid/lockhttp#44); lox add still
pins each lock's latest signed release. The release workflow now fails
before publishing a lox whose ShippedLocks is not each lock's latest
release. lockkit v0.19.1. Upgrade lox, then start lox run: it upgrades
the hub image to v0.11.0.
Breaking #
-
lox add(every type, both forms) andlox run(--no-starttoo) refuse a workspace that is not the user's or that group or others can write: the workspace directory,.lockfamily/and each lock's.<type>/, with one message for every type and its fix,chmod go-w <directory>(#178). Only locklog refused it before; a workspace made under umask 002 (0775) let a member of the group replace a lock's reviewed configuration between twolox runs. -
lox runapplies the company's managed configuration (#179, lockkit v0.19.1managed): it reads/etc/lockfamily/managed.tomlon the host (a file refused stops everything, exit 3), mounts it read-only at/etc/lockfamilyin every lock (with the verified signature stamp when signed), runs each lock's<tool> managed checkbefore starting it (a lock beyond the bounds is not started, the others are), and applies[lox]:auto_max/production_auto_maxcap the auto mode,allow_no_upgrade = falserefuses--no-upgrade,image_minandegresskeep a lock from starting. A change of the file alone asks nothing. Newlox managed show;lox doctorhas amanagedline. -
lox attach NAMEshows the question the console actually waits for, read in the lock's container from the question file its console keeps next to its socket (DavidGodefroid/lockkit#127, DavidGodefroid/locksql#182): only when the console does not listen on its socket, only that console's own file (its name, owner and mode 0600), at most 200 characters, escaped for the terminal (#119):lock NAME waits for: Passphrase for ...: (typed, not shown). A console that listens sayslock NAME is serving: nothing to answer; neither gives the container's state (starting, or stopped with its exit status). The list of what a console may ask is gone: it was taken for the question, and a policy answer was typed as an SSH passphrase. A console that ends under the attach makes lox attach print its next question once it restarted (#143).
Security #
-
lox run's short review lists a changed profile line's new value (#173): underOther changes since the last lox run:it showed only the old (-) lines of a profile, never the new (+) ones, so a loosening (tier = "write", a new host) was approved unseen. A changed line now shows-and+, an added one+, a removed one-. -
Two workspaces on one host (#172): their hubs share the facade socket, and a second workspace's
lox runused to serve the first one's hub, listing its tickets. The hub now answersGET /v1/hub(its key) on the facade socket;lox runandlox upcompare it with their workspace's hub key before starting anything,lox runagain whenever the hub comes back, and refuse another workspace's hub, naming its workspace and project (stop it, or run this workspace with its own XDG_RUNTIME_DIR), so the second hub is never started to fail on the socket.lox doctorreports it, and the hub's ownanother hub is serving iterror points at the shared runtime directory. e2e:ci/e2e/two-workspaces.sh. -
lox runasks the values a ticket wants typed (#187, lockkit v0.18.0Subject.Inputs): the card lists their names and a warning,yprompts for each without echo, and the values go to the lock sealed to its one-use key (ticket.SealInputs), never to the feed, the scrollback or the hub. An empty answer orEscdenies (no value typed). Such a ticket is never decided by an auto rule, and the hub denies another facade's approval of it (typed values: decide in lox run). First consumer: lockfile#9. lockkit v0.18.0 also drops the auto-mode identifiers andauto_approvefrom its status (lockkit#120).
Behaviour #
-
lockssh is a lock type (#133):
lox add lockssh(host, port, login, key or password, the key and known_hosts files in the lock's home, the allow rules,--exec),lox add lockssh NAME, andlox run, digest-pinned toghcr.io/davidgodefroid/lockssh(v0.1.0 or later). The console runs as10041, its clients group10040, with its launcher's limits; its egress reaches the profile'shost:portonly (an IP address refused); the review showsssh user@host:port, the login and the trusted host keys, and checks the key and known_hosts in the lock's home (with the command that copies them there). lox run reads its auto rules (lockssh profile rules --json: connect, run, exec, unmask), keeps them inlockssh/<instance>/rules.jsonand applies them by verb;unmaskand a productionexecare never auto, and unknown rules ask everything. A lockssh lock'sread-piiis never answered in a chat:routing.json's newlocal_risks, which the hub applies andlox doctorchecks.lox initlists the lockssh launcher and proposes the plain hosts of~/.ssh/config. e2e:ci/e2e/lockssh-agent.sh, a host agent'slockssh mcpagainst an SSH server in a container. Supported locks: lockssh joinslox run(lockssh v0.1.0 or later). -
lox runreuses the host's Vault CLI token (#124): avault: truelock may setvault_token: cli(defaultask), a change the review shows. lox run then reads the token withvault print token(or from~/.vault-token: the user's, 0600 or stricter), checks it at the reviewed Vault before each use (TTL, read capability on the lock'svault_path, the reviewed CA, no proxy, no redirect) and answers the console'sVault token for <address>:with it, without a card. An unusable token, or the console asking again within 30 s, falls back to the human with the reason. The session journal recordsvault token: cli|typed, never the value. -
lox runhas a tab per lock (1…9,Tab;0the global feed, the default) with what the lock decides on its own side, read at its console's terminal (the docker attachlox runholds, which no agent reaches; nothing goes to the hub): its plans (class, EXPLAIN summary, verdict, row bound, PII masking, the decision anddone: N rows in T ms), each statement masked from its first quote, comment or non-ASCII byte on, whatever the dialect, and every number before it (oshows it in clear, on screen only); its lock-side refusals with code and reason, identical ones folded with a counter, a loop of three raising an alert in the global feed; tickets not submitted under the lock's rate limits (THROTTLED, DavidGodefroid/lockkit#150); its policy state (approved, pending, refused: the last approved policy applies) with the change, reviewed from the tab (rtypes:review, the question is a card); its session (idle time, time left beforeidle_timeoutandmax_session, the limits in force from the console's:status, typed once per attach and oni). A badge marks a tab that wants you (#146). -
Withdraw pending or stuck tickets (#148). In
lox run,wlists every ticket pending at the hub, oldest first, with its age, per lock with how many lox run cannot verify;xwithdraws the selected one,Xall of its lock,Uall of its lock that lox run cannot verify, each after ay/N. Without the TUI:lox tickets [--lock NAME] [--error] [--json]andlox withdraw INSTANCE/ID… | --lock NAME --all-pending|--error. The hub ends the ticketwithdrawn(it frees the lock's slot at the hub), audits it with the human, the facade and the reason (withdrawn by USER in lox run), and sends the lock a denial it signs with that reason: the lock ends the plan, frees its own queue and its agent hears that the human withdrew it, instead of a request that hangs. A ticket the hub itself can no longer verify is withdrawn without a decision (audited as such). New facade routePOST /v1/tickets/{instance}/{id}/withdraw;lox audit showprints a decision's reason. Locks on a lockkit with DavidGodefroid/lockkit's #148 part end the plan as soon as the denial arrives and give reads a 1 h TTL; older ones end it on the agent's next run of the ticket. -
lox runno longer exhausts Docker's default address pools (#175): each network of a workspace gets a small subnet of its own (lockhuba /24, the others a /28), set in the compose file, from the workspace's pool (10.123.0.0/16, ornetwork.poolin lockfamily.yaml, reviewed). lox run reads the engine's networks first: a network keeps its subnet while it is free, new ones take the first free block, and two workspaces never overlap. A network the engine refuses for its addresses stops lox run with a clear message; the networks compose made before it are removed and nothing is applied.lox doctorwarns when the pool cannot hold the next run's networks, or when the engine's default address pools (lea's and other projects') have fewer than 4 subnets free, with thedefault-address-poolsfix for/etc/docker/daemon.json. Seedocs/networks.md. -
A bad locklog lock no longer stops
lox run(#177). A lock whose mount plan lox run refuses is not started, with its reason (lock <name>: not started: <reason>, in lox run's header and as alox doctorfailure, kept in the state'snot-started.json); the other locks start and are served. A locklog lock'slog_roots(reviewed, passed to its console asLOCKLOG_EXTRA_ROOTS) allows log directories outside/var/logand/run/log/journal:lox add locklog --onlywrites it for a directoryLOCKLOG_EXTRA_ROOTSlets in, and refuses any other with lox run's message, so it never declares a lock lox run refuses. -
lox runkeeps a session password per profile and endpoint (#165): a profile with twocredentials = "ask"legs (an SSH bastion, then its database) asks each leg its own password once, the prompt names the endpoint, the database leg right after the bastion's is no longer taken for a failed reconnect, and an idle reconnect reuses each leg's own password without asking. -
A console's card in
lox runshows the context lines the lock recorded with its question (#166): lockconsoles reads the question file with lockkit'sconsole.ParseQuestion(lockkit v0.17.0), takes its first line as the prompt and the lines printed above it as the context, cleaned for a terminal and capped at 16. A lock built on an older lockkit records none: its context is still read from its screen. Before, a question file with context became the card's whole prompt. lockkit v0.17.0 also raises the defaultslox doctorshows: 30 plans pending per lock and 30 new tickets per 5m0s. -
lox add/lox runhome-file checks (#167): a path into the profile's directory (such astls_ca = ".locksql/ca.pem") is checked in the copy the review applies, so a first run or a profile change no longer reports it "not found"; a key in an inline table (ssh = { auth = "key", key = "…" }) is checked, and ansshvalue the review cannot read is listed as not checked instead of skipped. -
The
lox addandlox runreview checks the files a locksql profile names (tls_ca,tls_cert,tls_key,ssh.key) in the lock's home volume, as the console account, with locksql's~/rule (one rule, documented in docs/lox-add.md): a missing file gets afilerow with the command that copies it there, a private key readable by others the command that runschmod 600on it; a present file says nothing.lox addoffers to copy a missing file from a host path, once you answery(#158, follow-up of DavidGodefroid/locksql#194). -
lox add lockfile --source sftp --known-hosts FILEno longer writes a host path the console cannot open (#174): it copies FILE to.lockfile/known_hostsand the profile names~/.config/lockfile/known_hosts, where the console sees its profile's reviewed copy.lox runchecks a lockfile sftp profile'sknown_hostsandkeybefore the start: in the reviewed copy, or in the console's container; a path into the workspace's.lockfileis said with the path to write instead. e2e:ci/e2e/lockfile-sftp.sh. -
lox runupgrades an image signed in the sigstore bundle format (#176): cosign's output names no release for it (no certificate subject), so a lockfile v0.8.x lock stayed on its first pin (its signature names no lockfile release). Its release is now the newest whose image in ghcr.io has the pinned digest (GitHub's release list, then one registry lookup per release), once cosign verified the pinned image for that very tag. -
lox run'swoverlay no longer shows again, for a moment, the tickets just withdrawn (#171): the hub's list read after the withdrawals could be one it made before them, andxthen picked a ticket already withdrawn instead of the selected one.
v0.10.1 (2026-11-07) #
Supported locks: as v0.10.0. lox run upgrades older images to locksql
v0.17.1, lockmq v0.8.1, lockfile v0.8.1, lockhttp v0.8.1 and locklog
v0.2.1 (family.ShippedLocks; the lock patches only change their texts
to name lox run). Upgrade lox, then start lox run: it upgrades the hub
image to v0.10.1.
New: lox run answers the consoles' start and reconnect questions in
question cards (lockhub#142): y/n for [y/N] and [yes/N], a typed line
otherwise (secrets as dots). The answer is typed at the console's terminal
only, never through the hub nor logged. Values typed this session are
offered again after a console's idle restart; a policy review is never
answered for the human. The header's locks line shows each console:
waiting for you, serving, stopped or not reached. lox attach stays as a
debugging fallback.
Fixes:
- A ticket holding
<,>or&(a SQL<,>=,<>,&) could never be decided inlox run: the hub's facade sent it HTML-escaped, out of its canonical form, so it showed asunreadable ticket does not verify under the pinned lock key: ticket: invalid: not in canonical formand stayed pending (lockhub#147). The facade API, the lock API and the mobile facade no longer HTML-escape, nor dolox run's and the phone CLI's requests; a decision whose reason holds these characters now verifies at the lock too. Tickets already stuck this way are stored intact: the upgradedlox runreads and decides them. lox runanswers a ticket routed to it that fails verification: refused with the reason (ticket rejected by lox run: …), so its lock ends the plan and frees the slot instead of waiting. Equal failures show as one feed line with a counter.lox attachdetaches on its own once the console serves (lock NAME serves: detached, its console keeps running), and runs docker's attach with--sig-proxy=false: closing the terminal only detaches, it never stops the console (lockhub#144).- An IP address as host (
host,ssh.host, a URL's host, the Vault address) is refused bylox add, andlox runstops before the review naming the key and the fix: the gateway answers by host name only, so such a lock failed later with a barenetwork is unreachable(lockhub#145).
v0.10.0 (2026-11-07) #
Supported locks: locklog joins lox run (locklog v0.2.0 or later).
lox run upgrades older images to locksql v0.17.0, lockmq v0.8.0,
lockfile v0.8.0, lockhttp v0.8.0 and locklog v0.2.1
(family.ShippedLocks); lox add still pins each lock's latest signed
release. lockkit v0.13.0.
New: locklog under lox run (lockhub#84, #87, #132):
lox add locklog <instance>is guided (--only,--production,--name): locklog's profile add on this host's logs,locklog init claude, then its doctor.lox initlists the locklog launcher and hints at log stores and this host's logs. Profiles takeallowed_uids. The console is capped at 2 CPUs (or the Docker host's count, if lower).lox runmounts what a locklog lock's profile reads: the planlocklog mountsderives from the reviewed copy, checked on the host, shown in the review, and bound read-only and non-recursive. A docker source's containers map lives in the state directory, is rewritten by the newlox refresh <instance>and is checked bylox doctor.- Networks and routing: a locklog lock with local sources only joins the
hub's network, with no route out; with a remote source it is on its own
locknet behind its gateway (
LOCKLOG_NETWORK), which no agent joins. Its routing defaults to tty, deck and mobile, and ahub.routingentry naming slack or telegram is refused. Local log sources are refused on an engine older than 19.03, or one whose version or rootless status cannot be read. lox runreads each locklog lock's auto rules from the reviewed copy (locklog profile rules), shows them in the review and applies them. Fail closed: a ticket whose rules are unknown (lock file not readable, instance or profile without applied rules) is always asked, never auto-approved (lockhub#138). The locklog routing line shows the facades in effect.
Fixes:
lox doctorcompared the hub's routing withhub.routinginstead of whatlox runwrites.lox doctorno longer fails a locklog lock with no applied plan; it warns to rerunlox run.lox add locklog --onlypasses the directories trimmed (lockhub#138).
Other changes:
- README and docs list locklog with the supported locks; the README's first line uses the harness-security category (lockfamily#19).
- e2e: locklog mounts and a host agent reaching a
lox runlocklog lock (a masked count, then an unmask approved at the terminal facade), with locklog's host launcher at the releaseShippedLockspins (v0.2.1).
v0.9.0 (2026-11-06) #
Supported locks: lox run upgrades older images to locksql v0.17.0,
lockmq v0.8.0, lockfile v0.8.0 and lockhttp v0.8.0 (family.ShippedLocks,
lockhub#118); lox add still pins each lock's latest signed release.
lockkit v0.13.0.
Upgrade notes (behaviour changes):
lox run(lockhub#121): in a terminal of its own, outside the agent, it starts the hub and the locks, then shows every ticket and decision live (masked) and serves decisions in auto mode by default. Reads are auto; writes and DDL are always asked; a connection is asked once per profile per session (anaskpassword or an expired Vault token is typed there and sealed to the lock); a new lockhttp endpoint is asked once. Shift+Tab switches to manual, the bell rings only for questions, and quitting fails closed.NO_COLOR, light/dark, ASCII and 80 columns are supported.lox upis deprecated.- The hub accepts lockkit version-2 tickets (kinds), signs decisions that
name the deciding rule (an auto rule's id, or human), relays a connect
ticket's sealed secret, records the rule in its audit, and refuses a
rule's approval where a human is required (
needs_human). lockkit v0.13.0 (lockhub#121). - Each lock but lockhttp reaches only the endpoints of its reviewed
profile (database, broker, source, and Vault with
vault: true), through a per-lock egress gateway on an internal network: no internet, cloud metadata or other host ports (lockhub#123). The review shows the allowlist andlox doctorchecks it. lox runupgrades the hub and lock images older than the releases the installed lox ships with, at start (lockhub#118). Each new image is resolved by digest and cosign-verified for its release tag, and lockfamily.yaml is rewritten; one line is printed per upgraded image. If verification fails it keeps the old image and says why. It asks only if something besides the images changed.--no-upgradekeeps the pins.- lockfile locks run under lockfile's shipped seccomp profile (as its own launcher runs them) instead of Docker's default (lockhub#45). The review shows the profile's digest; the copy is unchanged through lockfile v0.8.0.
Other changes:
lox auditlists the hub's exchanges (ticket, masked request, decision with its rule or human, outcome) inlox run's colours, with filters,--follow,--jsonand a pager;lox audit showdraws one exchange's masked plan, deciding rule, timings and signatures;ainlox runopens the same view (lockhub#122).- CI: macOS runs on main and nightly, not on pull requests (lockhub#117).
- e2e: the host launchers are the new releases (locksql v0.17.0, lockmq v0.8.0).
v0.8.0 (2026-11-04) #
Supported locks: as v0.7.0.
Upgrade notes (behaviour changes):
- The hub no longer crash-loops after a logout or a reboot (lockhub#112).
lox up(andlox init,lox add,lox remove) makes the facade directory$XDG_RUNTIME_DIR/lockhubbefore starting anything, owned by you, mode 0700, and repairs one Docker made as root (an empty one is removed and made again, with a note). The hub's compose file mounts it withcreate_host_path: false, so Docker never makes it again: a hub restarted while it is missing stays stopped until the nextlox up, which makes it and recreates the hub. A parent directory that is not yours, or is writable by others, is refused. The firstlox upafter the upgrade recreates the hub once. - Approving a production ticket no longer needs a typed name
(lockhub#113):
yinlox approve, the approve button on the phone or the lea deck approves it, whatever its risk. No current Lock (locksql, lockmq, lockfile, LockHTTP) sets lockkit'sflags.retype, the only case where the hub still asks for the instance name. Production stays flagged on every facade (PRODUCTIONinlox approve, red on the phone) and chat facades still never answer it.GET /v1/queueitems carryretypeand the phone'sretypeflag is set only then, so facades that read them (lox approve, the phone, the lea deck) need no change. This replaces v0.7.0's rule (every production ticket but a plain read, lockhub#105) and v0.6.0's (every production ticket, #64). lox add's production prompt and--productionhelp no longer say each approval needs the profile name typed.
v0.7.0 (2026-11-04) #
Supported locks: as v0.6.0.
The lea deck needs this version for the new retype contract (retype on
GET /v1/queue items) and for the 410 lock_removed answer.
- A plain read on a production lock (risk
read, notnever_auto) is approved without typing the instance name (lockhub#105):yinlox approve, the approve button on the phone. Every other production ticket (read-pii,write,prod-write,grant,policy-loosen,unlock, or anever_autoread) still needs the name typed, and the hub enforces it.GET /v1/queueitems carryretype: the name to type, empty when none is needed, so a facade (the lea deck) reads the rule instead of copying it. The phone'sretypeflag follows the same rule. - A pending ticket of a lock no longer declared (
lox remove, a lock renamed, or dropped fromlockfamily.yamlby hand) no longer stays in the queue forever (lockhub#103). It ends in the new final statelock_removed, audited ("event":"remove_lock"):lockhub servesweeps once at start, andlox uprecreates the hub when the lock set changes.GET /v1/queuenever lists such a ticket, and deciding, revealing or renewing it answers410 Gone, codelock_removed(it answered401).401stays for the signed lock API. lox add locksqlno longer asks for TLS (lockhub#102, following locksql#169):--tls, a TLS mode the project's files name, or a draft is written as is; otherwise the profile has notlskey and the driver's default applies.lox attachnames the three start questions a console can wait on (the policy review, the production profile name, a password) instead of claiming it is the policy review, and a detach (Ctrl-P Ctrl-Q) exits 0 with "detached from NAME" (lockhub#104).
v0.6.0 (2026-11-03) #
Supported locks: as v0.5.0.
- Grant tickets (lockhub#99): the hub knows lockkit v0.12.0's risk class
grant, one approval for a bounded read scope (tables, duration, read budget) that the lock then runs reads inside without a ticket each. A grant is queued, decided and signed like any ticket (the scope's hash is itsplan_hash) and is answered in the terminal or the deck only: never on the phone or a chat facade, andhub.routingcannot widen that.lox approvemarks a grant and shows the lock's scope lines before asking "approve this grant?"; production still needs the instance name typed. Audit records of a grant carry"risk":"grant". Ending a grant stays at the lock for now (:revoke, end of session, expiry). - lockkit v0.12.0.
v0.5.0 (2026-11-01) #
Supported locks: as v0.4.0.
The first run is brew install davidgodefroid/tap/lockfamily, then lea, then
lox add one source at a time.
- Breaking:
lox initsets no lock up and asks nothing. It checks the prerequisites, lists the data sources the project's files name as hints, writes alockfamily.yamlwith the hub alone and runslox up. The flags--yes,--manual,--prod,--credentials,--vault-path,--tlsand--no-attach, the plan screen and the manual checklist are removed, without an alias (the project is pre-v1).lox inittakes--file,--hub-image,--reviewand-v. lox add TYPE(locksql, lockmq, lockfile, lockhttp) sets one source up, guided and applied at once: its questions, the lock'sprofile add,init claudeanddoctor, the declaration inlockfamily.yaml, the short review of that lock and the console's questions at your terminal. A failure, anor Ctrl-C puts the lock'sconfig.tomlandlockfamily.yamlback and keeps the answers as a draft (add-<type>.jsonin the state directory, no secret) that the nextlox add TYPEoffers.--non-interactivetakes the answers as flags and only declares the lock.lox add TYPE INSTANCEstill only declares one (docs/lox-add.md).- Each question says what an empty answer means:
(required), or(optional: empty = ...)((optional: - = ...)when it has a default). Production is asked per lock, and there is no dev or prod question. - Vault is per lock: a lock chooses
askorvault, runs the-vaultimage variant, and names its own Vault path. The Vault address, namespace and CA file are one sharedvault:block, asked by the first lock with Vault. The token is typed at the console's start, never written.lox vault set --address URL [--namespace NS] [--ca-file PATH]writes that block. The "Not yet" note for Vault is gone from docs/lox-up.md. lox remove NAME [--purge]takes a lock out and stops its console after one review.lox upis quiet: cosign's and docker's own output shows only when a step fails (-vshows it). Its review is short, one block per lock, and--reviewshows everything and asks. The firstlox upof a workspace with no lock asks nothing.- When lea already runs, a lock
lox adddeclared reaches it at lea's next start:lea stop, thenlea.
v0.4.0 (2026-11-01) #
Supported locks: as v0.3.0. lox init drives the launcher verbs of
locksql v0.13.0, lockmq v0.6.0, lockfile v0.6.0 and lockhttp v0.7.0 or
later (lockkit v0.10.0); it runs doctor --fix --yes for locksql
v0.13.1 and lockmq v0.6.1 or later, and only the advisory doctor
before them.
- Release: each tag bumps the
loxformula of the Homebrew tap (brew install davidgodefroid/tap/lox), through a dispatch to DavidGodefroid/homebrew-tap once the archives and the image are signed (DavidGodefroid/lockfamily#8). The step is skipped while theTAP_DISPATCH_TOKENsecret is not set. lox initruns each lock'sdoctor --fix --yes --jsonagain (lockkit GUIDE.md §14), which #92 had left out: locksql v0.13.1 and lockmq v0.6.1 keep.locksql/and.lockmq/aslox upreads them. With a launcher or image older than these (asked with<lock> version, once per lock type), or versions it cannot read, it runs the advisorydoctor --jsonand says to update. lockhttp's--fixonly takes group and others' write away from its policy. lockfile's doctor still runs without--fix: its only repair wires every installed agent for your account. The plan says which doctor runs and what locksql's--fixalso does (it wires your installed agents for your account) (docs/lox-init.md). The e2e drives the launchers of locksql's and lockmq's main (the fix, until v0.13.1 / v0.6.1 are tagged) with the imageslox addpins (the latest signed releases), and checks whichever doctor ran: with images older than the fix, the advisory one and its note.lox init(#80): one command activates the Lock Family in a project. It checks Docker (with the install command for the system when it does not answer), cosign, lea and the locks' launchers; detects PostgreSQL, MySQL, MariaDB and RabbitMQ from the project's.env*,compose*.yamlandconfig/(read only; keys named like secrets are skipped and a URL keeps no password); proposes one lock per source (auto) or a checklist (--manual: lea, locksql, lockmq, lockfile, lockhttp, locklog, greyed with the reason when not possible); and, once the plan is confirmed, runs each lock's launcher verbs (profile add --non-interactive --json,init claude,doctor, lockkit GUIDE.md §14),lox add,lox up(its review unchanged) andlox attachfor each console that started. Re-runnable: declared locks are kept, new sources added, unchecked ones removed through the same review.--yeswith flags for non-interactive use. Sources on this machine are given ashost.docker.internal. lea's uid goes inallowed_uidswhen lea is found (lox addtakes it) (docs/lox-init.md). New e2e job: a compose project (PostgreSQL, RabbitMQ) set up bylox init --yes, a host agent's query and read approved, then a re-run that adds a database and leaves the other locks untouched.- Host agents reach
lox up's locks (#78): the locks' launchers (locksql mcpand the other agent commands, as<lock> initwrites them in.mcp.json; lockmq and lockfile alike) mount the socket volume up.json names, read-only, after lea's checks (docs/lox-up.md, with the threat model). up.json keeps its two-space indented layout, which they read. The e2e (phase 2) drives a hostlocksql mcpfrom plan to rows through a facade approval, and the refusals of a tampered up.json or a read-write squatter on the socket volume. - The e2e (phase 2) runs a lockmq lock too (#78): a RabbitMQ 4.1.8 broker
with a read-only user and a stream holding one message; a host
lockmq mcp(lockmq v0.5.0's launcher and image) reads it, pending, then gets the message once the ticket is approved atlox approve. lockhub's CI now runs a lockmq image underlox up.
v0.3.0 (2026-10-31) #
Supported locks: as v0.2.7.
- Breaking: the host CLI is renamed
lock→lox(#79), without an alias:lockis taken (GNOME's "Lock" in Nix, AUR, Guix and ALT, FreeBSD'slock(1)) and too generic to be found.lox up,lox add,lox import,lox attach,lox doctor,lox approve,lox mobile,lox audit verify,lox version(printslox X.Y.Z); every message, help text and error prefix sayslox. The release archives arelox_X.Y.Z_<os>_<arch>.tar.gzholdinglox; the hub image ships/usr/local/bin/lox; the source iscmd/lox, and docs/lock-up.md is now docs/lox-up.md. Unchanged: thelockhubbinary, image and service,$XDG_STATE_HOME/lockfamily,.lockfamily/lockfamily.yaml, up.json, the compose projectlockfamily-<sha8>, thedev.lockfamily.*labels and/run/lockfamily*. The Lock Family install.sh must installloxfrom this release on (lockfamily ships in lockstep).
v0.2.7 (2026-10-31) #
Supported locks: as v0.2.6.
- Builds on lockkit v0.8.1 (was v0.6.0). Nothing changes for the hub's
own API or for the locks it supports: the hub URL lock up gives a lock
(
http://lockhub:8443, up.json'shub_url) is already thehttp(s)://host[:port]form lockkit v0.7.2 and later require. A ticket the hub refuses (bad_ticket) no longer quotes the value lockkit found wrong in it (an unknown risk class). - CI's lockkit compatibility check also builds lockhub on the lockkit release before the pin (v0.8.0), against the same three sets of fixtures.
v0.2.6 (2026-10-31) #
Supported locks: as v0.2.5.
- Configuration errors no longer echo the value given (a token pasted
into the wrong key would have shown in the terminal, the hub's logs and
an agent's output): they name the key, the list entry (
hub.facades[1]) and the values allowed, never the value. Covers lockfamily.yaml (hub.image,hub.facades,hub.routing,hub.mobile.origin,require_device,allowed_uids, the slack and telegram allowlists, instance names that are not valid), the yaml errors oflock up,lock add,lock importand--pin(type errors, explicit tags, aliases to an unknown anchor, unknown or repeated keys: the line and the key path stay, and a key is shown only when lockfamily.yaml knows it or as a valid instance name underlocksandhub.routing), chat.json, locks.json and routing.json,lockhub serve --mobile-originand--facade-socket-mode,lock up --pinand--rotate,lock add --version,lock add telegram --chat/--fromandlock audit verify --lock. Still shown: valid instance names (a lowercase hex token pasted as a lock's name is one), positional arguments, and a bad value of a boolean flag (Go's flag package quotes it). lock add --hub-imageis checked before it is written in a fresh lockfamily.yaml: it can no longer make other keys there.- The telegram facade no longer lets a bot token that is not a valid URL
part (a
%, a control character) reach the hub's logs in a URL parse error; the lock keys warnings oflock upno longer quote an invalid instance name. - Error texts changed: those above, and the hub's refusal of an unknown
facade or risk class (
route), which now lists the known ones.
v0.2.5 (2026-10-31) #
Supported locks: as v0.2.4, plus locksql v0.11.2 (lockkit v0.7.1), the
first whose console takes --allowed-uids: with a lock's allowed_uids,
lea's agents reach a locksql lock too (locksql#139, lea#226). An older
locksql image with allowed_uids set refuses the flag and does not start:
leave the key off for it.
- lockfamily.yaml
allowed_uidson locksql locks too (locksql#139): lock up passes it as the console's--allowed-uids(locksql v0.11.2 or later; an older locksql image refuses the flag and does not start), so that lea's agents reach a locksql lock as they reach lockmq and lockfile ones. Same rules: uids only, each once, never root, 65534 nor the console's own uid (10001); still refused on lockhttp locks.
v0.2.4 (2026-10-30) #
Supported locks: as v0.2.3, plus lockmq v0.4.1 and lockfile v0.5.1
(lockkit v0.6.0), the first whose consoles take --allowed-uids: with a
lock's allowed_uids, lea's agents reach a lockmq or lockfile lock (lea#226).
An older lockmq or lockfile image with allowed_uids set refuses the flag
and does not start: leave the key off for it. A locksql lock still refuses
lea's agents under lock up (locksql#139).
- lockfamily.yaml
allowed_uidsper lockmq or lockfile lock (lea#226): lock up passes it as the console's--allowed-uids(lockmq v0.4.1, lockfile v0.5.1 or later), so that lea's agents, which run as the host user's uid with no account in the lock's image, reach it. Reviewed like any change; uids only, each once, never root nor the console's own uid; never 65534 (nobody); refused on locksql (no such flag yet) and lockhttp locks.lock importkeeps the uids every declaration lists.
v0.2.3 (2026-10-30) #
Supported locks: as v0.2.2, plus locksql v0.11.1 (lockkit v0.7.1), the
first locksql whose audit records of hub decisions carry hub_sig
(locksql#118, locksql#133, locksql#137): lock audit verify --lock checks
its approvals, denials and renewals of approved tickets against the hub's
signed decisions (older locksql records approvals unsigned, and does not
record denials or renewals, which show as decisions not in the log).
Upgrade notes (behaviour changes):
- Production tickets: approving one now needs its instance name typed on
every facade (#64). Upgrade the
lockCLI with the hub: an olderlock approve(v0.2.2 or older) is refused on production tickets by this hub, and thislock approveis refused by an older hub on a ticket whoseflags.retypeis set. An older lea deck (lea v0.3.1 or older) asks forflags.retype, which this hub refuses, so it no longer approves production tickets as it prompts: use this release'slock approveor the phone (where it is routed), or a lea release with DavidGodefroid/lea#222 (planned for lea v0.3.2); likewise, a lea with lea#222 needs this hub for tickets whoseflags.retypeis set. A phone page left open needs a reload to show the field. - Routing: a hub without
routing.json, or with one that lists nofacades, no longer routes anything to Slack or Telegram (#61). Underlock upnothing changes (it always writesfacades); a hand-run hub must list its chat facades.
Changes:
- Every production ticket needs its instance name typed to be approved,
on every facade that may answer it:
lock approve, the deck and the phone (#64). The hub,lock approveand the mobile web app used to ask for lockkit'sflags.retypeinstead, and only when it was set: lockkit sets it to the console's "retype values" answer (such asr), never to the instance name, so most production tickets were approved with a plainy. Chat facades still never answer production. - A hub started without
routing.json(or with one that lists nofacades) now routes tickets to the local facades only (terminal, deck, phone), never to Slack or Telegram (#61, invariant I9). It used to route and notify read, read-pii and write tickets to both chat facades, though none ran. A chat facade is routed only whenrouting.jsonlists it, aslock upwrites it; the phone stays implicit (it serves nothing without--mobile-listen). - Slack and Telegram: a message close (
chat.update,editMessageText) that fails (a rate limit, a network error, a 5xx) is retried with backoff (2 s doubling to 5 min, 15 tries, about 40 min), so a decided, withdrawn or expired ticket's message no longer keeps live buttons after one failure (#60). Every close is bounded (15 s), and the facade goes back to clicks and the queue after two retries. A message the service says can no longer be edited is not retried; Telegram's "message is not modified" (a retry after another chat failed) counts as done. A click on stale buttons is still refused (by the facade and by the hub); on Slack it also tries the close again at once, at most once per 2 s, without using up a try. A ticket decided on a chat is no longer posted again from a queue snapshot taken before the decision. - CI: phase 5 fails on a
lock doctorfailure (#63); phase 2 drops the database and role it made and checks the screen's literal (#62); tests for I9 (#58); deflaked chat-close and queue-order tests (#55, #57). - docs: design notes, the accepted deviations from the hub plan (#59).
v0.2.2 (2026-10-30) #
Supported locks: as v0.2.1. lock add now pins locksql v0.11.0 or later
(it was v0.10.0): a locksql lock under lock up needs LOCKSQL_PII_DIR.
lockhttp's devices: true is now wired, through lea's gate (lockhttp
v0.4.0 or later, lea v0.3.0 or later); the v0.2.0 and v0.2.1 notes saying
to leave it off no longer apply.
- A locksql console keeps its PII rule sets in its own home volume
(
LOCKSQL_PII_DIR, locksql v0.11.0):lock upno longer refuses a.locksql/without a rule set, and the console writes its first set,pii addand later proposals there, seeded once from the profile's.locksql/pii/(#53, #41).lock addpins locksql v0.11.0 or later. - up.json says what lea's lockhttp gate pins (
"lockhttp_gate": 1): the paired devices (top-leveldevices) and, per lock,devices,require_deviceandseal_clearas lock up passes them to its consoles, besideprofileandreveal(#51, #40). lea's gate asks the hub only fromlockhttp_gate1 on (lea#209). devices: trueon a lockhttp lock goes to its gate (its console only serves the policy), consistently in validation, docs and tests; when the devices or what a gate pins change,lock upsays to restart lea's gate (#54, #46).- CI: e2e phase 2 runs a locksql lock against Postgres next to the two LockHTTP locks: its first PII rule set is written in the console's volume, and an approved query reaches the agent with the email masked (#53).
v0.2.1 (2026-10-30) #
Supported locks: as v0.2.0 (builds on lockkit v0.6.0), plus locksql
v0.11.0 (lockkit v0.7.1), the first locksql release that takes
devices: true and reveals/renewals (lock up's --devices-file and
--facade-keys-file; lockhttp devices is still not wired, see v0.2.0).
- A
lock importorlock addrefused for a new workspace no longer leaves an empty.lockfamily/behind (#48). - Hub requests (reveal, renew): an open request still unanswered 2 s after it was given to a lock is offered again to every poll of the instance, so a request taken by an old lock process while the live one was between polls no longer times out at the facade (#49).
- CI: the e2e phases verify the hub's signer identity with real cosign and check the image the hub runs (#50).
- lock up's refusal of a group- or world-writable profile names the mode
in octal (
mode 0775) (#50).
v0.2.0 (2026-10-30) #
Supported locks #
Builds on lockkit v0.6.0. lock add pins only releases that run under
lock up (hub mode, lockkit v0.3.1 or later); the optional features need
newer locks:
| Lock | Runs under lock up (lockkit ≥ v0.3.1) | devices: true (lockkit ≥ v0.4.0) |
Reveals (reveal: true) and renewals (lockkit ≥ v0.5.0) |
|---|---|---|---|
| locksql | v0.10.0 (lockkit v0.3.1) | no release yet | no release yet |
| lockmq | v0.3.0 (lockkit v0.3.1) | v0.4.0 (lockkit v0.6.0) | v0.4.0 |
| lockfile | v0.4.0 (lockkit v0.3.1) | v0.5.0 (lockkit v0.6.0) | v0.5.0 |
| lockhttp (pre-releases) | v0.3.0 (lockkit v0.3.1) | not wired by lock up yet (the gate takes --devices-file from v0.4.0, but up.json does not carry the devices): leave devices off |
v0.5.0 (lockkit v0.6.0) |
lock up passes --devices-file and --facade-keys-file to the locksql,
lockmq and lockfile consoles only. The lockhttp console serves the policy;
the hub is asked by a hub-mode gate beside lea's proxy, which takes the
facade keys (not the devices) from up.json; that gate is not in lea yet
(docs/lock-up.md). A lock older than a feature it is asked for refuses the
flag and does not start: leave devices/reveal off for it. A lock on
lockkit older than v0.5.0 ignores the requests member of the decisions
poll: a reveal or renewal asked of it times out. lockssh has no release yet
and is not a lock up type.
Upgrade notes: lock up --rotate hub needs a hub image of this release
or later (lockhub rotate-key); once a hub key is rotated, an older hub
or lock audit verify refuses the log (key_rotated record), as an older
lock audit verify refuses one with outbox fields: upgrade the lock
CLI with the hub.
Changes #
-
First release of the
lockCLI with lock up's.lockfamily/layout, console accounts andup.jsonfor lea (#23: thelock up runs consoles lea's agents can reachentry below): lockfamily's install.sh installs the latest release, which was v0.1.1 without them. -
The paired devices' snapshot lock up mounts in the locks is 0644 whatever the umask (#39): under umask 077 the consoles could not read it.
-
CI: workflow e2e-lock-up runs the hub plan's phase 1 (locksql's TestHubPhase1 against this tree), phase 2 (lock up, lock attach, a ticket approved through the facade socket, I2 directly and through a LockHTTP gate, I11, a second lock up) and phase 5 (install.sh, lock add, lock up, lock import) end to end with released images.
-
Note for upgrades: lock up now resolves the workspace's links, so a workspace opened through a symbolic link gets a new project (and state): its first lock up is a full review again.
-
A renewal's
uncheckedinputs (lockkit v0.6.0: the lock could not check them now) are shown as such, and never allow one click. -
Hub requests (reveal, renew) and two pollers of one instance (a lock restarted while its old process still polls): a request now ends every long poll that began before it was first given to a lock, not only the first to take it; the others no longer sleep out their poll with it (a lost wake-up). A poll that begins after the request was given still waits.
-
lock up --pin sha256:<digest>restores a previous hub image by digest (reviewed, cosign-verified, written to lockfamily.yaml once applied;hub-images.logkeeps the history), andlock up --rotate hub|INSTANCE|allreplaces the hub's or locks' keys through the review (docs/lock-up.md). The hub's rotation islockhub rotate-key: akey_rotatedaudit record signed by both keys, the old key in the data directory'sretired-keys.pub, which the hub reads at start.lock audit verify --hub-key-filetakes a file of keys, oldest first (lock up'shub-keys.pub); with one key, as before. An olderlock audit verifyor hub refuses a log with akey_rotatedrecord. -
lock upno longer overwrites the per-userfacades/locks.jsonwith the current workspace's lock keys: each workspace's keys go tofacades/locks.d/<project>.json(with the workspace's path) andlocks.jsonis their union (one hub per user, many workspaces), rewritten under a lock. An instance name two workspaces pin with different keys, one key on two instances, or another workspace's file or key that does not parse is left out, andlock upwarns, naming the workspaces and files: clear values of those tickets cannot be shown until one is renamed, or the file of a workspace that no longer exists (moved or deleted) is deleted. After the upgrade, runlock uponce in each workspace; alockbinary of an earlier release still overwriteslocks.jsonwithout the lock. -
A lockkit compatibility check (
scripts/compat.sh, CI jobcompat): lockhub built on the pinned lockkit, its ticket codec checked against the signed ticket fixtures of the pinned lockkit, of the release before it and of the newest release (LOCKKIT_FIXTURES). A build on the release before the pin (compat.sh prev …) is manual: lockhub uses lockkit v0.6.0's API. -
Mobile facade: optional Web Push (
hub.mobile.push: true→lockhub serve --mobile-push; off by default). When a ticket the phone may answer becomes pending, the hub sends a payload-less push (no ticket content at the push service;TTL,Urgency: high,Topic, at most one round per 30 s) with VAPID (RFC 8292, ES256; P-256 keyvapid.keycreated once in the hub's data directory) to every subscription of an active device. Endpoints are restricted to the known push services (FCM, Mozilla, Apple, WNS: https, default port, public addresses, no redirects); subscriptions are tied to the device session (/m/v1/push,/m/v1/push/subscribe|unsubscribe, Origin and custom header), dropped on revoke and on 404/410. The web app gets an "Enable notifications" button; the service worker shows a generic notification.lock upreviews push and gives the hub itsegress-hubnetwork for it. -
lock up runs consoles lea's agents can reach, and tells lea how (docs/lock-up.md):
- lockfamily.yaml lives in
.lockfamily/(lea mounts it read-only in its agents' containers); a legacy./lockfamily.yamlis still read, with a warning. lock import finds*/.lockfamily/lockfamily.yaml(or a legacy*/lockfamily.yaml). - After applying, lock up writes
.lockfamily/up.json: the compose project, the hub's network and key, each lock's socket volume (labelleddev.lockfamily.role=socket, which lea checks before it mounts one) and clients group. - Each console runs as its image's console account, with a terminal
(
tty,stdin_open): it asks its policy and the credentials it does not take from Vault there;lock attach INSTANCEanswers it. Its own profile directory (<workspace>/.<type>) is never mounted: lock up snapshots it (no links, the user's own files only), shows its fingerprint in the review, and copies the reviewed archive into a root-written volume, read-only in the lock (at the same path for locksql and lockmq, whose socket name derives from the project path).profile:(optional) names the profile; lockfile's defaults todefault. - The hub's public key reaches the locks in the
hubkeyvolume, written by root, read-only in the locks (locksql refuses a key file another account could rewrite). - lockhttp: the hub is asked by the gate, which lea runs beside its proxy, not by the console (lockhttp#11). lock up runs the lockhttp console (policy only, no hub network) and makes the gate's own volume and instance key; lea's gate uses them.
- lockfamily.yaml lives in
-
lock add pins the latest release (or
--version vX.Y.Z) of the lock, and of the hub for a new workspace, by digest, once cosign verified it was signed by that repository's release workflow for that tag.--imagestill pins one by hand. -
Mobile facade (#10): an installable web app (embedded, no build step) on its own listener, published on host loopback for the human's TLS reverse proxy or tunnel (
hub.facades: [mobile],hub.mobile.origin). Phones pair withlock mobile pair(one-time code, WebAuthn registration with user verification, fingerprint confirmed at the terminal);lock upreviews paired devices and pins them at the locks that setdevices: true(--devices-file; their image needs lockkit ≥ v0.4.0), withseal_clear(clear screens sealed end to end to the phones) andrequire_device(locks refuse approvals of those risk classes not signed by a phone). Each decision on the phone is a WebAuthn assertion over the decision and the hash of the lock's signed ticket, checked by the hub before it signs and by the locks after (lockkit v0.4.0). Sign counters, single-use challenges and drafts,__Host-session cookie, Origin and custom-header checks on every POST, strict CSP. The phone answers every risk class butunlock. Audit records carry the device; device pairings and revocations are audited.lock mobile list|revoke;lock doctorreports devices paired since the lastlock up. -
Tickets and decisions without devices are unchanged: locks on lockkit v0.3.1 still work; pinning devices needs lockkit v0.4.0 in the lock.
-
lock audit verify --lock INSTANCE=PATH(repeatable) also verifies lock-side audit logs (lockkit'saudit.log, or its directory): the chain (lockkitaudit.Verify), and every record of a hub decision (approval: hubapproved or denied, or any record withhub_sig) against the decision the hub signed for that instance and ticket: same outcome, approver and facade, and same signature when the record has one (locksql does not recordhub_sigyet: its records are checked on the rest, and counted). A record whose hub fields do not read fails. Hub decisions the log has no record of are counted (not acted on yet, or cut). With--anchors FILE(host only, 0600) each verified log's anchor is kept between runs and the next run uses lockkitaudit.VerifyAnchored, so a log cut before it or stripped of its chain is refused; a failed log keeps its old anchor (a cut after the anchor is not seen until the next run). A second hub approval of one ticket fails. Not checked: records a hub-mode lock should not write at all (a local approval). -
lock doctorasks the hub for its queue over the facade socket: it says whether the hub answers, shows the pending caps and rate limit (lockkit's defaults: each lock 20 pending plans, 10 new tickets per 5 min, counted in memory; the hub: 50 per lock), that this quota is per lock instance, shared by every agent of the workspace, and each lock's pending tickets on the hub (warned from 80%). -
Audit records are written after the database commit, through a transactional outbox: each change stores its record in its own transaction, signed with the hub key together with its outbox row id, and the record is appended to
tickets.jsonlonce the transaction commits (after a crash, at the next start, before serving). A rolled-back change never has an audit record, and a committed one always gets one, exactly once: each record now carries itsoutboxid, ids only grow along the log, and a row replayed into the database, or not signed for its id, stops the flush instead of being signed into the log. While the log cannot be written, every change starting after the failure is refused (HTTP 503audit_down); the changes already in flight commit with their records waiting in the outbox, and their decisions are delivered to locks meanwhile (durable and signed, not yet in the log). A failed append is cut back from the log, and a torn last line left by a power loss is cut at start, so a retry chains the record once.lock audit verifyreads the database as of one instant, then the log, so it can run against a live hub; it accepts a decision whose record is still in the outbox, says how many records await the log, and reports replayed or foreign outbox rows. An olderlock audit verifyrefuses a log withoutboxfields: use this release's. Ahub.dbrestored from a backup older thantickets.jsonlmakes every new outbox row look like a replay, and the hub refuses to start: restore both together (or setaudit_outbox's row insqlite_sequencepast the log's lastoutboxid). Not detected: an outbox row deleted from the database by someone who can write it, before its flush, for a record other than a decision (decisions are cross-checked). -
GET /v1/helloalso answersticket_versions(the ticket schema versions the hub verifies: lockkit'sticket.Version) andhub_key_id(lockkitticket.KeyIDof the hub key). Unsigned like the rest of the hello: for display and diagnosis only, never to pick or pin a hub key (a lock trusts only the key it pinned). An N-1 version appears once lockkit verifies two ticket versions. -
A rotated Slack or Telegram token (
lock add slack/telegramagain) is now a changelock upreviews and applies: the summary carries each chat secret's identity (a short hash, never the token), so the hub's config label changes and compose recreates the hub, which reads its tokens at start. Before, the hub kept the old token until it restarted.lock doctorwarns about a token rotated since the lastlock up. The firstlock upafter upgrading with a chat facade shows the newhub secretlines once, asks for a yes, and recreates the hub. -
Clear values on demand (Ctrl-O) and re-approval of expired tickets (decision H1).
- Hub requests: the hub signs a
revealorrenewrequest (lockkitticket.HubRequest), hands it to the lock in itsGET /v1/decisionslong poll (newrequestsmember, waking the poll), and relays the lock's answer (POST /v1/requests/{id}/answer, signed like every lock call, accepted once and only from that instance) to the waiting facade. In memory only, 8 open per instance, 30 s wait (504), bounded bodies; a lock's error is a fixed code, never its text. - Facade socket:
POST /v1/tickets/{instance}/{id}/reveal(tty and deck only, routed to the ticket, the key signed for the caller's facade) and…/renew;GET /v1/queuegains anexpiredlist (expired in the last 24 h, not renewed yet). The hub relays the sealed answer without storing or logging it; the audit log recordsrevealandrenewevents (who, which facade, outcome), never content. - Renewals: a ticket that renews another ends it expired (pending, or approved and not run; only a renewal ends an approval, never the sweeper) and links them. A renewal asked from the terminal or the deck within 24 h of the expiry, unchanged, with the old ticket's risk and flags, and not for production, is approved at once; otherwise it waits with its changes, production still asks for the typed name.
lock upcreates per-user tty and deck facade keys ($XDG_STATE_HOME/lockfamily/facades/, 0600), reviews them (facade tty key …), mounts their public halves read-only in every lock withreveal: trueat/run/lockfamily-facades/facades.jsonwith--facade-keys-file(reviewed:lock … pins the host facades' keys), and keeps a host-only copy of the pinned lock keys there for facades to verify a lock's answers. up.json carries the same set (facade_keys) and each lock'srevealfor lea's lockhttp gate (--facade-keys-file, lockhttp#16, lockhttp v0.5.0 or later), which lock up does not run.- A renewal routed to mobile with Web Push on may send a push for a ticket the one-click path approves a moment later (the push carries no content; the phone then finds nothing waiting).
lock approve:oreveals (Ctrl-O too where the terminal passes it; macOS keeps ^O), clearly marked and escaped;ron an expired ticket approves it again; a renewal shows what changed.
- Hub requests: the hub signs a
v0.1.1 (2026-10-30) #
Supported locks: images built on lockkit v0.3.1 (tested), such as the
first locksql, lockfile, lockmq and lockhttp releases with hub mode. Locks built
on lockkit v0.3.0 do not know --hub-plain-http and refuse the generated
command.
-
The generated lock command passes
--hub-plain-http(#20): lockkit refuses an http:// hub without it (DavidGodefroid/lockkit#72), and the hub is reached by its service name on the internallockhubnetwork. -
Builds on lockkit v0.3.1 (was a pre-release commit, 1d085bb) (#21).
Correction (v0.2.3): the note above overstates what v0.1.1's lock up
runs. It passes no --profile, mounts no profile and gives the consoles
no terminal: lockfile (v0.4.0 requires --profile) and lockhttp (its
console takes no --hub) refuse the generated command, and locksql
v0.10.0 and lockmq v0.3.0 refuse to run without a terminal (and would
find no profile in their empty home volume). Use lock up from v0.2.0
on with any lock (see the v0.2.0 table).
v0.1.0 (2026-10-30) #
First release. Supported locks: images built on lockkit v0.3.0 (hub
mode). lockkit v0.3.1 and later refuse the generated http://lockhub:8443
hub URL without --hub-plain-http: use lockhub v0.1.1 with them.
(v0.2.3: neither this release's nor v0.1.1's lock up runs a released
lock as generated, see the v0.1.1 correction; use v0.2.0 or later.)
-
Telegram facade (#8): same rules as Slack, over long-polled
getUpdates(outbound only). Answers count only from the (chat, sender) pairs ofhub.telegram.chats; agent text is HTML-escaped and kept in<code>/<pre>, link previews off; decisions signed astelegram:<chat>:<from>.lock add telegram --chat --fromasks for the bot token at the terminal. The hub itself now refuses a production ticket answered on any chat facade, and a chat facade that fails for good (a revoked token) stops alone instead of stopping the hub. -
Slack facade (#7): runs inside the hub when
hub.facadeslistsslack, in Socket Mode only (an outbound websocket: the hub listens for nothing). Only the lock's masked screen is posted, every agent-provided string escaped and kept inside code spans; the requester is shown as unverified; never a reveal. Answers count only from the team, channel and member ids ofhub.slackin lockfamily.yaml (reviewed by lock up, written to the hub's chat.json); production tickets and the risk classes kept off chat get no buttons. Decisions are signed by the hub asslack:<team>:<user>. The hub then joins its ownegress-hubnetwork; without a chat facade it has no route out.lock add slack --team --channel --userasks for the tokens at the terminal and stores them 0600 with the hub's data, never in the workspace; the hub reads them only when slack is enabled. -
lock add,lock import,lock doctor(#9).adddeclares a lock (image pinned by digest) in lockfamily.yaml, keeping its comments;importmerges sub-projects' lockfamily.yaml, keeping each instance's first declaration and the strictest routing, and reports every difference; both validate before writing and leave the review to the nextlock up.doctorchecks the configuration, engine, images, state directories, keys, routing, facade socket, containers and the hub's internal network, and prints the commands that remove host accounts left by pre-hub consoles; it changes nothing. -
lock up [--file] [--dry-run](#5), minimal: reads lockfamily.yaml (version 1: which locks run, images pinned by digest, facades and routing; a lock's target, production flag and credentials stay in its own profile), verifies every distinct image's cosign signature against its release workflow, gets the hub key (created once) and each lock instance's key (<lock> hub-key), shows the human the whole configuration on the first run and the change afterwards, and applies only on a typed yes: writes the hub's locks.json and routing.json (applied by the hub), thendocker compose up -dwith the compose file on stdin. Everything lock up writes or runs from (compose file, applied fingerprint, hub data) lives in$XDG_STATE_HOME/lockfamily/<project>/, outside the projects directory; lock up refuses a state or runtime directory that is a link, not its own, or open to others. Generated stack: an internallockhubnetwork for the hub and the locks only, one egress network per lock, read-only containers without capabilities, the hub's data and facade socket on host directories owned by the host user. -
lock audit verify --data DIR --hub-key-file FILE(#4): checks the hub's hash chain and that every record is signed by the pinned hub key, the signed decision matching its audit record, every decision's signature against the pinned hub key, and that the database and the log agree (a cut log tail shows as a decision missing from the log). Lock audit chains follow with lockkit's audit chain (DavidGodefroid/lockkit#30). -
lock approve(#3): the terminal facade. It follows the hub's queue on the host facade socket (lockhub serve --facade-socket, default$XDG_RUNTIME_DIR/lockhub/facade.sock, mode 0600), shows each masked screen through lockkit's terminal sanitizer, and sends y / n/ skip; production tickets need their name typed. The approver is recorded from the socket peer's uid. Refuses a non-terminal stdin. -
Facade API (host socket only):
GET /v1/queue?wait=,POST /v1/tickets/{instance}/{id}/decide(tty or deck),GET /v1/decided?since=andGET /v1/tickets/{instance}/{id}(read-only, for lea). -
Ticket store, state machine and signed decisions (#2): SQLite store (pure Go) keyed by (instance, ticket id); states pending, approved, denied, withdrawn, expired, consumed; one decision per ticket, signed with the hub key in lockkit's decision format; per-risk TTL (lockkit
ticket.ClampTTL), swept every 30 s; per-instance pending cap; hash-chained audit logtickets.jsonlthat the hub refuses to start on when broken; production tickets need their name retyped. -
Lock API v1 on the hub network:
POST /v1/tickets,POST /v1/tickets/{id}/withdraw,POST /v1/tickets/{id}/consumed,GET /v1/decisions?since=&wait=(long poll). Every request is signed with the lock instance's key (lockkitticket.RequestMessage, contextlockkit/request/v1), single-use nonce, ±60 s clock window; an instance only sees its own tickets and decisions. -
Routing rules (#6): terminal, deck and mobile answer every risk class; Slack and Telegram answer read, read-pii and write only, with no opt-in; a workspace can only narrow the table.
-
lockhub keygen;lockhub serve --data DIRreadshub.key(0600) andlocks.json. -
Skeleton (#1):
lockhub servewithGET /v1/hello,lock version, read-only non-root image (uid 10040), CI, and a release workflow that publishes a cosign-signed multi-arch image by digest. -
Release archives of the
lockCLI (lockfamily#4): staticlock_X.Y.Z_<os>_<arch>.tar.gzfor Linux and macOS (amd64, arm64) andchecksums.txt, signed with cosign (keyless, bundlechecksums.txt.sigstore.json) by the release workflow, attached to the GitHub release. The Lock Familyinstall.shverifies them.