How lockhttp works
Two planes on two sides of a boundary. Only the human side can say yes.
The data plane, lockhttp gate, is a proxy in lea’s proxy container. The control plane, lockhttp console, is the standard lockkit console on the host, in its own account. The gate asks the console only about requests that need a human.
Pre-release. lockhttp v0.1.0 is out as a pre-release: command names are fixed, flags may still move. TLS decryption ships in it and is optional: without a local CA and the signer (--ca-cert, --signer), HTTPS is decided at the domain. Design decisions: docs/decisions.md, contract with lea: docs/lea.md.
Where each piece runs
agent container (internal network, no other route)
┌──────────────────────────────────────────────────────┐
│ claude code · curl · git · npm · chromium │
│ HTTP(S)_PROXY=http://<agent>:<token>@lea-proxy:3130 │
└──────────────────────────┬───────────────────────────┘
│ the only way out
lea proxy network namespace ▼ (gate + signer sidecars)
┌──────────────────────────────────────────────────────┐
│ lockhttp gate :3130 (own uid, never 0) │
│ source → container · policy → FREE / ASK / DENY │
│ lockhttp signer (optional: holds the CA key) │
│ squid 127.0.0.1:3128 (network floor) ──▶ Internet │
└──────────────────────────┬───────────────────────────┘
│ Unix socket, volume lockhttp-run:
│ the gate and the console only
console container ▼
┌──────────────────────────────────────────────────────┐
│ lockhttp console --profile default │
│ account lockhttp, volume lockhttp-home (policy) │
│ approvals · policy review · auto mode · audit │
└──────────────────────────────────────────────────────┘
The agent containers never see the console socket. The console authenticates the gate by its uid (SO_PEERCRED) and never goes on the network itself.
How a request is decided
The first step that decides wins:
- Network floor (DENY, cannot be loosened): IP literals, DNS over HTTPS, private ranges and metadata addresses, ports other than 80/443, an SNI that differs from the host,
Upgrade: h2c, nested CONNECT. - Internal hosts you declared (CI, registry): decided at the domain, FREE only when you marked them
read, only on their declared ports. - A single-use authorisation left by an approval whose client had already given up.
- Exfiltration heuristics (data in the URL, high-entropy values and labels, unusual headers, known secrets, PII): logged only by default (
heuristics = "shadow"); withheuristics = "enforce"a score of 3 or more asks, never in auto mode. - Policy rules, from the most specific to the least.
- Defaults: body-less reads of trusted hosts are FREE; reads of unknown hosts, writes, uploads, messaging and WebSockets ASK; a body over the buffer limit (10 MiB) is denied.
The life of a held request
- Agent → gate:
POST https://api.unknown.io/v1/upload - Gate: source → container; policy → ASK; body buffered (up to the limit); local queue
- Gate → console: summary, masked preview, warnings, correlation (container, agent, task, session)
- Console → you: approval screen
- You → console:
y, orn <reason> - Gate → upstream: the held request (on
y), or403 E_DENIEDwith your reason
The gate holds a request for 60 s. If nobody answered by then, the agent gets 403 E_PENDING and the item stays on the console; once you approve, the same request (same fingerprint) goes through within 10 minutes, once. Without a console, an ASK is refused at once with E_NO_CONSOLE, while FREE traffic continues on the last approved policy.
HTTPS, with or without decryption
| Mode | What the gate sees | What you approve |
|---|---|---|
| CONNECT + SNI (no CA) | host, port, SNI | A host for a container: y opens container × host × port for 10 minutes, reads and writes, content invisible. The screen says so. |
| TLS decryption | method, path, headers, body | One request. A local CA, trusted only inside lea’s containers, is issued per installation; lockhttp signer holds its key. |
Hosts in the policy’s splice list (clients that pin their certificates) and internal hosts are never decrypted: they are decided at the domain, and never in auto mode.
The Claude API is a relay
api.anthropic.com is not only the model’s channel: mcp_servers makes Anthropic call any remote MCP server, and some server tools fetch or run things outside your machine. Once TLS is decrypted:
POST /v1/messagesand/v1/messages/count_tokensare free only withoutmcp_serversand with no server tool other thanweb_search;/v1/files*asks;- a credential that is not one of the container’s own is denied (
E_SECRET_EGRESS); - any other path or host under the Anthropic domains asks, never in auto mode.
Rules and auto mode
The policy lives with the console, never in the project: the gate reads no policy file, it uses what the console serves. A rule applies to all containers or to one, never to an agent (an agent name is a claim, not an identity).
[[profiles.default.rules]]
id = "github-api-read"
host = "api.github.com"
methods = ["GET", "HEAD"]
verdict = "free"
[[profiles.default.rules]]
id = "npm-audit"
container = "lea-ab12cd34ef"
host = "registry.npmjs.org"
methods = ["POST"]
path = "/-/npm/v1/security/advisories/bulk"
verdict = "auto" # asks, but auto mode may answer
max_body = "256KiB"
A rule shape from the design document; the exact schema is settled with v0.1.0. Any loosening (a new host, a wider method or path, ask → auto → free, a wider scope) is confirmed by you in the console’s review. An auto rule is refused for uploads, messaging, registry publishing, git-receive-pack, spliced hosts, heuristic hits, secrets and production profiles.
Refusal codes
A refusal is a 403 with X-LockHTTP-Code and X-LockHTTP-Request headers and a text body saying why and what to ask. The codes are part of the protocol and never renamed.
| Code | Meaning | Agent should |
|---|---|---|
E_DENIED | the human denied the request | ask the user |
E_TIMEOUT | nobody answered the approval in time | ask the user |
E_PENDING | the request waits for the human | retry the same request in a moment |
E_NO_CONSOLE | no lockhttp console is running | ask the user to run lockhttp console |
E_HOST_UNKNOWN | the host is not trusted and needs a human | ask the user |
E_METHOD | the method is not allowed for this host | ask the user |
E_RULE | a policy rule denies the request | ask the user |
E_BODY_TOO_LARGE | the body is larger than the buffer limit | send less, or ask the user |
E_EXFIL_SUSPECT | the request looks like data exfiltration | send less data, or ask the user |
E_CLAUDE_API_PARAM | a Claude API parameter needs a human | ask the user |
E_FLOOR | IP literal, DoH, private range, port, SNI mismatch, h2c | stop |
E_SECRET_EGRESS | a credential is leaving its legitimate host | stop |
E_PINNED | the client refused the gate’s certificate | stop |
E_SOURCE_UNKNOWN | the gate cannot tell which container sent the request | stop |