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).