Files
zooqueenandHanzo Dev 82e0507e34 lsp: a jailed language-server daemon, so a definition can leave the repository
A static symbol index knows what a repository says about itself. It cannot say
where dep.Greet is DEFINED, because the definition is in a module the repository
only names — and finding it means downloading that module and type-checking both
halves, which means running a third-party binary over untrusted bytes. That does
not belong in the API process. It belongs here, behind a jail.

The invariant is the separation: the network and untrusted bytes are never in the
same process. FETCH runs `go mod download all` with a socket, first-party
toolchain only, writing the tree and the dependency cache. SERVE runs gopls with
seccomp denying socket/socketpair/io_uring and every bind read-only. The phase is
not a field anyone sets — it is jail.Fetch or jail.Serve, so there is no value to
get wrong, and the only difference between their filters is two syscall numbers.

Roots are (org, repo, resolved-sha). Refusing a branch name at the door is the
entire cache-invalidation design: a sha names one tree for all time, a push is a
different key, and nothing decides when anything is stale. The daemon holds no
git credential and makes no git request; the caller sends the tree, and /ask
answers 409 {"need":"tree"} when it has not.

Fail closed. On Linux the jail is required: at boot a canary is built in each
phase and asked the kernel for a socket — serving must be refused one, fetching
must get one — and if that does not hold, /readyz is 503 and every request that
would run a language server is refused. There is no environment variable that
turns the jail off, so there is none that can be set by accident.

REUSED, not rewritten:
  internal/lsp/conn.go, langs.go — ported from hanzoai/cloud branch `lsp`
    (apps/lsp, 67975187) with their tests, including the two regressions that
    build pinned: Close hanging on a wedged server, and a frame reader that
    would allocate whatever Content-Length it was told.
  internal/jail/ — a copy of hanzoai/code-exec's jailer (d2794ce), generalized
    to two phases. It SHOULD be github.com/hanzoai/jail imported by both; a
    boundary that exists twice drifts. Noted in LLM.md, not decided here.

Three bugs found and fixed on the way to green, all of which made the service
answer null with no error anywhere:
  - the jail child now locks its OS thread before installing seccomp. The filter
    and no_new_privs are per-THREAD and syscall.Exec execs the calling thread; a
    goroutine migration between them would leave the exec'd program unfiltered.
    This is a fix over the code-exec original and belongs upstream there too.
  - clean() returns the root's own spelling, not the symlink-resolved one. The
    proof of containment still resolves, but a server rooted at /var/x and handed
    a document at /private/var/x does not consider it to be in its workspace.
  - didOpen is sent once per document. It is not idempotent in LSP, and a root is
    immutable, so once is also all that is ever needed.
  - the fetch is `go mod download all`: the bare form records only each
    dependency's go.mod hash, so the read-only serve phase then failed on a
    missing go.sum entry.

Phase 1 serves Go. The table carries five complete languages and Available()
decides, so adding one is installing its server in the Dockerfile.

Acceptance is hermetic: a module is published into a directory laid out as a Go
proxy and fetched with GOPROXY=file://, so the real `go mod download` runs with
no network, and locate/definition on dep.Greet must return external:true at the
module coordinate. Passes in 0.08s once warm.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 03:58:01 -07:00

903 B

lsp

The jailed language-server daemon. It holds immutable per-(org, repo, commit) working trees, runs a language server over each one inside a jail with no network, and answers position questions about them.

The answer it exists for is a definition that leaves the repository and lands in a dependency — which a static symbol index structurally cannot give you.

POST /root       {org, repo, rev, files}       → {ready, cold, langs}
POST /root/warm  same body, only for a held repo
POST /ask        {org, repo, rev, op, path, line, character, relation}
                                               → the answer, or 409 {"need":"tree"}
GET  /healthz    liveness
GET  /readyz     readiness — green only once the jail proved out on this host

Everything else — the wire contract in full, the two-phase security model, the deployment, and what phase 2 is — is in LLM.md.