Diagnostics
── Diagnostics ───────────────────────────────────────────────────────────── Editor tooling for bldr's own inputs (bldr-workspace.yml, build scripts), served by the daemon rather than by an IDE extension. The daemon is the only process that already knows what a workspace *means* — its members, the content each one resolves to, the build graph they produce — so an extension that re-derived any of that would be a second implementation, permanently a little behind this one. The editor therefore speaks ordinary LSP to a thin passthrough (bldr internal lsp), which does nothing but framing, and every answer comes from the node.
── Diagnostics ─────────────────────────────────────────────────────────────
Editor tooling for bldr's own inputs (bldr-workspace.yml, build scripts),
served by the daemon rather than by an IDE extension. The daemon is the only
process that already knows what a workspace means — its members, the
content each one resolves to, the build graph they produce — so an extension
that re-derived any of that would be a second implementation, permanently a
little behind this one. The editor therefore speaks ordinary LSP to a thin
passthrough (bldr internal lsp), which does nothing but framing, and every
answer comes from the node.
Service builder.Diagnostics, 1 rpcs.
StreamLanguageServerProtocol(stream LspClientMessage) -> stream LspServerMessage
One whole LSP session as one bidirectional stream: the editor's JSON-RPC requests and notifications in, the server's responses and notifications out. Closing either half ends the session; there is no resume.
Only the JSON-RPC payloads travel here. LSP's Content-Length framing is a
property of the byte pipe the editor speaks (stdio or TCP) and is stripped
by the passthrough — gRPC already delimits messages, so carrying the
framing too would mean two message boundaries that can disagree.
Request: LspClientMessage
One message from the editor side of a session.
The header MUST be the first message on the stream, and MUST be sent
exactly once. A jsonrpc frame before it, or a second header after it, is
a protocol error: the daemon fails the RPC with INVALID_ARGUMENT naming the
rule that was broken, rather than guessing at a session it cannot place.
A oneof (rather than optional fields on one flat message) is what makes "header first, then frames" expressible on the wire at all: every message is unambiguously one or the other, so neither side has to infer which it got from which fields happen to be set.
message LspClientMessage {
oneof message {
LspSessionHeader header = 1;
string jsonrpc = 2;
}
}| Field | |
|---|---|
header | |
jsonrpc | One complete JSON-RPC message, byte-for-byte as the editor wrote it, with the Content-Length framing removed. |
Response: LspServerMessage
One message from the daemon side of a session: a single complete JSON-RPC message (a response, or a server-initiated notification), unframed.
No oneof here, because the server has nothing to say that is not JSON-RPC: anything it must report out-of-band — a rejected header, a session it cannot serve — is a gRPC status, which ends the stream, and that is the distinction worth keeping.
message LspServerMessage {
optional string jsonrpc = 1;
}Types used above
LspSessionHeader
The opening frame of an LSP session, sent once before any JSON-RPC payload.
LSP has nowhere to put "where is the editor sitting". initialize carries a
root URI, but it arrives only after the editor has decided to talk, and a
daemon serving several editors at once cannot read the caller's directory off
its own process. So a session states its context up front, before the first
word of the protocol it is tunnelling.
message LspSessionHeader {
optional string cwd = 1;
optional map<string, string> options = 2;
}| Field | |
|---|---|
cwd | Absolute working directory of the passthrough process. The daemon resolves the enclosing workspace from it exactly as bldr build does, so an editor opened anywhere inside a workspace gets that workspace's answers. |
options | Free-form key=value settings from the IDE extension (-o KEY=VALUE on the passthrough). Nothing reads these yet. They exist so that the first setting an extension needs to pass — a log level, a feature toggle, an explicit workspace override — does not force a wire change, and with it a lock-step upgrade of daemon, CLI and extension for what is one string. |
Secrets
── Secrets ───────────────────────────────────────────────────────────────── CRUD for tenant-scoped secrets. The tenant is inferred from the request's TenantId extension (stamped by the gRPC interceptor); callers never pass it explicitly. Payloads are transmitted in plaintext over the (typically local-socket) transport; the server stores them encrypted at rest.
Other data types
Messages and enums that more than one Daemon service uses.