Build registry
The build registry is a catalog of pipelines and where they live. Each
build registers a small versioned BuildDescriptor — its id,
CLI surface, and launch location — into a pluggable store. Point
zuke mcp --registry at that store and it re-reads the
catalog on every tools/list and tools/call, so a
newly-registered pipeline shows up as a runnable tool with
no restart.
BuildDescriptor & BuildRegistry
A registry is anything that satisfies BuildRegistry: get one
build, register (or update) one with optimistic-concurrency versioning,
deregister one, or list them by BuildQuery.
export type PutBuildResult = { ok: true; version: string } | { ok: false; conflict: true };
export interface BuildRegistry {
getBuild(id: string): Promise<{ descriptor: BuildDescriptor; version: string } | null>;
register(descriptor: BuildDescriptor, expectedVersion: string | null): Promise<PutBuildResult>;
deregister(id: string): Promise<void>;
listBuilds(query: BuildQuery): Promise<BuildSummary[]>;
}
export type BuildLocation =
| { kind: "module"; module: string; cwd: string; repo?: string }
| { kind: "command"; command: string[]; cwd: string; repo?: string };
export interface BuildDescriptor {
id: string; name: string; location: BuildLocation;
surface: CliDescription; // exactly describeCli(build) — flags only, no values
actor: string; createdAt: string; updatedAt: string;
}
export interface BuildQuery { name?: string; since?: string }
A descriptor is secret-free by construction: surface is
exactly describeCli(build) (flags only, no values),
module-URL credentials are stripped, and register resolves
no parameter values.
FS vs HTTP backends
Two backends ship, both behind the same BuildRegistry
interface:
| Backend | Storage |
|---|---|
FileSystemBuildRegistry | constructor(dir: string, host?) — defaults to
.zuke/builds, one <id>.json file per
build, written with an O_EXCL+rename compare-and-swap.
|
HttpBuildRegistry | { url: string; token?: string; fetch?: typeof fetch }
— GET/PUT/DELETE /builds/:id and
GET /builds, ETag/If-Match compare-and-swap, bearer
auth.
|
Override registry() on a build to point it at an HTTP-backed
registry — this is the same shape as the run state
store, but a separate concern (.zuke/builds sits beside
.zuke/runs; over HTTP it rides a /builds REST
contract beside /runs):
class CD extends Build {
registryUrl = parameter("registry URL").required();
registryToken = parameter("registry token").secret();
override registry() {
return new HttpBuildRegistry({ url: this.registryUrl.value, token: this.registryToken.value });
}
}
Resolution & env vars
Which registry a build uses is resolved in order:
- An explicit registry passed in code (
false disables it). - The build's
registry() override. -
Env:
ZUKE_REGISTRY_URL (+ ZUKE_REGISTRY_TOKEN)
selects an HTTP registry; otherwise ZUKE_REGISTRY_DIR
selects an FS registry.
-
For
zuke register and zuke mcp --registry, an
FS registry under .zuke/builds by default.
Env var Effect ZUKE_REGISTRY_URLSelects the HTTP backend; base URL for the /builds contract. ZUKE_REGISTRY_TOKENBearer token sent with every HTTP registry request. ZUKE_REGISTRY_DIRSelects the FS backend at this directory (default .zuke/builds).
Registering a build
zuke register writes (or idempotently updates) this build's
descriptor into the resolved registry, excluding secrets:
deno run -A zuke.ts register # writes .zuke/builds/<id>.json by default
deno run -A zuke.ts register --json
zuke mcp --registry --allow-run
Dynamic MCP discovery
zuke mcp --registry serves the whole catalog instead of one
build. Registry-mode discovery adds two shapes to the
tools already documented for zuke
mcp: list_builds returns the catalog, and
describe_build (given a build id) returns one
build's surface. Each registered target becomes a
run:<buildId>:<target> tool,
re-read live — a build registered after the server started appears with
no restart.
Execution here is a spawn of the descriptor's registered
launch location, not a call into a live instance — so it's off unless
--allow-run is set, exactly like the
single-build server. Authorization
is matched against the qualified <buildId>:<target>
name, e.g. --allow-run=Api:* or
--protect=Api:deploy.
Run tool parameters
A run tool exposes the registered build's declared parameters as its
input schema — keyed by the parameter's property name (e.g.
skipE2e), with the kind, description, enum, and default from
the descriptor. Supplied values are validated against their kinds
before the build spawns (a type mismatch — a bare string
where an array is required, a non-numeric number, an out-of-set enum, or
an unknown parameter — is a clean tool error, not a failed subprocess),
then forwarded to the child as --flag=value arguments
alongside the target. A value still set in the server's environment
applies when the call omits it. Because a descriptor does not record
whether a target is read-only, every registry run tool is treated as
destructive.
Secrets never cross the boundary
.secret() parameters are omitted from the descriptor
entirely (zuke register writes the secret-free surface), so
a secret can neither be requested nor forwarded — it is rejected as an
unknown parameter if a client tries. The spawned build resolves a secret
from its own environment / .from() source instead. In the
audit log only a recognised parameter's
value is recorded; any unknown argument keeps its name but its value is
elided, so a value mistakenly supplied under a secret's name is never
written to the durable trail.
The spawned build inherits the server's environment minus ZUKE_OPERATOR_TOKEN and ZUKE_MCP_TOKEN, so a
spawned pipeline can never read the very tokens gating it.
Concurrency + cap
Unlike the single-build server, the registry server handles requests
concurrently — a read tool (list_builds,
describe_build) is never blocked behind a running
run: call, and independent runs proceed in parallel.
Concurrent run-tool spawns are capped (default 4,
--max-concurrent-runs <n>); a call past the cap gets an
immediate structured at_capacity busy error
({ running, cap, hint }) rather than an unbounded queue.
Read tools are never counted against the cap. Cross-process state safety
rides the store's CAS, so no new coordination is introduced.