MDK Logo

Workers

Workers as integration components — discovery model, capability contract, and adding hardware

Overview

This page introduces the Worker as a development component. It covers what a Worker owns, how Kernel discovers it, what the capability contract is, and how a Worker Plugin can be spun up to support new hardware.

Read this before integrating new hardware, configuring discovery, or building on top of the Worker protocol.

What a Worker owns

A Worker wraps a device library and exposes it to Kernel via the MDK Protocol. Workers are the integration handlers between physical hardware and @tetherto/mdk-kernel, and the unyielding source of truth for that hardware. @tetherto/mdk-kernel operates purely as a synchronized state machine over Worker-reported state — it never reads hardware directly.

Workers are passive: they become a reachable endpoint and wait. The Kernel initiates every call; Workers only ever respond.

Deployment topologies connection model details how this directionality shapes transport choices.

For approval-gated writes, Workers answer write.calls.request while the Kernel resolves candidate writes, then execute the approved write as a normal command.request.

Discovery model

Each Worker package supplies its own boot function that constructs its runtime internally (for example startWhatsminerWorker, or startVendorWorker if you're building your own on v1 — see Add hardware for what a v2 Worker Plugin does instead, which has no boot function at all) — there is no single generic startWorker(WorkerClass, opts) entry point. The code samples below use startYourWorker as a stand-in for whichever boot function (or WorkerRuntimeV2 call) your Worker uses.

How Kernel finds a Worker depends on the deployment topology you're running — that page has the diagrams and the trade-offs for choosing between single-process, local, and microservices. This section carries only the Worker-side code for each.

In all cases, the post-discovery sequence is identical — Kernel requests identity, registers the Worker, then queries its capabilities. Once connected, all three shapes use the same HyperswarmRPC transport and the same MDK Protocol envelope (command.request, telemetry.pull, and so on); only how Kernel first obtains the Worker's RPC public key differs. After a Worker reaches READY, the Kernel Scheduler initiates telemetry pulls and health checks over HRPC; the Worker remains passive throughout.

Single-process mode

Skips all network discovery. Register the runtime's public key directly with the live Kernel instance in the same process — no topic, no directory, no network lookup:

const kernel = await getKernel(opts)
const worker = await startYourWorker(opts)
await kernel.registerWorker(worker.runtime.getPublicKey())

Two behaviors differ from the other two modes:

  • Registration: the host module calls kernel.registerWorker() directly with the runtime's public key. The Worker reaches READY synchronously — no waitForDiscovery() required
  • Lifecycle: registration alone does not couple the Worker's shutdown to Kernel's — the host process that constructed the runtime owns its lifecycle in every mode. Push the Worker's stop() onto Kernel's _cleanup queue yourself if Kernel shutdown should cascade to it (see bootWorker for the pattern), or manage it directly in your own shutdown handler

Use this mode for the run a mining site tutorial and single-process deployments.

Local mode

In local mode, Kernel and Workers coordinate through a shared directory on the same machine (default <root>/.worker-keys/). No Hyperswarm topic is joined and no outbound internet connection is required.

Worker side: after runtime.start(), publish the runtime's RPC key to the shared directory with publishWorkerKey from @tetherto/mdk's local-discovery helpers. The entry is stable across restarts (the key is seed-derived), so restarting a Worker is a no-op from Kernel's perspective.

const { keysDir, publishWorkerKey } = require('@tetherto/mdk/backend/core/mdk/lib/local-discovery')

const worker = await startYourWorker(opts)
publishWorkerKey(keysDir(root), workerId, worker.runtime.getPublicKey().toString('hex'))

Kernel side: getKernel watches the directory with fs.watch and runs a full scan every four seconds. Each entry found triggers the normal discovery listener (Identity → Capability → Ready), the same sequence used in DHT mode.

const kernel = await getKernel({ discovery: { mode: 'local' } })

A custom directory can be passed when the default path is not suitable:

const kernel = await getKernel({ discovery: { mode: 'local', dir: '/shared/mdk-keys' } })
publishWorkerKey('/shared/mdk-keys', workerId, worker.runtime.getPublicKey().toString('hex'))

Keys persist across restarts and the directory is read again each time Kernel starts, so Workers and Kernel can start in any order without coordination.

All processes must share the same filesystem path. Local mode requires every component to run on the same machine — use DHT mode for Workers on separate hosts.

The Starter site example demonstrates local mode as its default multi-process setup — its config/site.deploy.json's discovery field defaults to "local", and switches to "dht" without any other code change.

Microservices mode

Also called DHT mode: instead of a shared directory or same-process registration, Kernel and the Worker join the same Hyperswarm topic — the mechanism production microservices and Workers on separate hosts or networks depend on. Generate a random 32-byte hex topic in whichever process starts first, persist it somewhere the other process can read it, and pass the same value to both sides:

const kernel = await getKernel({ topic: '<32-byte-hex>' })
const worker = await startYourWorker({ kernelTopic: '<32-byte-hex>', ...opts })

The Worker must join the topic before Kernel starts listening. Start the Worker process first, then start Kernel. waitForDiscovery() polls the registry until discovered Workers reach READY state.

The DHT pattern is demonstrated end-to-end by the full-site example's up --discovery dht.

Capability contract

mdk-contract.json is the canonical source of truth for a Worker's programmatic capabilities and its AI context. MDK deliberately merges formal validation and semantic guidance into a single JSON contract:

  • description does double duty as the human UI label and AI edge-case rule (for example, "Outlet temperature > 85C requires intervention")
  • constraints governs orchestration limits
  • troubleshooting provides if/then recovery behaviors alongside the payload it evaluates

The exhaustive JSON Schema is mdk-contract.schema.json, with a reference instance at mdk-contract.json.

Add hardware

External integrators add new hardware by building a Worker Plugin that conforms to the strict Device-Lib Contract:

  1. Reference mdk-contract.schema.json to author the mdk-contract.json, validating strict data schemas while injecting explanations, constraints, and troubleshooting directly into the relevant nodes.
  2. Build a Worker Plugin. The full build walkthrough is the source of truth for the current model: a package directory (mdk-contract.json + handler files, no connect/disconnect) hosted by pointing WorkerRuntimeV2 at it.
  3. Boot the Worker instance and register with @tetherto/mdk-kernel using the appropriate discovery mode. @tetherto/mdk-kernel detects the peer and pulls its identity and capabilities.

WorkerRuntime remains exported and supported: existing Workers built against the older { contract, dir, connect, disconnect? } object passed to new WorkerRuntime(plugin, opts) — for example whatsminer/plugin/index.js — still work unchanged; WorkerRuntimeV2 extends it and is the model for new hardware.

createModuleContext is the private-module-registry primitive that gives each plugin instance its own require cache, so module-level state (a client constructed at load time, say) belongs to that one instance alone. WorkerRuntimeV2, the Gateway, and the MCP server each build one per plugin; WorkerRuntime v1 has no notion of per-device isolation and does not use it.

v1 Worker Plugins

Legacy, but still what every Worker package under backend/workers/ ships today — Whatsminer, Antminer, Avalon, and the rest — not v2 above: a plugin object { contract, dir, connect, disconnect? } passed to new WorkerRuntime(plugin, opts) from @tetherto/mdk-worker, with handlers invoked as (ctx, params) instead of v2's ambient (params). connect/disconnect translate command.request and telemetry.pull calls into real device I/O — the one thing v1 does that v2 has no equivalent for, since v2 assumes reaching the device is the handler's own problem, resolved once at load time.

WorkerRuntime generalizes the former MDKWorkerAdapter (persistent seeds, single HRPC respond loop, DHT topic announce carried over) and replaces ThingManager delegation with per-device handler dispatch; see Worker Runtime legacy services for the full migration history and the optional built-in services surface. whatsminer/plugin/index.js is a reference v1 plugin implementing connect/disconnect against a real device.

Build new Worker Plugins on v2 above unless you specifically need v1's connect/disconnect device-transport model.

Next steps

On this page