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

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:

  1. 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.
  2. Internal hosts you declared (CI, registry): decided at the domain, FREE only when you marked them read, only on their declared ports.
  3. A single-use authorisation left by an approval whose client had already given up.
  4. Exfiltration heuristics (data in the URL, high-entropy values and labels, unusual headers, known secrets, PII): logged only by default (heuristics = "shadow"); with heuristics = "enforce" a score of 3 or more asks, never in auto mode.
  5. Policy rules, from the most specific to the least.
  6. 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

  1. Agent → gate: POST https://api.unknown.io/v1/upload
  2. Gate: source → container; policy → ASK; body buffered (up to the limit); local queue
  3. Gate → console: summary, masked preview, warnings, correlation (container, agent, task, session)
  4. Console → you: approval screen
  5. You → console: y, or n <reason>
  6. Gate → upstream: the held request (on y), or 403 E_DENIED with 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

ModeWhat the gate seesWhat you approve
CONNECT + SNI (no CA)host, port, SNIA host for a container: y opens container × host × port for 10 minutes, reads and writes, content invisible. The screen says so.
TLS decryptionmethod, path, headers, bodyOne 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/messages and /v1/messages/count_tokens are free only without mcp_servers and with no server tool other than web_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.

CodeMeaningAgent should
E_DENIEDthe human denied the requestask the user
E_TIMEOUTnobody answered the approval in timeask the user
E_PENDINGthe request waits for the humanretry the same request in a moment
E_NO_CONSOLEno lockhttp console is runningask the user to run lockhttp console
E_HOST_UNKNOWNthe host is not trusted and needs a humanask the user
E_METHODthe method is not allowed for this hostask the user
E_RULEa policy rule denies the requestask the user
E_BODY_TOO_LARGEthe body is larger than the buffer limitsend less, or ask the user
E_EXFIL_SUSPECTthe request looks like data exfiltrationsend less data, or ask the user
E_CLAUDE_API_PARAMa Claude API parameter needs a humanask the user
E_FLOORIP literal, DoH, private range, port, SNI mismatch, h2cstop
E_SECRET_EGRESSa credential is leaving its legitimate hoststop
E_PINNEDthe client refused the gate’s certificatestop
E_SOURCE_UNKNOWNthe gate cannot tell which container sent the requeststop