lock family docs GitHub

lea · Agents at work

The Locks in lea

Databases, with locksql #

lea comes with locksql: a console that stands between your agents and your databases. The agents propose SQL; you approve each statement, and the console alone holds the credentials. The console runs hub-attached only, under the Lock Family's lox run (lockhub's docs/lox-run.md), in a compose service of its own: lea starts no locksql console. Add a locksql lock to your projects directory (lox add locksql NAME: with lea on your PATH, lox add puts your uid in the lock's allowed_uids in .lockfamily/lockfamily.yaml, because lea's agents run as your uid, which has no account in locksql's image); lox run applies it. lea's agents then reach that lock's socket volume (read-only, once docker confirms it is lox run's, see below); without such a lock they get no locksql socket and lea says "no such lock yet: lox add locksql". locksql is on for lea as soon as lox run runs a locksql lock for the projects directory: lox add locksql is all there is to it (LEA_LOCKSQL=0 in your configuration keeps it off; lea init --locksql or LEA_LOCKSQL=1 turns it on without a lock). A lock added while lea runs reaches the containers started afterwards only (Docker cannot add a mount to a running container): the deck says "lock NAME added: restart lea to use it (lea stop, then lea)", the agents' status lines say "+lock NAME: restart lea", and the fix is lea stop, then lea: restarting an agent or the deck does not give a running container the mount. lea locksql and lea doctor say which applies. With several locksql locks, each reaches the agents on /run/locksql/<instance>; a Codex agent gets one MCP server per lock (locksql-<instance>), and lea's MCP server tells a Claude agent, which calls the client itself, "several locksql locks: orders stock; pass --instance NAME" (the lock names are yours). The lock clients themselves also refuse a call that names no lock when several are mounted: "several locksql locks: orders stock; pass --instance NAME (or set LOCKSQL_INSTANCE)".

lea init builds and installs no locksql (lox runs the lock's own image): when your projects directory has no database profile yet, it walks you through a first one with the locksql client of lea's image (the release of config/locks.lock; engine, host, TLS, where the password comes from; never the password). lox run reviews that profile (.locksql in your projects directory) and copies it into the lock; the agents see .locksql read-only. Vault profiles wait for lox run to pass Vault to the console (lockhub#45). lea puts no locksql command on the host (it removes the link older releases made).

Each query of an agent waits for lox run's decision, in its terminal outside lea: lea shows no ticket and answers none. The decision reaches the agent as a new turn once its turn is over, also for a query it ran through the shell (lea knows which agent waits on each pending ticket of the hub). The deck's top border counts the container's requests that wait (locksql 2 wait for lox run). The console's own questions (its policy review on a first start or after a loosening, a production profile's typed name, the credentials its profile does not take from Vault) are lox run's too; lea shows no lock console and lea lock is gone.

The two projects are independent: lea runs without locksql, and locksql works with any agent. Off (the default, lea init --no-locksql or LEA_LOCKSQL=0), lea leaves no trace of locksql.

Queues and files, with lockmq and lockfile #

lockmq (RabbitMQ) and lockfile (files on S3, SFTP or a local directory) keep a human between the agents and production the same way: the agents ask, a console that alone holds the credentials shows each request and waits for your approval. Both are pre-releases, so they are off by default, and on for lea as soon as lox run runs a lock of their type for your projects directory (lox add lockmq or lox add lockfile; LEA_LOCKMQ=0 / LEA_LOCKFILE=0 in your configuration keeps one off). lea init asks (no by default; a no records nothing, so lox add still turns the tool on), or lea init --lockmq / lea init --lockfile; lea init --no-lockmq / --no-lockfile record the opt-out.

Their console runs hub-attached only, in a container of its own that the Lock Family's lox run starts for your projects directory (lox add lockmq <name> or lox add lockfile <name>, which applies at once; lockhub's docs/lox-run.md): no host account, no sudo <tool> install. lea's agents run as your uid, which has no account in the console's image: the console serves it only when your uid is in the lock's allowed_uids (.lockfamily/lockfamily.yaml). lox add puts it there when lea is on your PATH; lox run reviews it with you; without it the agents get no console. Edit it by hand only for a lock added without lea on your PATH. No agent can. lea installs nothing for them; their launchers update themselves (lockmq update, lockfile update). lea init points to the launcher when it is missing, then runs lockmq's profile wizard (lockmq profile add, never a password); for lockfile it names lockfile profile add --profile NAME --source ..., which writes the profile lox run reviews. lockfile below v0.1.1 is refused (a plugin sandbox escape).

lea never starts, sees or drives these consoles. Its containers get only the socket volume of lox run's lock of the tool (.lockfamily/up.json) on /run/<tool> (each of several locks on /run/<tool>/<instance>), read-only, and the tool's clients group (lockmq-clients 10010, lockfile-clients 10030): lea's image names the console accounts with the ids of the tools' images, so the client checks that the socket is the console's. The agents reach the socket and nothing else (no credential, no profile, no audit log). A tool that is on without a lock of lox run in the projects directory gets no socket, with a warning that says "no such lock yet: lox add <tool>": there is no fixed <tool>-run volume of a console started by the tool's own launcher any more. lea lockmq / lea lockfile tell you whether the tool is on and how its console runs. Restart lea (lea stop, then lea) after adding a lock or turning one on.

When an agent's Lock client finds no console (a console that waits at its terminal for its start questions, after its idle session ended, has no socket yet), the agent asks lea (its lock_hint tool) and reports what you must do, from what lea knows of the lock rather than a guess: "lock NAME waits for you: answer in lox run (or lox attach NAME)" while lox run runs the lock, and "start lox run in the projects directory" when no lox run serves; never a command that restarts or recreates the lock. The agents' status line says "no lox run: start it in the projects directory" while none serves.

Logs, with locklog #

locklog lets the agents read the logs (files, journald, containers, Loki, Elasticsearch) without the people in them: its console masks every record it gives an agent (e-mail addresses, IPs, tokens become pseudonyms), counts, patterns and searches are free, and the clear value of a span (locklog_unmask) waits for your approval at lox run. It is wired exactly as lockmq and lockfile: off by default, and on for lea as soon as lox run runs a locklog lock for your projects directory (lox add locklog, which finds this host's logs and writes the profile, .locklog/config.toml, that lox run reviews with its mount plan); LEA_LOCKLOG=0 keeps it off, lea init --locklog / --no-locklog record the choice. lea's containers get only the socket volume of lox run's lock on /run/locklog (each of several on /run/locklog/<instance>, LEA_LOCKLOG_INSTANCES naming them), read-only, and the clients group locklog-clients (10050; the console account is 10051, as in locklog's image): no log, no key, no audit reaches them. The client is lea's image's (config/locks.lock); Codex agents get it as the locklog MCP server; lea locklog tells you whether it is on, and lea doctor compares it with the lock lox run runs.

Web requests, with LockHTTP #

LockHTTP puts a human between the agents and the web: every outbound HTTP(S) request of an agent goes through a LockHTTP gate beside lea's proxy, which lets through what its policy trusts and asks you, on the Lock Family hub (lox run, outside lea, or a paired phone), about the rest. It is off by default (LEA_LOCKHTTP=0): lea then behaves as before.

LockHTTP runs in Docker only (docs/docker.md in the lockhttp repository). To turn it on, install the launcher of the lockhttp release lea pins, v0.7.0 (LOCKHTTP_VERSION=v0.7.0 sh install.sh with lockhttp's scripts/install.sh, which checks the release's signatures and pins its image by digest; lea doctor warns about an older one), add a lockhttp lock to your projects directory (lox add lockhttp <name>, which applies at once and starts the hub and the lockhttp console; its policy in .lockhttp/config.toml, which lockhttp profile add --trusted "<your LEA_EGRESS_ALLOW>" writes). Then set LEA_LOCKHTTP=1 in ~/.config/lea/lea.conf and restart lea (lea stop --all, then lea); lea doctor checks each step (its lockhttp line says off, or on with the gate's state). LEA_LOCKHTTP=1 lea is not enough: the configuration wins, and lea warns that the environment's value is ignored. lea-proxy and its gate are shared by every lea on the machine, so LockHTTP is on or off for all of them at once, and once it is on, a lea whose projects directory has no lockhttp lock does not start. The gate runs hub-attached only: without a trusted .lockfamily/up.json naming a lockhttp lock, lea does not start (it says to run lox run). Nothing else on the host: no account, group or socket directory.

With it on, squid listens on the proxy's loopback only, behind the gate (its floor stays: DoH resolvers, IP literals and private ranges are refused; the allowlist is the gate's policy now). lea runs the gate as a container of its own, lea-lockhttp-gate, in the proxy's network namespace, from the image the launcher pins (never a tag), as the image's gate account (never root), without capabilities, on a read-only root; it is the only container with the console's socket volume (lox run's <project>_<name>-run): no agent container gets it. Its audit stays in the volume lea-lockhttp-audit, its status (for the deck) in ~/.local/share/lea/lockhttp/status/. A proxy started again gets a new gate. The gate names the calling container from the network the request arrives on, which no agent can forge: the container is the trust boundary. Each agent also has its own proxy password, and the daemon tells the gate whose it is (sources.json, under ~/.local/share/lea/lockhttp/), but the agents of one container share a uid and can read each other's environment: that name is for the audit and the console's display (attribution), never a permission, and one agent can pass for another of the same container. lea-keyproxy goes through the gate too, and so do the forge calls of grants, with the calling agent's own password. The daemon also tells the gate which Claude login each container holds (credentials.json: hashes of the OAuth access token in its state, and of the keys lea-keyproxy injects); under MITM the gate denies a Claude API key that is not the caller's (E_SECRET_EGRESS). That login file is the agent's to write: the check stops a key sent from the wrong container, not an agent that rewrites its own login file. LEA_GIT_SSH and LEA_OTEL_UPSTREAM are refused (their raw TCP forwarders would bypass the gate: use git over HTTPS), and claude.ai's connectors are turned off in the agents' Claude Code (WebSearch and WebFetch stay).

The hub (lockhub's docs/lox-run.md): lox run runs a lockhttp lock for the projects directory (its .lockfamily/up.json), and the gate asks the Lock Family hub about every held request. It mounts that lock's socket volume and gate volume (--home), and reads lea's read-only copies (in /run/lockfamily) of the hub's key; of the host facades' keys, for a lock lox run lets reveal (reveal: true: --facade-keys-file, so that a reveal, is sealed only to them); and of the paired phones, for a lock that pins them (devices: true: /run/lockfamily/devices.json, with --require-device and --seal-clear as hub.mobile says; without it, a phone's decision is refused). Only the proxy joins the hub's network, whose addresses the gate refuses as targets: no agent reaches the hub. lea checks first the volumes' and the network's labels, the key, and the facade keys and devices as the gate reads them (lockkit's parsers; it writes them in canonical form); anything it cannot trust stops lea, which says why and to run lox run; so does an up.json that does not say what the gate pins (lockhttp_gate 1: a lock's devices, required and sealed, its reveals), and a lock whose required or sealed devices lea cannot pass. The pinned lockhttp release decides too: the hub needs v0.3.0 (an older one stops lea), the devices v0.4.0, the facade keys v0.5.0; lea passes no flag that release lacks, and says so. One lockhttp lock at a time: the gate serves every projects directory, and asks the hub of the one whose lea started it last; lea refuses to switch it to another hub while agents of another projects directory run.

LEA_LOCKHTTP_MITM=1 (with LEA_LOCKHTTP=1) lets the gate decrypt HTTPS, but for the policy's splice hosts, so that it decides on methods, paths and bodies. lea makes a local CA on first use through the launcher (lockhttp ca init, 90 days, in ~/.local/share/lea/lockhttp/ca/; lea doctor warns a week before it ends: lockhttp ca init --force --dir <it>, then lea stop --all and lea). Its key goes to one container only, lea-lockhttp-signer (the image's signer account, no network), through the volume lea-lockhttp-ca (key owned by signer, 0600); the signer hands leaf certificates to the gate on a socket in the volume lea-lockhttp-signer. The certificate, never the key, goes to the gate, to lea-keyproxy and into the agents' trust stores (system store, NODE_EXTRA_CA_CERTS, REQUESTS_CA_BUNDLE, PIP_CERT, SSL_CERT_FILE).