Build a worker driver
A worker driver provisions the SSH-addressable machines the warm pool leases to agent runs: your own cloud, your VM fleet, a container runtime. This page is for extension authors. It covers the WorkerDriver contract, what the pool guarantees you for free, what your driver must guarantee in return, and the in-repo recipe to ship one.
The split is the point. The pool in @lorenz/worker-pool owns every lifecycle decision (leasing, reaping, spend caps, the ledger, crash recovery) and calls your driver for exactly four operations: provision, probe, destroy, list. You implement a backend; you never touch the engine. The contract lives in @lorenz/worker-sdk, which depends on @lorenz/domain alone, so an extension implementing a driver pulls in the SDK layer and nothing else.
The driver contract
A driver is a WorkerDriver (in packages/worker-sdk/src/types.ts). The pool calls only these members.
| Member | Signature | What you implement |
|---|---|---|
kind |
readonly string |
The selector matched against worker.worker_pool.driver. |
provision |
(req: ProvisionRequest) => Promise<WorkerDescriptor> |
Create or re-adopt one machine. Idempotent on req.workerId. |
probe |
(worker, { timeoutMs, signal? }) => Promise<WorkerHealth> |
Cheap readiness check. Returns { ok: false, reason } instead of throwing. |
destroy |
(worker, { timeoutMs, reason }) => Promise<void> |
Tear the machine down. Idempotent; a gone machine is tolerated. |
list |
() => Promise<WorkerDescriptor[]> |
The live inventory: provisioned minus destroyed. The pool's source of truth. |
capabilities |
readonly DriverCapabilities |
Three static booleans (see below). |
You register a WorkerDriverFactory, not a driver instance:
interface WorkerDriverFactory {
readonly kind: string;
create(options: Readonly<Record<string, unknown>>, deps: DriverDeps): WorkerDriver;
}
create receives the selected workers.<name> profile options verbatim (minus the driver key) and the pool's DriverDeps. It constructs the driver and validates its options, throwing an actionable error when they are unusable. That throw lands at pool construction, the same fail-loud startup point as an unregistered kind.
Capabilities
DriverCapabilities is three booleans the pool reads to decide how to treat your backend:
| Field | Meaning |
|---|---|
sshAddressable |
The workerHost you yield is an SSH destination. |
ephemeral |
Workers are disposable machines the pool may destroy. |
usesLedger |
Gate the write-ahead ledger. A cloud backend that bills per machine sets this so survivors recover across a daemon restart. |
The fake driver sets all three false. static-ssh sets { sshAddressable: true, ephemeral: false, usesLedger: false }. docker sets all three true.
Descriptors and requests
provision takes a ProvisionRequest and returns a WorkerDescriptor:
ProvisionRequest:{ workerId, affinityKey?, labels, timeoutMs, signal?, driverOptions? }.workerIdis the pool's idempotency key.affinityKeycarries a priorworkerHostso a retry can re-land on the same machine.labelsincludesPOOL_OWNED_LABEL(stamp it onto the machine).WorkerDescriptor:{ workerId, workerHost, driverRef, createdAtMs, labels, metadata }.workerHostis the SSH-addressable string the runner threads end to end.driverRefis your own handle (a machine id, a container id) thatdestroyandlistreconcile against.
WorkerHealth is { ok: true } | { ok: false; reason: string }. TeardownReason, passed to destroy, is one of ttl, idle, shrink, unhealthy, failed, drain, orphan.
What the pool gives you for free
You implement four operations. The pool builds the rest of the machinery on top of list() and your descriptors. You do not write any of this.
- Leasing. The pool selects an idle worker, stamps a lease, hands the
workerHostto the run, and settles the lease exactly once on release. A long single-turn run is never force-returned. - Warm top-up and reaping. A single serial reaper reconciles against
list(), reaps idle workers pastidle_reap_msabovemin, expires leases pastttl_ms, demotes workers that fail a probe, and tops the pool back up towardwarm. - Spend caps.
max_concurrent_workers,max_worker_seconds(lifetime), anddaily_worker_seconds(per UTC day) gate growth and acquisition. The pool bills worker-seconds per lease and persists the daily total to aspend.jsonsidecar. - Write-ahead ledger. When
usesLedgeris true and a ledger path is configured, the pool writes a provisional row before it callsprovisionand correlates it after, so a crash mid-provision leaves a recoverable trace. The ledger is inert (zero fs I/O) for drivers that do not setusesLedger. - Crash recovery. On
hydrate, the pool callslist(), re-adopts every survivor carryingPOOL_OWNED_LABELas idle, drops orphan ledger rows, and reseeds daily spend. A paid driver (usesLedgerorephemeral) that cannot list after retries fails startup loud withworker_pool_hydrate_failed, so a billing backend never proceeds blind. - Drain. On disable or shutdown, the pool rejects new acquires, awaits in-flight leases to a deadline, then force-destroys every worker so no paid machine leaks.
What your driver must guarantee
The pool's machinery is correct only if your driver holds up its end. The conformance suite (below) pins each of these.
provisionis idempotent onworkerId. A secondprovisionwith the sameworkerIdreturns the same descriptor and creates no duplicate inlist(). The pool retries;dockerkeys idempotency on a label on the live daemon and adopts a surviving container rather than double-launching.destroytolerates a gone machine. Destroying twice, or destroying a never-provisioned worker, is a no-op that never throws. Throw only on a transport failure the reaper should retry (dockerthrowsdocker_rm_failedwhen the daemon is unreachable, but swallowsNo such container).list()is the truth. It returns exactly the provisioned-minus-destroyed inventory. The pool reconciles, hydrates, and reaps against this list, so a stale or partiallist()corrupts ownership accounting.probegates, does not throw. A created-but-unreachable worker returns{ ok: false, reason }with a non-empty reason. Both SSH-addressable built-in drivers (static-sshanddocker) runprintf readyover SSH and map any non-zero exit or transport error took: false, so the reaper can demote it.- Round-trip
POOL_OWNED_LABEL. The constant is the literal stringlorenz.pool=worker-pool. The pool stamps it on everyprovisionrequest. If your driver isephemeral, every descriptor yourlist()returns MUST carry it back. The hydrate re-adoption and reaper reconcile gate keys on this label: a survivor missing it is never touched, so a leaked paid machine would leak forever. Non-ephemeral drivers (fixed-inventory, in-memory) own no disposable resource behind this gate, so the label is not load-bearing for them.
DriverDeps: you never import the engine
create receives DriverDeps, the only channel between the pool and your driver:
interface DriverDeps {
clock: ClockPort;
logEvent: (event: Record<string, unknown>) => void;
runSsh: SshRunner;
}
runSsh is the injected SSH runner. The pool wires in the real @lorenz/ssh implementation; your driver probes workers over SSH through deps.runSsh and never imports the engine ssh package. clock makes createdAtMs deterministic in tests. logEvent emits structured events. There are deliberately no workspace or hook deps here: a driver manages worker lifecycle only.
A transport the engine does not provide (a cloud SDK client, a Modal or E2B handle) is closed over by your extension's registration, never threaded through DriverDeps. The SDK surface stays stable as backends come and go.
The in-repo recipe
To ship a driver inside the repo:
Create the package. Add
extensions/<name>-worker. Thefakedriver lives inpackages/worker-sdkandstatic-sshlives inpackages/static-worker, butdocker, a built-in too, already lives underextensions/docker-worker, which is where your new backend goes. A dependency-cruiser rule (extensions-depend-on-sdk-layers-only) blocks an extension from reaching into engine packages, so depend on@lorenz/worker-sdkand@lorenz/domainonly.Implement the factory. Export a
class MyWorkerDriver implements WorkerDriverand aWorkerDriverFactorywhosecreateconstructs it fromoptionsanddeps:const KIND = "my-cloud"; export const myWorkerDriverFactory: WorkerDriverFactory = { kind: KIND, create: (options, deps) => new MyWorkerDriver(options, deps), };Register idempotently. Export a
register*function guarded so a second call is a no-op:export function registerMyWorkerDriver( registries: { workerDrivers?: WorkerDriverRegistry } = {}, ): void { const drivers = registries.workerDrivers ?? defaultWorkerDriverRegistry; if (drivers.get(KIND) === undefined) { drivers.register(myWorkerDriverFactory); } }WorkerDriverRegistry.registerthrowsworker driver already registered for kind: <kind>when a different factory claims the samekind.requirethrowsworker_pool_driver_unavailable: <kind>listing the known kinds when an operator names a driver nobody registered, orworker.worker_pool.driver is requiredwhen the key is unset.Wire it into the composition root.
registerBuiltinBackendsinapps/cli/src/daemon.tsis the single place builtin backends are registered. Add aregisterMyWorkerDriver({ workerDrivers })call there alongsideregisterFakeWorkerDriver,registerStaticSshWorkerDriver, andregisterDockerWorkerDriver. An operator then selects it withworker.worker_pool.driver: my-cloudand passes options through aworkers.<name>profile.Run the conformance suite. Import
runDriverConformanceSuitefrom the@lorenz/worker-sdk/conformancesubpath and invoke it at the top level of your test file. It registers its owndescribe/testblocks:import { runDriverConformanceSuite } from "@lorenz/worker-sdk/conformance"; runDriverConformanceSuite(() => new MyWorkerDriver(options, deps), { suiteName: "MyWorkerDriver", workerIds: ["worker-a", "worker-b"], makeProvisionRequest: (workerId) => ({ workerId, labels: [POOL_OWNED_LABEL], timeoutMs: 30_000, }), makeUnreachable: () => ({ driver: makeFailingDriver(), workerId: "worker-down" }), });makeDrivermust return a fresh driver per call so each case starts clean.makeUnreachableis optional: a driver that cannot represent a created-but-unreachable worker omits it and the probe-gating case is skipped. The suite asserts provision idempotency, destroy tolerance,list = provisioned − destroyed, thePOOL_OWNED_LABELround-trip on everyephemeraldriver'slist(), and probe gating.
The conformance source lives under src/ (not test/) on purpose, so it compiles to dist/ and every driver's own test can import it.
The fake driver as reference
FakeWorkerDriver in packages/worker-sdk/src/fake.ts is the SDK's reference implementation. It owns no real machines and touches no disk: every operation mutates a Map<workerId, WorkerDescriptor>, the workerHost is a synthetic fake://worker-<workerId>, and createdAtMs comes from the injected clock. Capabilities are all false. Read it to see the exact idempotency and tolerance behavior the suite expects in the smallest possible form.
It exposes per-worker failure injection (injectProbeFailure, injectProvisionFailure, injectDestroyFailure, and the matching clear* methods) so tests can drive provision/probe/destroy faults, including the conformance suite's unreachable-worker case. A fsWriteCount getter is structurally pinned at 0, so a test can assert the driver performed zero filesystem I/O.
static-ssh (packages/static-worker/src/index.ts) and docker (extensions/docker-worker/src/index.ts) are the two backends that talk to real infrastructure. Read static-ssh for the fixed-inventory shape (the address is the workerId, destroy forgets the address and never deletes a machine) and docker for a disposable, usesLedger backend that adopts surviving containers by label and force-removes a half-built one before rethrowing.
Loading a driver out of tree
You do not have to land a driver in the repo. A driver can ship as a standalone npm module and load by specifier at startup through an sdkVersion handshake, with no fork of Lorenz. That path, the defineWorkerDriver / assertWorkerDriverModule shape, and its trust boundary are covered in out-of-tree extensions.
See also
- Out-of-tree extensions - ship a driver as a module without forking the repo
- Warm worker pool - the operator-facing view of the pool that leases your workers
- Docker workers - the reference disposable, ledger-backed driver in action
- Extension points - the four SDK contracts and how backends register
- Configuration reference - every
worker.worker_poolandworkers.<name>key