Lsp
Package lsp is live semantic code intelligence — definitions, references, types, hover, outline and diagnostics — over a repository AND its resolved dependencies, with no toolchain on the caller's…
Package lsp is live semantic code intelligence — definitions, references, types, hover, outline and diagnostics — over a repository AND its resolved dependencies, with no toolchain on the caller's machine.
| Base URL | https://api.hanzo.ai |
| Operations | 5 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1214 · LSP — Live Code Intelligence — Draft · read the specification →
/v1/lsp is live semantic code intelligence — definitions, references, types,
hover, outline, diagnostics, completion — over a repository and its resolved
dependencies, with no toolchain on the caller's machine. The cloud side,
hanzoai/cloud apps/lsp, is a proxy: the language servers run in the
hanzoai/lsp daemon, jailed on its own deployment, and this side owns the
three things the daemon must never hold — the tenant, the repository and the
ledger (apps/lsp/lsp.go:14-31).
Motivation
Answering a cross-dependency question means running a third-party toolchain over untrusted bytes, and a git credential that can fetch any repository is exactly what must not exist next to an unjailed compiler. Splitting the capability at that line — identity and money here, compilers there — is the design; this HIP also settles its address, which today nests inside another capability's root.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
The surface
Every address is under /v1/lsp, and every operation is typed: POST /hover,
/locate, /symbols, /diagnostics and /complete
(apps/lsp/mount.go:67-76), each taking one Query — one position in one file
of one repository at one revision (apps/lsp/lsp.go:60-84). lsp is on
HIP-0139 §2.5's list, so the name needs no argument. Positions are the LSP's
own — 0-based lines, UTF-16 character units — passed through untouched, because
the callers are agents and editors that already speak the protocol and a
silent re-basing corrupts every multi-byte line (apps/lsp/lsp.go:33-41).
Today's router serves this surface at /v1/code/lsp (manifest/apps.go:325,
apps/lsp/mount.go:71), under the code capability's root — nested rather than
top-level, so it is not a line openapi/misfiled.txt can carry; /v1/lsp is
the target this HIP declares, not yet served. code and lsp are two reads of
one repository — lexical search and a live language server — and stay
cross-referenced siblings, each at its own root, per §3.1.
What this side owns
No store. The tenant is resolved from the validated principal on every request
and is the daemon's isolation key; a caller supplies a repository slug, never
an owner and never a URL, so there is no input from which one tenant could name
another's repository (apps/lsp/lsp.go:21-25,60-66). The revision and tree
come from the forge, read as the caller, so the forge's own ACL decides which
repositories answer; the daemon holds no git credential — the tree is pushed to
it, never pulled by it (apps/lsp/lsp.go:26-30).
Money
Preparing a revision is what costs — a tree write, a dependency fetch, a
language server indexing for seconds to minutes — so the prepare carries the
fee: a flat 2¢, flat because the caller chooses the repository, not the cost of
indexing it (apps/lsp/meter.go:38-44). A query against a prepared revision is
a JSON-RPC round trip costing microseconds; it is recorded for attribution and
costs nothing, so callers are taught to reuse revisions rather than re-key
them. The ledger kind is lsp with models prepare and query, which one it
was being the daemon's answer, never a guess (apps/lsp/meter.go:35-52). The
gate runs before the work — an out-of-funds caller gets a clean 402 instead of
a dependency fetch nobody can bill — gating the worst case and charging the
real one (apps/lsp/meter.go:70-77). The payer is the selected billing org
from the request, never a body field (apps/lsp/meter.go:54-67). The plugin
declares Price: cloud.Metered (plugin/lsp/main.go:31).
Events, observability, stage
It publishes no events on the bus, so a customer's webhooks receive nothing
from it, and it emits nothing to observability beyond the request span every
route gets; attribution lives on the ledger rows. The stage is ga — the
manifest row declares none — and ga here rests on the hanzoai/lsp daemon
deployment actually serving; a deployment without the daemon MUST declare the
capability beta rather than 503 behind a ga door.
Upstreams
This package derives from none: it implements the Language Server Protocol's
position semantics as a wire fact and proxies its own daemon. The third-party
language servers themselves run inside hanzoai/lsp, jailed, and are that
repository's account.
Rationale
The alternative argued in the package's own doc was one home — lsp under
/v1/code, because an agent searches with code and is certain with lsp
(apps/lsp/lsp.go:7-12). The cross-reference survives; the address does not:
HIP-0139 §3.1 makes a second capability under another's root a misfiled route,
and lsp is its own app, its own plugin and its own deployment with its own
meter — everything a capability is except the address. The fold under its own
name costs one route move and removes the mount-order subtlety of a nested
prefix entirely.
Security Considerations
What an attacker gets from the wrong implementation is a compiler running over
their bytes next to another tenant's code. The design splits the two: the
daemon that runs toolchains holds no tenant mapping, no git credential and no
ledger, and the side that holds those runs no toolchain. Tenancy has no
caller-writable input — org from the principal, repository by slug within that
org, tree by the forge's ACL as the caller — so the cross-tenant read has no
parameter to arrive through. The billing subject is read from the request's
validated identity, never from the body, so a caller cannot bill somebody else
(apps/lsp/meter.go:60-63).
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | lsp at its own prefix | 5 operations |
| CLI | — | no command reaches it yet — use HTTP or an SDK |
| SDK | — | no published client declares one yet — regenerating the clients is what adds them |
| MCP | tool lsp on https://api.hanzo.ai/v1/mcp | 5 operations, 0 under the document's own id — ask describe for the rest |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call. This one takes arguments — the placeholders are yours to fill. POST /v1/lsp/hover, operation post_lsp_hover:
hanzo has no subcommand for this operation — the CLI serves only what cloud's live route table confirms. Use HTTP or an SDK.
import { Configuration, LspApi } from 'hanzoai';
const api = new LspApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.postLspHover({ character: 0, line: 0 });from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import LspApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = LspApi(client).post_lsp_hover(character=0, line=0)cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.LspAPI.PostLspHover(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, lsp_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = lsp_api::post_lsp_hover(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.LspApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new LspApi(client).postLspHover();The method above is the one at the current release of the document. [email protected] (npm) and [email protected] (PyPI) were generated from an earlier release, where this operation carried a different id, so it spells the method differently — regenerating the clients is what makes the two agree. SDKs →
curl -X POST https://api.hanzo.ai/v1/lsp/hover \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"character": 0,
"line": 0
}'The door declares no tool for lsp — tools/list on https://api.hanzo.ai/v1/mcp names the products it does reach. Use HTTP or an SDK.
Answers 200 with object — ok.
Endpoints
| Endpoint | What it does |
|---|---|
POST /v1/lsp/complete | Offers the candidates a language server has at a position, typed and resolved through the repository's dependencies rather than guessed from text. |
POST /v1/lsp/diagnostics | Reports every problem the language server finds in one file — compile errors, type errors and lints, each with its span and its severity (1 error, 2… |
POST /v1/lsp/hover | Renders the type and documentation of the symbol at a position, as the language server itself renders it. |
POST /v1/lsp/locate | Finds where a symbol lives: its definition, its references, its type or its implementations, chosen by relation (definition, reference, type,… |
POST /v1/lsp/symbols | Outlines one file: every declaration in it, with its kind and its span. |
How is this guide?