@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.
- package/LICENSE +202 -0
- package/README.md +133 -0
- package/dist/skills/mdk/SKILL.md +114 -0
- package/dist/skills/mdk/references/architecture.md +89 -0
- package/dist/skills/mdk/references/glossary.md +27 -0
- package/dist/skills/mdk/references/mdk-contract.schema.json +138 -0
- package/dist/skills/mdk/references/package-index.md +53 -0
- package/dist/skills/mdk/references/protocol.md +95 -0
- package/dist/skills/mdk-create-app/SKILL.md +231 -0
- package/dist/skills/mdk-deployment/SKILL.md +161 -0
- package/dist/skills/mdk-gateway-plugin/SKILL.md +192 -0
- package/dist/skills/mdk-gateway-plugin/references/controller-patterns.md +99 -0
- package/dist/skills/mdk-gateway-plugin/references/plugin-authoring.md +121 -0
- package/dist/skills/mdk-publish-security/SKILL.md +177 -0
- package/dist/skills/mdk-publish-security/references/vuln-classes.md +151 -0
- package/dist/skills/mdk-publish-security/scripts/scan-packages.mjs +544 -0
- package/dist/skills/mdk-site-sizing/SKILL.md +160 -0
- package/dist/skills/mdk-site-sizing/eval/held-out.json +104 -0
- package/dist/skills/mdk-site-sizing/eval/profiles/cap-100devices-2workers-mixed.json +24 -0
- package/dist/skills/mdk-site-sizing/eval/profiles/cap-100devices-2workers.json +23 -0
- package/dist/skills/mdk-site-sizing/eval/profiles/cap-10devices-1workers.json +23 -0
- package/dist/skills/mdk-site-sizing/eval/profiles/cap-15devices-2workers.json +24 -0
- package/dist/skills/mdk-site-sizing/eval/profiles/cap-200devices-4workers.json +23 -0
- package/dist/skills/mdk-site-sizing/eval/profiles/cap-500devices-4workers.json +23 -0
- package/dist/skills/mdk-site-sizing/eval/profiles/cap-50devices-2workers.json +23 -0
- package/dist/skills/mdk-site-sizing/references/envelope.json +284 -0
- package/dist/skills/mdk-site-sizing/references/envelope.md +42 -0
- package/dist/skills/mdk-site-sizing/scripts/compile-envelope.mjs +45 -0
- package/dist/skills/mdk-site-sizing/scripts/eval-held-out.mjs +50 -0
- package/dist/skills/mdk-site-sizing/scripts/lib.mjs +591 -0
- package/dist/skills/mdk-site-sizing/scripts/parse-site.mjs +25 -0
- package/dist/skills/mdk-site-sizing/scripts/size-site.mjs +49 -0
- package/dist/skills/mdk-ui-component/SKILL.md +207 -0
- package/dist/skills/mdk-ui-component/references/page-recipe.md +78 -0
- package/dist/skills/mdk-ui-component/references/ui-registry.json +1 -0
- package/dist/skills/mdk-worker-plugin/SKILL.md +198 -0
- package/dist/skills/mdk-worker-plugin/assets/mdk-contract.template.json +45 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/README.md +93 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/mdk-contract.json +26 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/mock/server.js +93 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/package.json +23 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/smoke.config.js +41 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/src/client.js +47 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/src/commands/reboot.js +5 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/src/commands/set-power-mode.js +10 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/src/telemetry/hashrate-rt.js +5 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/src/telemetry/power-mode.js +5 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/src/telemetry/power.js +5 -0
- package/dist/skills/mdk-worker-plugin/assets/worker-template/src/telemetry/temperature.js +5 -0
- package/dist/skills/mdk-worker-plugin/references/contract-authoring.md +135 -0
- package/dist/skills/mdk-worker-plugin/references/device-families.md +96 -0
- package/dist/skills/mdk-worker-plugin/references/local-testing.md +134 -0
- package/dist/skills/mdk-worker-plugin/references/mdk-contract.schema.json +138 -0
- package/dist/skills/mdk-worker-plugin/references/worker-base-api.md +152 -0
- package/dist/skills/mdk-worker-plugin/scripts/validate-contract.mjs +129 -0
- package/dist/skills/mdk-worker-plugin/scripts/worker-smoke.mjs +256 -0
- package/package.json +47 -1
- package/src/assemble.mjs +11 -0
- package/src/index.d.ts +57 -0
- package/src/index.mjs +254 -0
- package/src/install.mjs +57 -0
- 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
|
+
}
|