@tetherto/mdk-skill 0.0.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +133 -0
  3. package/dist/skills/mdk/SKILL.md +114 -0
  4. package/dist/skills/mdk/references/architecture.md +89 -0
  5. package/dist/skills/mdk/references/glossary.md +27 -0
  6. package/dist/skills/mdk/references/mdk-contract.schema.json +138 -0
  7. package/dist/skills/mdk/references/package-index.md +53 -0
  8. package/dist/skills/mdk/references/protocol.md +95 -0
  9. package/dist/skills/mdk-create-app/SKILL.md +231 -0
  10. package/dist/skills/mdk-deployment/SKILL.md +161 -0
  11. package/dist/skills/mdk-gateway-plugin/SKILL.md +192 -0
  12. package/dist/skills/mdk-gateway-plugin/references/controller-patterns.md +99 -0
  13. package/dist/skills/mdk-gateway-plugin/references/plugin-authoring.md +121 -0
  14. package/dist/skills/mdk-publish-security/SKILL.md +177 -0
  15. package/dist/skills/mdk-publish-security/references/vuln-classes.md +151 -0
  16. package/dist/skills/mdk-publish-security/scripts/scan-packages.mjs +544 -0
  17. package/dist/skills/mdk-site-sizing/SKILL.md +160 -0
  18. package/dist/skills/mdk-site-sizing/eval/held-out.json +104 -0
  19. package/dist/skills/mdk-site-sizing/eval/profiles/cap-100devices-2workers-mixed.json +24 -0
  20. package/dist/skills/mdk-site-sizing/eval/profiles/cap-100devices-2workers.json +23 -0
  21. package/dist/skills/mdk-site-sizing/eval/profiles/cap-10devices-1workers.json +23 -0
  22. package/dist/skills/mdk-site-sizing/eval/profiles/cap-15devices-2workers.json +24 -0
  23. package/dist/skills/mdk-site-sizing/eval/profiles/cap-200devices-4workers.json +23 -0
  24. package/dist/skills/mdk-site-sizing/eval/profiles/cap-500devices-4workers.json +23 -0
  25. package/dist/skills/mdk-site-sizing/eval/profiles/cap-50devices-2workers.json +23 -0
  26. package/dist/skills/mdk-site-sizing/references/envelope.json +284 -0
  27. package/dist/skills/mdk-site-sizing/references/envelope.md +42 -0
  28. package/dist/skills/mdk-site-sizing/scripts/compile-envelope.mjs +45 -0
  29. package/dist/skills/mdk-site-sizing/scripts/eval-held-out.mjs +50 -0
  30. package/dist/skills/mdk-site-sizing/scripts/lib.mjs +591 -0
  31. package/dist/skills/mdk-site-sizing/scripts/parse-site.mjs +25 -0
  32. package/dist/skills/mdk-site-sizing/scripts/size-site.mjs +49 -0
  33. package/dist/skills/mdk-ui-component/SKILL.md +207 -0
  34. package/dist/skills/mdk-ui-component/references/page-recipe.md +78 -0
  35. package/dist/skills/mdk-ui-component/references/ui-registry.json +1 -0
  36. package/dist/skills/mdk-worker-plugin/SKILL.md +198 -0
  37. package/dist/skills/mdk-worker-plugin/assets/mdk-contract.template.json +45 -0
  38. package/dist/skills/mdk-worker-plugin/assets/worker-template/README.md +93 -0
  39. package/dist/skills/mdk-worker-plugin/assets/worker-template/mdk-contract.json +26 -0
  40. package/dist/skills/mdk-worker-plugin/assets/worker-template/mock/server.js +93 -0
  41. package/dist/skills/mdk-worker-plugin/assets/worker-template/package.json +23 -0
  42. package/dist/skills/mdk-worker-plugin/assets/worker-template/smoke.config.js +41 -0
  43. package/dist/skills/mdk-worker-plugin/assets/worker-template/src/client.js +47 -0
  44. package/dist/skills/mdk-worker-plugin/assets/worker-template/src/commands/reboot.js +5 -0
  45. package/dist/skills/mdk-worker-plugin/assets/worker-template/src/commands/set-power-mode.js +10 -0
  46. package/dist/skills/mdk-worker-plugin/assets/worker-template/src/telemetry/hashrate-rt.js +5 -0
  47. package/dist/skills/mdk-worker-plugin/assets/worker-template/src/telemetry/power-mode.js +5 -0
  48. package/dist/skills/mdk-worker-plugin/assets/worker-template/src/telemetry/power.js +5 -0
  49. package/dist/skills/mdk-worker-plugin/assets/worker-template/src/telemetry/temperature.js +5 -0
  50. package/dist/skills/mdk-worker-plugin/references/contract-authoring.md +135 -0
  51. package/dist/skills/mdk-worker-plugin/references/device-families.md +96 -0
  52. package/dist/skills/mdk-worker-plugin/references/local-testing.md +134 -0
  53. package/dist/skills/mdk-worker-plugin/references/mdk-contract.schema.json +138 -0
  54. package/dist/skills/mdk-worker-plugin/references/worker-base-api.md +152 -0
  55. package/dist/skills/mdk-worker-plugin/scripts/validate-contract.mjs +129 -0
  56. package/dist/skills/mdk-worker-plugin/scripts/worker-smoke.mjs +256 -0
  57. package/package.json +47 -1
  58. package/src/assemble.mjs +11 -0
  59. package/src/index.d.ts +57 -0
  60. package/src/index.mjs +254 -0
  61. package/src/install.mjs +57 -0
  62. package/src/sources.map.json +34 -0
@@ -0,0 +1,135 @@
1
+ # Authoring `mdk-contract.json`
2
+
3
+ Load this when writing or reviewing a Worker contract. The machine-checkable
4
+ shape is [`mdk-contract.schema.json`](../../../../../../backend/core/mdk-worker/mdk-contract.schema.json) (bundled beside this file as
5
+ `references/mdk-contract.schema.json`); validate with
6
+ [`../scripts/validate-contract.mjs`](../scripts/validate-contract.mjs); this file covers the semantics the schema
7
+ can't express. Real contracts to imitate:
8
+ [`packages/cli/templates/worker/mdk-contract.json`](../../../../../cli/templates/worker/mdk-contract.json) (minimal scaffold / skill
9
+ template), [`backend/workers/miners/avalon/plugin/mdk-contract.json`](../../../../../../backend/workers/miners/avalon/plugin/mdk-contract.json)
10
+ (full-featured; older `plugin/` layout),
11
+ [`backend/workers/temperature/seneca/plugin/mdk-contract.json`](../../../../../../backend/workers/temperature/seneca/plugin/mdk-contract.json) (read-only
12
+ sensor; older `plugin/` layout).
13
+
14
+ The contract is dual-purpose: the Kernel validates commands against it, and AI
15
+ operator agents read it as context. Every prose field you write is
16
+ simultaneously documentation and agent instructions.
17
+
18
+ ## `metadata`
19
+
20
+ All five fields required:
21
+
22
+ ```json
23
+ "metadata": {
24
+ "provider": "microbt", // vendor slug, lowercase
25
+ "deviceFamily": "miner", // miner | power-meter | sensor | container | minerpool
26
+ "brand": "Whatsminer", // human-readable
27
+ "modelsSupported": ["M53S", "M56S"],
28
+ "overview": "…" // what the worker controls + critical environmental constraints
29
+ }
30
+ ```
31
+
32
+ `overview` is the agent's first impression — state what the device is, which
33
+ protocol the Worker wraps, and any site-level caution (e.g. hydro miners must
34
+ not run without coolant flow).
35
+
36
+ ## `capabilities.telemetry`
37
+
38
+ One entry per channel:
39
+
40
+ ```json
41
+ { "name": "temperature", "unit": "C", "type": "number",
42
+ "handler": "src/telemetry/temperature.js",
43
+ "description": "Hash board temperature. If 0, device is booting." }
44
+ ```
45
+
46
+ - `name` is the wire name — consumers pull it via
47
+ `telemetry.pull { query: { type: 'metrics' } }` (all channels) or
48
+ `{ query: { type: 'temperature' } }` (just this one). `Snake_case` by
49
+ convention (`hashrate_rt`, `power_mode`).
50
+ - `type` is one of `number | string | boolean | array | object` and is what
51
+ the handler must return (the smoke harness asserts this).
52
+ - Channels that take params (e.g. a `history` channel with `{ limit }`)
53
+ document them in `description` — extra `query` fields are passed to the
54
+ handler as `params`.
55
+ - `unit` whenever numeric (`TH/s`, `W`, `C`, `V`, `kW`).
56
+ - `description` must carry semantic boundaries an agent can reason over:
57
+ sentinel values, staleness, validity ranges.
58
+
59
+ ## `capabilities.commands`
60
+
61
+ ```json
62
+ { "name": "setPowerPct",
63
+ "description": "Set power percentage.",
64
+ "constraints": "Do not call more than once per 5 minutes.",
65
+ "handler": "src/commands/set-power-pct.js",
66
+ "params": [ { "name": "pct", "type": "number", "min": 0, "max": 200 } ] }
67
+ ```
68
+
69
+ - `name` is camelCase by convention (`reboot`, `setPowerMode`, `setupPools`).
70
+ - **Bounds rule:** every `number` param declares `min` and `max`. The Kernel's
71
+ dispatcher rejects out-of-range values with `ERR_PARAM_RANGE` *only for
72
+ declared bounds*
73
+ ([`backend/core/kernel/lib/modules/command-dispatcher/index.js`](../../../../../../backend/core/kernel/lib/modules/command-dispatcher/index.js)); nothing
74
+ else protects the hardware from `setLiquidSupplyTemperature(9000)`. The
75
+ bundled validator treats a missing bound as an error.
76
+ - `constraints` is a strict semantic limit in prose — rate limits, approval
77
+ requirements ("Requires two approvals."), ordering rules. Agents obey it;
78
+ reviewers check it.
79
+ - `examples` (optional) — `{ intent, steps[] }` workflows teaching an agent
80
+ the optimal use of a non-obvious command.
81
+ - Params arrive at your handler as a named object (`params.pct`). Positional
82
+ legacy payloads (`{ value }` / `{ args: [] }`) are normalized onto declared
83
+ names by the runtime — another reason to declare params accurately.
84
+ - Read-only devices: `"commands": []` (see the ABB / Seneca contracts).
85
+
86
+ ## `capabilities.health`
87
+
88
+ ```json
89
+ "health": {
90
+ "supportedStates": ["OK", "OFFLINE"],
91
+ "alerts": ["overheat", "fan_failure"],
92
+ "troubleshooting": ["If temperature > 85 for 5m, reduce power mode before rebooting."]
93
+ }
94
+ ```
95
+
96
+ `supportedStates` is required. `alerts` and `troubleshooting` are optional but
97
+ production contracts declare them — `troubleshooting` is an if-then
98
+ remediation list for agents.
99
+
100
+ ## `capabilities.errors`
101
+
102
+ Every `ERR_*` code your handlers or device client can throw, mapped to a
103
+ human/AI-readable explanation:
104
+
105
+ ```json
106
+ "errors": {
107
+ "ERR_BAD_POWER_MODE": "The firmware rejected the requested power mode.",
108
+ "ERR_DEVICE_CALL_FAILED": "The device API call failed or returned an error."
109
+ }
110
+ ```
111
+
112
+ Errors thrown by handlers travel verbatim in `command.result`/telemetry
113
+ payloads — keeping this map complete is what makes them actionable downstream.
114
+
115
+ ## What does NOT go in the contract
116
+
117
+ - **No device list.** Devices are supplied at runtime
118
+ (`WorkerRuntime opts.devices`); the schema rejects a top-level `devices` key.
119
+ - **No secrets** (credentials live in per-device `config`).
120
+ - **No handler implementation detail** — `handler` paths are stripped before
121
+ the contract leaves the process.
122
+
123
+ ## Validation
124
+
125
+ ```bash
126
+ node ../scripts/validate-contract.mjs <worker>/mdk-contract.json
127
+ ```
128
+
129
+ Checks schema conformance, handler file existence (relative to the contract's
130
+ directory), duplicate telemetry/command names, and the numeric-bounds rule.
131
+ Exit 0 = clean. Pass `--schema-only` to skip the semantic checks and test shape
132
+ alone. The bundled schema is a verbatim copy of the runtime's
133
+ [`backend/core/mdk-worker/mdk-contract.schema.json`](../../../../../../backend/core/mdk-worker/mdk-contract.schema.json); the repo-wide catalogue
134
+ ([`backend/workers/scripts/generate-catalogue.js`](../../../../../../backend/workers/scripts/generate-catalogue.js)) lints shipped
135
+ contracts against that same file.
@@ -0,0 +1,96 @@
1
+ # Device families — what exists, what to copy
2
+
3
+ **Load this only when the user explicitly asks to follow the existing MDK
4
+ Worker families and their structure** — typically when contributing a Worker
5
+ into the MDK monorepo itself, or deliberately mirroring how a shipped family
6
+ handles its device protocol. The families below are tightly coupled to the
7
+ mining use cases they were built for; a third party integrating their own
8
+ device should NOT start from them — the default starting point is
9
+ `../assets/worker-template/` (step 1 of the skill workflow).
10
+
11
+ All paths under [`backend/workers/`](../../../../../../backend/workers/README.md). Every family below is a
12
+ Worker Plugin on the older module-exporting shape (`plugin/index.js` →
13
+ `{ contract, dir, connect, disconnect? }`); production families additionally
14
+ ship `plugin/boot.js`, which wires Worker-infra services and exposes a
15
+ `start<Family>Worker()` helper. This reflects what's actually shipped today,
16
+ not a recommendation — a **new** Worker should follow the directory-loaded
17
+ `WorkerRuntimeV2` model instead (`mdk-contract.json` + `src/` handlers, no
18
+ `plugin/index.js`, no `connect()`), the shape `mdk create worker`'s scaffold
19
+ uses. See this skill's main authoring flow
20
+ ([`SKILL.md`](../SKILL.md) steps 5-6) and
21
+ [`worker-base-api.md`](./worker-base-api.md#directory-loaded-plugins-workerruntimev2)
22
+ for that model; the rest of this page describes the older shape's conventions.
23
+
24
+ | Family | Path | Protocol | Telemetry / commands | Copy it when… |
25
+ | --------------- | ---- | -------- | -------------------- | ------------- |
26
+ | **Avalon** | [`miners/avalon`](../../../../../../backend/workers/miners/avalon/README.md) | CGMiner ASCII over TCP | 9 / 4 | Plain-text TCP command APIs |
27
+ | **ABB** | [`power-meter/abb`](../../../../../../backend/workers/power-meter/abb/README.md) | Modbus TCP | 9 / 0 | Modbus register maps; **multi-model**: `MODEL_CLASSES` lookup keyed off `config.model` (B23/B24/M1M20/M4M20/REU615) |
28
+ | **Satec / Schneider** | [`power-meter/satec`](../../../../../../backend/workers/power-meter/satec/README.md), [`power-meter/schneider`](../../../../../../backend/workers/power-meter/schneider/README.md) | Modbus TCP | read-only | Same shape as ABB, single/dual model |
29
+ | **Seneca** | [`temperature/seneca`](../../../../../../backend/workers/temperature/seneca/README.md) | Modbus TCP | 2 / 0 | Minimal read-only sensor; per-device `register` in config; fault-sentinel semantics (850.0) in the contract |
30
+ | **Antspace** | [`containers/antspace`](../../../../../../backend/workers/containers/antspace/README.md) | HTTP JSON | 5 / 5 | Cooling containers; model variants (hydro/immersion); approval-gated commands via `constraints` |
31
+ | **Bitdeer** | [`containers/bitdeer`](../../../../../../backend/workers/containers/bitdeer/README.md) | MQTT | — | Subscription-style transports |
32
+ | **f2pool/ocean** | [`minerpools/f2pool`](../../../../../../backend/workers/minerpools/f2pool/README.md), [`minerpools/ocean`](../../../../../../backend/workers/minerpools/ocean/README.md) | Pool HTTP APIs | — | Non-hardware "device" integrations |
33
+
34
+ Whatsminer (CGMiner JSON over TCP, AES-encrypted, token auth) is no longer an in-repo family to copy from — it's the
35
+ external [`whatsminer-mdk-worker`](https://github.com/whatsminer/whatsminer-mdk-worker) contract plugin, hosted on
36
+ `WorkerRuntimeV2` via a thin per-deployment adapter (see `examples/mvp-site/backend/whatsminer-adapter.js`). Antminer
37
+ (HTTP CGI, Digest auth) is likewise the external [`antminer-mdk-worker`](https://github.com/bitmain-official/antminer-mdk-worker)
38
+ contract plugin, hosted the same way (see `examples/mvp-site/backend/antminer-adapter.js`).
39
+
40
+ Do **not** copy from `miners/wm-v3` (empty stub) or model anything on the
41
+ `ThingManager`/`MinerManager` class architecture in [`backend/workers/README.md`](../../../../../../backend/workers/README.md)
42
+ — that's the legacy pre-plugin model.
43
+
44
+ ## Anatomy shared by the module-exporting families
45
+
46
+ Doesn't apply to the `mdk create worker` scaffold, whose flat directory-loaded layout is
47
+ described in [`worker-base-api.md`](./worker-base-api.md#directory-loaded-plugins-workerruntimev2)
48
+ instead.
49
+
50
+ ```
51
+ <family>/
52
+ ├── index.js # Exports { plugin, start<Family>Worker?, <DeviceClass>? }
53
+ ├── plugin/
54
+ │ ├── index.js # The Worker Plugin
55
+ │ ├── boot.js # Production families: worker-infra hosting helper
56
+ │ ├── mdk-contract.json
57
+ │ └── src/{telemetry,commands}/*.js
58
+ ├── lib/ # Device client class (plain vendor I/O; multi-model families add lib/models/)
59
+ ├── mock/server.js # Firmware simulator
60
+ ├── config/ # Sample device configs (production families)
61
+ └── tests/{unit,integration}/
62
+ ```
63
+
64
+ Conventions that hold across every module-exporting family:
65
+
66
+ - `connect(config, { deviceId })` validates required config keys up front
67
+ (throwing `ERR_DEVICE_CONFIG_INVALID`-style errors), builds the device
68
+ client, probes it once, returns it. `disconnect` exists wherever the client
69
+ holds a socket.
70
+ - Multi-model families select a class from a `MODEL_CLASSES` map keyed by
71
+ `config.model` / `config.type` (see ABB, Antspace).
72
+ - Handler files mirror contract names 1:1 under `plugin/src/`.
73
+ - Numeric safety is expressed as param `min`/`max` (Whatsminer `setPowerPct`)
74
+ or an approval `constraints` string (Antspace `resetCoolingSystem`:
75
+ "Requires two approvals."). Prefer declaring both for physical setpoints.
76
+ - Tests are brittle, split `tests/unit/` (handlers against the mock) and
77
+ `tests/integration/` (plugin hosted end-to-end).
78
+
79
+ ## The shared mock framework
80
+
81
+ [`backend/workers/mock/`](../../../../../../backend/workers/mock/README.md)
82
+
83
+ Per-family mocks are thin leaves over `@tetherto/mdk-worker-mock`:
84
+ `BaseMock` (CLI, initial-state loading, lifecycle) → category mock
85
+ ([`miner.mock.js`](../../../../../../backend/workers/mock/miner.mock.js) resolves commands to `cmds/<command>.js` files,
86
+ [`powermeter.mock.js`](../../../../../../backend/workers/mock/powermeter.mock.js) pins Modbus,
87
+ [`sensor.mock.js`](../../../../../../backend/workers/mock/sensor.mock.js) reuses it,
88
+ [`container.mock.js`](../../../../../../backend/workers/mock/container.mock.js),
89
+ [`minerpool.mock.js`](../../../../../../backend/workers/mock/minerpool.mock.js)) → device leaf
90
+ (`<family>/mock/server.js`, e.g. `class WhatsminerMock extends MinerMock`
91
+ with `static dir/TYPES/defaultPort` and a `createTransport()` override).
92
+ Transports live in [`mock/transports/`](../../../../../../backend/workers/mock/transports/) (tcp, http, modbus, mqtt).
93
+
94
+ A standalone Worker (like the template) can instead ship a self-contained
95
+ `mock/server.js` exporting `createServer(opts) → { server, state, exit }` —
96
+ that's the pattern the smoke harness and template tests use.
@@ -0,0 +1,134 @@
1
+ # Local testing — no site required
2
+
3
+ Load this when verifying a Worker. A Worker is site-agnostic, so the entire
4
+ loop below runs on your machine against the Worker's own device mock. Run the
5
+ steps in order; each catches a different failure class.
6
+
7
+ ## 1. Contract validation (static, no runtime)
8
+
9
+ ```
10
+ node <this-skill>/scripts/validate-contract.mjs <worker>/mdk-contract.json
11
+ ```
12
+
13
+ Schema conformance + handler-file existence + duplicate names + the
14
+ numeric-bounds rule. Exit 0/1. Fix and re-run until clean — everything later
15
+ assumes a valid contract.
16
+
17
+ ## 2. In-process smoke (plugin + mock, no Kernel, no DHT)
18
+
19
+ ```
20
+ node <this-skill>/scripts/worker-smoke.mjs <worker-dir>
21
+ ```
22
+
23
+ The harness loads `@tetherto/mdk-worker` as the worker dir resolves it (run
24
+ `npm install` there first). Only if that fails does it fall back to
25
+ `backend/core/mdk-worker` in the MDK checkout that contains the skill itself,
26
+ never above that repo's root. `MDK_WORKER_BASE=<dir>` overrides both.
27
+
28
+ The harness looks for `<worker-dir>/smoke.config.js` (the template ships one):
29
+ `setup()` boots the device mock and returns `{ config, commands?, teardown }`.
30
+ The harness then:
31
+
32
+ 1. Loads the contract through the real `loadContract` (catching handler wiring
33
+ errors exactly as `WorkerRuntimeV2` construction would).
34
+ 2. Builds one device instance via `createInstance` and calls **every declared
35
+ telemetry handler**, asserting each returns a defined value of the
36
+ contract-declared type.
37
+ 3. Replicates the Kernel dispatcher's param validation to assert that every
38
+ bounded numeric param **rejects below-min / above-max** and accepts the
39
+ bounds themselves.
40
+ 4. Executes each command listed in `smoke.config.js` `commands` with its
41
+ sample in-bounds params and asserts success.
42
+
43
+ Alternative to `smoke.config.js`: pass `--config '<json>'` (or a path to a
44
+ `.json`/`.js` config module) if the mock is already running.
45
+
46
+ ## 3. Standalone protocol check (WorkerRuntimeV2, real envelopes, still no Kernel)
47
+
48
+ Host the package's directory on the runtime and drive envelopes through
49
+ `handleRequest` — this exercises envelope dispatch and multi-device routing.
50
+ Minimal caller (run the
51
+ body inside an `async function main ()` in a `.js` file, or as-is in an
52
+ `.mjs`):
53
+
54
+ ```js
55
+ 'use strict'
56
+ const path = require('path')
57
+ const WorkerRuntimeV2 = require('<repo>/backend/core/mdk-worker/lib/worker-runtime-v2')
58
+
59
+ const runtime = new WorkerRuntimeV2(path.resolve('<your-worker-dir>'), {
60
+ workerId: 'standalone-check',
61
+ devices: [{ deviceId: 'dev-0', config: { host: '127.0.0.1', port: MOCK_PORT } }]
62
+ })
63
+ await runtime.start() // no kernelTopic → HRPC up, but nothing announced
64
+
65
+ const res = await runtime.handleRequest({
66
+ id: 'req-1', version: '0.2.0', type: 'request', action: 'telemetry.pull',
67
+ sender: 'standalone-check', target: null, deviceId: 'dev-0',
68
+ timestamp: Date.now(), payload: { query: { type: 'metrics' } }
69
+ })
70
+ console.log(res.payload.metrics)
71
+ await runtime.stop()
72
+ ```
73
+
74
+ Check here: `identity.request`, `capability.request` (handler paths must be
75
+ stripped), `telemetry.pull` per channel, `command.request` success and
76
+ `FAILED` paths, and behavior with the mock stopped (the affected channel's
77
+ handler call errors; siblings and other channels are unaffected — there is no
78
+ boot-time probe and no offline state to hold the device in).
79
+
80
+ ## 4. Site integration (a real Kernel)
81
+
82
+ In a CLI-managed project (Worker scaffolded with `mdk create worker`, listed
83
+ under `mdk.yaml` → `spec.workers`), this step is just `mdk run worker <name>`
84
+ followed by `mdk run kernel` (or `mdk run` to boot both together) — see
85
+ `mdk-deployment`. `mdk status` then confirms registration.
86
+
87
+ Otherwise, your Worker package doesn't need to live in the MDK monorepo for
88
+ this step — a caller can require it from anywhere; you only need a local
89
+ Kernel to register against. In a checkout of the MDK monorepo, drop the Worker into the
90
+ mvp-site example: [`examples/mvp-site/deploy/run-process.js`](../../../../../../examples/mvp-site/deploy/run-process.js)
91
+ runs one PM2-supervised process per role (mocks, Kernel, each Worker, Gateway) — add a role that boots your Worker
92
+ and list it under `pm2.roles` in `config/site.deploy.json` (the `worker-antminer` role and
93
+ [`examples/mvp-site/backend/antminer-adapter.js`](../../../../../../examples/mvp-site/backend/antminer-adapter.js) show the
94
+ third-party-plugin shape, no worker-infra plumbing).
95
+ Confirm the Kernel registers it, telemetry flows, and commands round-trip.
96
+
97
+ ## Unit tests (accompany every step)
98
+
99
+ Pattern (`tests/unit/handlers.test.js`): boot the mock on a free port, build one
100
+ device instance with `createInstance({ dir, entries: loadContract(dir).entries,
101
+ device })`, then call handlers directly off `instance.telemetry.get(name)` /
102
+ `instance.commands.get(name)` — plugin-level tests never need `WorkerRuntimeV2`
103
+ (loadContract/createInstance-level wiring is covered by [`worker-smoke.mjs`](../scripts/worker-smoke.mjs)
104
+ already, so these tests are free to focus on handler behavior):
105
+
106
+ ```js
107
+ const { loadContract, createInstance } = require('<repo>/backend/core/mdk-worker')
108
+
109
+ const dir = path.resolve('<your-worker-dir>')
110
+ const instance = createInstance({
111
+ dir,
112
+ entries: loadContract(dir).entries,
113
+ device: { id: 'test-0', opts: { host: '127.0.0.1', port: MOCK_PORT }, env: {}, config: {} }
114
+ })
115
+
116
+ const value = await instance.telemetry.get('hashrate_rt')({})
117
+ const result = await instance.commands.get('setPowerMode')({ mode: 'eco' })
118
+ ```
119
+
120
+ [`backend/workers/miners/avalon/tests/unit/miner.test.js`](../../../../../../backend/workers/miners/avalon/tests/unit/miner.test.js)
121
+ is a worked example of brittle tests in this repo. Runner is **brittle** (no mocha, no
122
+ jest). It must be a `devDependency` of the worker, pinned like the rest of the
123
+ repo (`"brittle": "3.18.0"`); `--no-install` makes `npx` fail instead of
124
+ downloading an unpinned copy when it is missing:
125
+
126
+ ```
127
+ npm install --save-dev --save-exact brittle@3.18.0 # once, if package.json lacks it
128
+ npx --no-install brittle tests/unit/*.test.js
129
+ ```
130
+
131
+ Cover at minimum: every telemetry handler against mock values, every command's
132
+ effect on mock state, a telemetry handler's error when nothing listens (there
133
+ is no boot-time probe to hold the device offline instead), and one
134
+ firmware-error path surfacing its contract `ERR_*` code.
@@ -0,0 +1,138 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$comment": "This schema describes a static Worker Plugin contract. Runtime device IDs and connection configuration belong to the host process and are reported dynamically by WorkerRuntime identity responses; they are intentionally not embedded here.",
4
+ "title": "MDK Device-Lib Contract Schema",
5
+ "description": "Formal JSON Schema defining the unified programmatic and semantic capabilities contract for MDK device integration workers.",
6
+ "type": "object",
7
+ "required": [
8
+ "metadata",
9
+ "capabilities"
10
+ ],
11
+ "additionalProperties": false,
12
+ "properties": {
13
+ "$schema": { "type": "string" },
14
+ "metadata": {
15
+ "type": "object",
16
+ "description": "Identity and family metadata defining what the worker represents.",
17
+ "required": ["provider", "deviceFamily", "brand", "modelsSupported", "overview"],
18
+ "properties": {
19
+ "provider": { "type": "string", "description": "The corporate or individual provider behind the worker (e.g., 'microbt')." },
20
+ "deviceFamily": { "type": "string", "description": "The broad category of devices (e.g., 'miner', 'sensor', 'switchgear')." },
21
+ "brand": { "type": "string", "description": "The human-readable brand name of the device." },
22
+ "modelsSupported": {
23
+ "type": "array",
24
+ "items": { "type": "string" },
25
+ "description": "List of specific models this specific contract supports."
26
+ },
27
+ "overview": {
28
+ "type": "string",
29
+ "description": "Mandatory semantic overview. Used to instruct AI agents on the broad purpose and critical environmental constraints for this device group."
30
+ }
31
+ }
32
+ },
33
+ "capabilities": {
34
+ "type": "object",
35
+ "description": "The unified machine and semantic capabilities mapping for the device.",
36
+ "required": ["telemetry", "commands", "health", "errors"],
37
+ "properties": {
38
+ "telemetry": {
39
+ "type": "array",
40
+ "description": "Metrics continuously updated and read from the device. Natural semantic rules should be appended to the 'description' field.",
41
+ "items": {
42
+ "type": "object",
43
+ "required": ["name", "type", "handler", "description"],
44
+ "properties": {
45
+ "name": { "type": "string" },
46
+ "unit": { "type": "string" },
47
+ "type": { "type": "string", "enum": ["number", "string", "boolean", "object", "array"] },
48
+ "handler": { "type": "string", "description": "CommonJS handler path relative to the Worker Plugin directory. Removed from the contract published to Kernel." },
49
+ "description": { "type": "string", "description": "Human and AI readable metric description. Should include semantic boundaries (e.g., 'If 0, device is booting')." }
50
+ }
51
+ }
52
+ },
53
+ "commands": {
54
+ "type": "array",
55
+ "description": "Callable operations the device accepts.",
56
+ "items": {
57
+ "type": "object",
58
+ "required": ["name", "handler"],
59
+ "properties": {
60
+ "name": { "type": "string" },
61
+ "handler": { "type": "string", "description": "CommonJS handler path relative to the Worker Plugin directory. Removed from the contract published to Kernel." },
62
+ "description": { "type": "string", "description": "What the command does physically." },
63
+ "constraints": { "type": "string", "description": "Strict semantic limits (e.g., 'Do not call more than once every 15 minutes')." },
64
+ "examples": {
65
+ "type": "array",
66
+ "description": "Concrete AI instruction workflows for how to utilize this command optimally.",
67
+ "items": {
68
+ "type": "object",
69
+ "required": ["intent", "steps"],
70
+ "properties": {
71
+ "intent": { "type": "string", "description": "The goal or overarching reason for executing the workflow." },
72
+ "steps": { "type": "array", "items": { "type": "string" }, "description": "Step-by-step numbered execution plan for the AI." }
73
+ }
74
+ }
75
+ },
76
+ "params": {
77
+ "type": "array",
78
+ "description": "Input arguments required by the command.",
79
+ "items": {
80
+ "type": "object",
81
+ "required": ["name", "type"],
82
+ "properties": {
83
+ "name": { "type": "string" },
84
+ "type": { "type": "string", "enum": ["number", "string", "boolean", "object"] },
85
+ "required": { "type": "boolean", "description": "Whether the command handler requires this parameter." },
86
+ "enum": {
87
+ "type": "array",
88
+ "description": "Allowed values. WorkerRuntime rejects a command whose value is not listed (ERR_PARAM_ENUM) before the handler runs.",
89
+ "items": {}
90
+ },
91
+ "min": { "type": "number", "description": "Allowed minimum value (for numbers)." },
92
+ "max": { "type": "number", "description": "Allowed maximum value (for numbers)." }
93
+ }
94
+ }
95
+ },
96
+ "verify": {
97
+ "type": "object",
98
+ "description": "Device-state readback after the command settles. Kernel pulls the `telemetry` metric until it equals the request's `param` value (verification VERIFIED) or `withinMs` passes (UNVERIFIED).",
99
+ "required": ["telemetry", "param"],
100
+ "properties": {
101
+ "telemetry": { "type": "string", "description": "Name of a telemetry entry that reports the state this command sets." },
102
+ "param": { "type": "string", "description": "Name of the command param holding the requested value." },
103
+ "withinMs": { "type": "number", "minimum": 1, "description": "How long to keep reading back before UNVERIFIED. Default 60000." }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ },
109
+ "config": {
110
+ "type": "object",
111
+ "description": "Device configuration models.",
112
+ "properties": {
113
+ "schema": { "type": "string", "description": "Reference identifier for config payload shape." }
114
+ }
115
+ },
116
+ "health": {
117
+ "type": "object",
118
+ "description": "Standardized diagnostic mappings.",
119
+ "required": ["supportedStates"],
120
+ "properties": {
121
+ "supportedStates": { "type": "array", "items": { "type": "string" } },
122
+ "alerts": { "type": "array", "items": { "type": "string" } },
123
+ "troubleshooting": {
124
+ "type": "array",
125
+ "items": { "type": "string" },
126
+ "description": "Semantic AI reasoning array. Provides if-then remediation workflows for specific failures."
127
+ }
128
+ }
129
+ },
130
+ "errors": {
131
+ "type": "object",
132
+ "description": "Lookup table mapping specific device error codes to human/AI-readable text.",
133
+ "additionalProperties": { "type": "string" }
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }