Build controller
The contract between a node and the build controller it dispatches a build to.
External build controller service.
A build controller is a separate process that the node connects to via a Unix
socket. The node calls GetInfo once at startup to learn the controller's name
(which must match the workspace's build_controller: value), then calls Build
once per generation (including once per watch-mode iteration).
Service builder.controller.v1.BuildController, 4 rpcs.
GetInfo(GetInfoRequest) -> GetInfoResponse
Return controller metadata so the node can register it under the right name.
Request: GetInfoRequest
message GetInfoRequest {
// no fields
}Response: GetInfoResponse
message GetInfoResponse {
optional string name = 1;
optional string version = 2;
repeated string capabilities = 3;
}| Field | |
|---|---|
name | Controller name — must match the workspace's build_controller: value (e.g. "default" → build_controller: default). |
version | Informational version string, e.g. "0.1.0". |
capabilities | Optional features this controller implements, as stable names (e.g. "build.targets"). The node reads them to decide what it may ask of a controller it did not ship with — a workspace pins its controller image independently of the daemon, so the two versions drift by design. |
Build(BuildRequest) -> stream BuildControllerEvent
Execute one build generation. Streams events (progress, task-tree nodes, and a terminal output or error event) back to the node.
Request: BuildRequest
message BuildRequest {
optional string input_cid = 1;
repeated string args = 2;
optional string owner_kind = 3;
optional string owner_id = 4;
repeated string node_capabilities = 5;
optional map<string, string> option_overrides = 6;
}| Field | |
|---|---|
input_cid | CID (string form) of the BuildInput DAG-CBOR block. |
args | The build's selector vector, composed by the node from the build's typed fields: <member> --only <item> for a single-item build, --build-member <m> per selected member, --no-targets, --no-cache, and the targets spec as --targets-mode <mode> + repeated --target-filter <f>. A string list rather than typed fields because a workspace pins its controller image independently of the daemon: a vector an older image already parses survives the hop verbatim, where a new field it has not learned would be silently dropped. Nothing a user typed travels here. |
owner_kind | Owner (kind + id) that owns this build's invocations, so pods the controller spawns via pod.dag link back to the owning build/run/deployment and are cleaned up when it is removed. Empty when unset. |
owner_id | |
node_capabilities | Optional features this node implements, as stable names (e.g. "pod.cache-volumes"). Build code reads them through Bldr.nodeCapabilities() and asks only for what the node understands. |
option_overrides | Per-build overrides of the workspace's options: map (bldr build -o key=value). Each value is text to parse as YAML, as bldr ws set does. The controller merges them over the BuildInput's options before the build code runs, so input.options is the effective map. A controller that understands this advertises the build.options capability; the node refuses to dispatch overrides to one that does not, rather than let them vanish. |
Response: BuildControllerEvent
One event streamed from the controller back to the node during a build. The stream ends after exactly one terminal event (output or error).
message BuildControllerEvent {
oneof event {
ControllerTaskEvent task = 2;
ControllerOutputEvent output = 3;
ControllerErrorEvent error = 4;
ControllerTargetsEvent targets = 5;
ControllerDiagnosticsEvent diagnostics = 6;
}
}| Field | |
|---|---|
task | A progress-tree node update (non-terminal, repeatable). |
output | Build succeeded — output_cid is the artifact (terminal). |
error | Build failed (terminal). |
targets | What this workspace's build code declares it can produce (non-terminal, repeatable). |
diagnostics | What the build's tooling said about the source: type errors, lints (non-terminal, repeatable). |
StreamLanguageServerProtocol(stream ControllerLspMessage) -> stream ControllerLspMessage
Serve LSP for the workspace's build code — .bldr.ts and
.bldr/**/*.ts.
Those files are TypeScript, but not TypeScript a general language server
can make sense of: their imports resolve through the build runtime's own
rules (bare member specifiers, .bldr/ as an implicit source root, a
generated ambient module per member), and the types they are checked
against are generated from this daemon's function registry. The
controller is the only thing that knows all of that — it already
type-checks these files on every build, with exactly the virtual
filesystem and paths map a language service needs. So the node routes
build-code LSP here rather than reimplementing the resolution.
Its own RPC rather than messages multiplexed into Build: an editor
session is long-lived and has no build, while a Build stream belongs to
one generation and ends with it. Sharing one would tie the lifetime of a
developer's editor to the lifetime of a build.
Request: ControllerLspMessage
One message of an LSP session with the controller.
The first client message must be the header; every later one carries a JSON-RPC payload. The payload is deliberately just a string: the node is a conduit here, not a participant — it routes and rewrites paths, and giving it a typed view of LSP would mean teaching it the protocol twice.
message ControllerLspMessage {
oneof message {
ControllerLspHeader header = 1;
string jsonrpc = 2;
}
}| Field | |
|---|---|
header | Session header. Client's first message, exactly once. |
jsonrpc | A JSON-RPC request, response or notification. |
Response: ControllerLspMessage
The same message as ControllerLspMessage.
ExecuteGraph(stream GraphNodeMessage) -> stream GraphControllerMessage
Declare one build generation as a graph and answer for its inputs.
The controller declares nodes; the node executes them. The controller's only other part is to say, for a node whose dependencies have resolved, what that node's input is. Scheduling, caching, node state and failure belong to the node (specs/concepts/build-graph.yml).
The node's first message is the start; the controller answers with the root graph. From then on the node sends one PrepareInput per node that has an input mapper and the controller answers each with one InputPrepared. The node ends the generation by closing its stream.
A controller that implements this advertises the build.graph
capability; the node uses Build for one that does not.
Request: GraphNodeMessage
One message from the node to the controller during ExecuteGraph.
message GraphNodeMessage {
oneof message {
BuildRequest start = 1;
PrepareInput prepare_input = 2;
}
}| Field | |
|---|---|
start | The generation to declare. The node's first message, exactly once. |
prepare_input | Ask for one node's input. |
Response: GraphControllerMessage
One message from the controller to the node during ExecuteGraph.
message GraphControllerMessage {
oneof message {
ControllerGraph graph = 1;
InputPrepared input_prepared = 2;
ControllerDiagnosticsEvent diagnostics = 3;
ControllerErrorEvent error = 4;
}
}| Field | |
|---|---|
graph | The root graph. The controller's first graph message, exactly once. |
input_prepared | The answer to one PrepareInput. |
diagnostics | What the build's tooling said about the source (repeatable). |
error | The graph could not be declared. Terminal. |
Types used above
ControllerTaskEvent
A snapshot of one node in the controller's progress tree. Consumers key by id; link children via parent; render state. Each event is a full snapshot of that node (idempotent to replay).
message ControllerTaskEvent {
optional uint64 id = 1;
optional string label = 3;
optional ControllerTaskState state = 4;
optional map<string, string> labels = 10;
repeated uint64 waiting_for = 11;
oneof _parent {
uint64 parent = 2;
}
oneof _output {
string output = 5;
}
oneof _progress_update {
ControllerTaskProgress progress_update = 6;
}
oneof _log_stream_id {
string log_stream_id = 7;
}
oneof _log_cid {
string log_cid = 8;
}
oneof _pod_id {
string pod_id = 9;
}
oneof _input {
string input = 12;
}
oneof _wait {
ControllerTaskWait wait = 13;
}
}| Field | |
|---|---|
id | Unique id within this build's task forest. |
parent | Absent on root nodes; set to the parent's id for child nodes. |
label | Human-readable label for this node. |
state | Current lifecycle state. |
output | Terminal output value (result CID or error message), present when state is COMPLETED or FAILED. |
progress_update | Quantitative progress, present when state is RUNNING. |
log_stream_id | A live log stream id capturing this node's command output, if it opened one (the daemon surfaces it so bldr logs follow <id> / the UI can tail it). Set on the RUNNING re-emit once the stream opens. |
log_cid | The sealed log stream's final Blob CID, carried on the terminal emit. |
pod_id | The id of the pod this node ran, if any. Set on the RUNNING re-emit once the pod starts; the daemon surfaces it so the UI can link to the Pods page. |
labels | Free-form key/value metadata for this node. language names a syntax for rendering the label (e.g. "sh" for a shell command), so a consumer need not guess from the text. |
waiting_for | Ids of the tasks this one is waiting on, when queued. Lets a consumer say what a pending node is blocked behind instead of just "not started". |
input | The invocation's input CID, once one is attached to this node. |
wait | What the node is held on, present when state is WAITING or WAITING_FOR_RESOURCES. |
ControllerTaskProgress
message ControllerTaskProgress {
optional uint64 current = 1;
optional uint64 total = 2;
}ControllerTaskWait
Why a node is not making progress. Mirrors builder.TaskWaiting and
builder.TaskWaitingForResources so the node's translation is a copy.
message ControllerTaskWait {
optional string reason = 1;
optional string resource = 2;
optional uint64 needed = 3;
optional uint64 available = 4;
}| Field | |
|---|---|
reason | Human-readable, e.g. "waiting for an execution slot (all 8 in use)". |
resource | Which resource admission is short of: "cpu" | "memory" | "slots", or "queue" when the execution is waiting behind earlier ones. Empty for WAITING, which is not an admission wait. |
needed | What admission needs and what is free, in the resource's own unit (millicores, bytes, slots; for queue, the executions ahead and 0). Zero for WAITING. |
available |
ControllerOutputEvent
message ControllerOutputEvent {
optional string output_cid = 1;
}| Field | |
|---|---|
output_cid | CID (string form) of the build's output artifact. |
ControllerErrorEvent
message ControllerErrorEvent {
optional string message = 1;
}ControllerTargetsEvent
The targets a workspace's build code declares, and how much to trust the list.
Sent as its own event rather than folded into the terminal output because it is known long before the build finishes — the code declares everything by the time its members have been imported — and because a build that fails still has a useful answer to "what could this workspace build". A list that only arrived with a successful output would be missing exactly when it is most wanted.
message ControllerTargetsEvent {
optional TargetListStatus status = 1;
repeated BuildTarget targets = 2;
optional string error = 3;
}| Field | |
|---|---|
status | How current the list is. |
targets | Every declared target. Empty while generating, and on failure. |
error | Why the list could not be produced. Set only with STATUS_FAILED. |
BuildTarget
One thing a workspace can build, run, deploy or test.
message BuildTarget {
optional string name = 1;
optional TargetKind kind = 2;
optional map<string, string> labels = 3;
}| Field | |
|---|---|
name | Its full path: <member>/<scope path>/<name> — e.g. bldr/daemon/tests/unit. A target's name is where it was declared. |
kind | What it is, which decides what can be done with it. |
labels | Free-form labels the build attached, for the targets that carry any. |
ControllerDiagnosticsEvent
Diagnostics the build's tooling produced, as they are found.
Its own repeatable event rather than something attached to the terminal result, because the value here is timeliness: a typecheck that runs for two minutes should report its first error at three seconds, and a build that fails is exactly the one whose diagnostics matter most — a payload that only rode a successful output would be missing whenever it was wanted. The node appends each event to what it already has; an event is never the complete set, and a controller that has nothing new to say sends nothing.
message ControllerDiagnosticsEvent {
repeated CodeDiagnostic diagnostics = 1;
}| Field | |
|---|---|
diagnostics | The diagnostics found since the previous such event on this stream. Batching is the controller's choice: one event per tool invocation costs less than one per finding, and neither changes what the node does. |
CodeDiagnostic
One thing a build's tooling said about one place in the source.
Mirrored field-for-field from builder.CodeDiagnostic rather than imported,
exactly as BuildTarget and TargetKind above are: this file is the whole
contract a controller binary compiles against, and making it depend on the
node's own API proto would drag the entire client surface along with it. The
node translates one to the other verbatim.
Every positional field is optional in the sense that 0 means "not reported". A tool that knows only the file must still be able to say so — a diagnostic with no line is worth strictly more than no diagnostic.
message CodeDiagnostic {
optional string member = 1;
optional string path = 2;
optional uint32 line = 3;
optional uint32 column = 4;
optional uint32 end_line = 5;
optional uint32 end_column = 6;
optional DiagnosticLevel level = 7;
optional string title = 8;
optional string description = 9;
optional string snippet = 10;
optional uint32 snippet_start_line = 11;
optional string code = 12;
optional string tool = 13;
optional string target = 14;
repeated RelatedDiagnostic related = 15;
repeated DiagnosticFix fixes = 16;
optional map<string, string> labels = 17;
optional string root = 18;
optional uint64 offset = 19;
optional uint64 end_offset = 20;
}| Field | |
|---|---|
member | Workspace member the file belongs to, e.g. bldr/ui. Empty when the controller could not attribute it to one. |
path | Path within the member, relative and without a leading slash, e.g. src/App.tsx. When member is empty this is whatever path the tool gave, verbatim, since anything else would be an invention. |
line | 1-based. 0 means the tool did not report a position. |
column | |
end_line | 1-based, inclusive end of the span. 0 for a point diagnostic. |
end_column | |
level | |
title | One line, no trailing newline — what a list shows. |
description | The full text, including whatever notes and hints the tool attached. Empty when it adds nothing to title. |
snippet | Source lines around the diagnostic, verbatim and newline-separated, so a consumer can render context without fetching the file — which it may not be able to do at all. Empty when the controller chose not to include one. |
snippet_start_line | 1-based line number of snippet's first line. 0 when there is no snippet. |
code | The tool's own code, e.g. TS2322 or clippy::needless_borrow. Empty when the tool has no such concept. |
tool | What produced it, e.g. tsc, clippy, rustc. Names a tool, not a version. |
target | The build target it came out of, when the controller knows — bldr/ui/check. Empty otherwise. |
related | Mirrors of builder.CodeDiagnostic's later fields, so a controller that has them can pass them through rather than flattening them away. See that message for what each means; the node forwards them untouched. |
fixes | |
labels | |
root | |
offset | |
end_offset |
RelatedDiagnostic
Mirrors builder.RelatedDiagnostic.
message RelatedDiagnostic {
optional string message = 1;
optional DiagnosticSpan location = 2;
}DiagnosticSpan
Mirrors builder.DiagnosticSpan.
message DiagnosticSpan {
optional string member = 1;
optional string path = 2;
optional uint32 line = 3;
optional uint32 column = 4;
optional uint32 end_line = 5;
optional uint32 end_column = 6;
optional uint64 offset = 7;
optional uint64 end_offset = 8;
optional string root = 9;
}DiagnosticFix
Mirrors builder.DiagnosticFix.
message DiagnosticFix {
optional string message = 1;
optional string applicability = 2;
}ControllerLspHeader
What an LSP session over build code needs before it can answer anything.
The controller resolves a workspace's members from a BuildInput — that is
how it learns which members exist, where their .bldr/ sources are, and what
paths map their imports resolve through. A build carries one; an editor
session does not, so the node supplies the workspace's current input here.
It is not a build. Nothing is executed, no pods start: the input is used only to index the build code the language service then answers about. When the workspace changes on disk the node syncs a new input and sends a fresh header, which is what keeps an editing session current.
message ControllerLspHeader {
optional string input_cid = 1;
}| Field | |
|---|---|
input_cid | The workspace's current BuildInput CID. |
PrepareInput
Call a node's input mapper with the outputs of its dependencies.
message PrepareInput {
optional uint64 call_id = 1;
optional string node_id = 2;
optional uint64 mapper_id = 3;
optional map<string, string> dependency_outputs = 4;
}| Field | |
|---|---|
call_id | Names this call; the answer carries it back. |
node_id | The node whose input is wanted. |
mapper_id | The mapper the node declared. |
dependency_outputs | Output CID (string form) of each dependency, by dependency node id. |
ControllerGraph
A set of nodes declared together, and the targets that point at them.
message ControllerGraph {
repeated GraphNode nodes = 1;
repeated GraphTarget targets = 2;
}GraphNode
One declared node: everything about it except its input.
message GraphNode {
optional string id = 1;
optional string function = 2;
optional string label = 3;
optional map<string, string> labels = 4;
repeated string dependencies = 5;
oneof input {
uint64 mapper_id = 6;
ConstantBlock constant_input = 7;
}
oneof _target_prefix {
string target_prefix = 8;
}
}| Field | |
|---|---|
id | Unique in the build. A subgraph's nodes are scoped under their boundary node's id with a /. |
function | The node function to invoke, or execute-graph for a node whose mapper returns a subgraph. |
label | Human-readable label. |
labels | Free-form key/value metadata. |
dependencies | Ids of the nodes whose outputs this node needs. |
mapper_id | An input mapper in this controller's isolate. The id means nothing outside that isolate and is never stored or hashed. |
constant_input | An input known at declaration. |
target_prefix | Set on an execute-graph node whose subgraph declares targets: every target it declares has this prefix. |
ConstantBlock
A block the controller authored, for the node to store: a JSON value in
the form of InputPrepared, and the type to mint its CID with (a DataType
label such as "deployment"; empty for generic DAG-CBOR).
message ConstantBlock {
optional string json = 1;
optional string data_type = 2;
}GraphTarget
A named pointer at a node.
message GraphTarget {
optional BuildTarget target = 1;
optional string select = 3;
oneof artifact {
string node_id = 2;
ConstantBlock constant = 4;
}
}| Field | |
|---|---|
target | Name, kind and labels, as on every target. |
node_id | The node whose output is this target's artifact. |
select | A dag-path expression (docs/dag-paths.md) selecting the artifact inside the node's output block, e.g. .cid for a function that answers { cid: <link> }. Empty: the output block itself. |
constant | A block authored by the build code: a deployment, a runnable, a pipeline. The target's artifact is the stored block. |
InputPrepared
What an input mapper produced.
message InputPrepared {
optional uint64 call_id = 1;
oneof result {
string input_json = 2;
ControllerSubgraph subgraph = 3;
string error = 4;
}
}| Field | |
|---|---|
call_id | The PrepareInput this answers. |
input_json | The input, as a JSON value the node turns into blocks. A CID link is { "/": "<cid>" } and a byte string { "/": { "bytes": [...] } }. |
subgraph | For an execute-graph node: the subgraph to execute. |
error | The mapper threw. The message carries the JavaScript error and stack. |
ControllerSubgraph
The subgraph an execute-graph node's mapper returned.
message ControllerSubgraph {
optional ControllerGraph graph = 1;
optional string result_node_id = 2;
}| Field | |
|---|---|
graph | |
result_node_id | The node whose output becomes the execute-graph node's output. |
ControllerTaskState
enum ControllerTaskState {
CONTROLLER_TASK_RUNNING = 0;
CONTROLLER_TASK_COMPLETED = 1;
CONTROLLER_TASK_FAILED = 2;
CONTROLLER_TASK_QUEUED = 3;
CONTROLLER_TASK_CANCELLED = 4;
CONTROLLER_TASK_WAITING = 5;
CONTROLLER_TASK_WAITING_FOR_RESOURCES = 6;
}| Value | |
|---|---|
CONTROLLER_TASK_RUNNING | |
CONTROLLER_TASK_COMPLETED | |
CONTROLLER_TASK_FAILED | |
CONTROLLER_TASK_QUEUED | Node opened before its work exists: it shows in the tree straight away, as queued, instead of appearing only once it becomes runnable. |
CONTROLLER_TASK_CANCELLED | Never ran, or never finished, because the build failed elsewhere. Not a failure of its own — it has no error to report. |
CONTROLLER_TASK_WAITING | Started, and blocked on something outside itself (a lock another task holds, a rate limit). wait.reason says what. Non-terminal. |
CONTROLLER_TASK_WAITING_FOR_RESOURCES | Held at the node's admission gate: the host has no capacity to start it yet. wait carries the shortfall. Non-terminal; RUNNING follows. A node that predates this value reads it as RUNNING, which is what it showed for a held task anyway. |
TargetListStatus
How much to trust the target list.
enum TargetListStatus {
TARGET_LIST_STATUS_UNSPECIFIED = 0;
TARGET_LIST_STATUS_GENERATING = 1;
TARGET_LIST_STATUS_FAILED = 2;
TARGET_LIST_STATUS_DONE = 3;
TARGET_LIST_STATUS_STALE = 4;
}| Value | |
|---|---|
TARGET_LIST_STATUS_UNSPECIFIED | |
TARGET_LIST_STATUS_GENERATING | The controller is working on it. Nothing to show yet, but something is coming — distinct from a workspace that genuinely declares nothing. |
TARGET_LIST_STATUS_FAILED | The list could not be produced: the build code did not type-check, or failed while declaring. error says why. This is a list failure, not necessarily a build failure. |
TARGET_LIST_STATUS_DONE | Fresh, and describes the build code as it currently stands. |
TARGET_LIST_STATUS_STALE | A watch build whose inputs have moved since the list was produced. The contents are still the last good answer — worth showing, worth marking, and not worth blanking the page over. |
TargetKind
What a target is. Decides which verb applies to it: an output is exported, a test is run for its verdict, a runnable is started, a deployment invoked.
enum TargetKind {
TARGET_KIND_UNSPECIFIED = 0;
TARGET_KIND_OUTPUT_DIRECTORY = 1;
TARGET_KIND_CONTAINER_IMAGE = 2;
TARGET_KIND_TEST = 3;
TARGET_KIND_RUNNABLE = 4;
TARGET_KIND_DEPLOYMENT = 5;
TARGET_KIND_COVERAGE = 6;
TARGET_KIND_METRICS = 7;
TARGET_KIND_DIAGNOSTICS = 8;
TARGET_KIND_PIPELINE = 9;
}| Value | |
|---|---|
TARGET_KIND_UNSPECIFIED | |
TARGET_KIND_OUTPUT_DIRECTORY | A filesystem tree (scope.addOutputDirectory). |
TARGET_KIND_CONTAINER_IMAGE | A container image (scope.addImage). |
TARGET_KIND_TEST | A test (scope.addTest) — building it is running it. |
TARGET_KIND_RUNNABLE | A runnable (scope.addRunnable), including language servers. |
TARGET_KIND_DEPLOYMENT | A deployment (scope.addDeployment). |
TARGET_KIND_COVERAGE | A coverage report (scope.addCoverage). Its own kind rather than an output directory because what a client does with it differs: there is a percentage to show and files to rank by it, neither of which a file tree offers. |
TARGET_KIND_METRICS | A metrics blob (scope.addMetrics): line-protocol rows queried via GetBuildMetrics. Same value as builder.proto's TargetKind — the daemon forwards the integer untouched. |
TARGET_KIND_DIAGNOSTICS | A diagnostics report (scope.addDiagnostics): the messages a tool emitted about code. Same value as builder.proto's TargetKind — the daemon forwards the integer untouched. |
TARGET_KIND_PIPELINE | A pipeline definition (scope.addPipelineTarget): a declarative pipeline authored by build code. Same value as builder.proto's TargetKind — the daemon forwards the integer untouched. |
DiagnosticLevel
Mirrors builder.DiagnosticLevel, numerically identical so the node's
translation is a copy and never a lookup table that can drift.
enum DiagnosticLevel {
DIAGNOSTIC_LEVEL_UNSPECIFIED = 0;
DIAGNOSTIC_LEVEL_ERROR = 1;
DIAGNOSTIC_LEVEL_WARNING = 2;
DIAGNOSTIC_LEVEL_INFO = 3;
}