@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,198 @@
1
+ ---
2
+ name: mdk-worker-plugin
3
+ description: >
4
+ Integrate a new device into MDK as a device Worker. Use for any task like
5
+ "new device / miner / power meter / sensor / container Worker", "build a
6
+ Worker", "integrate hardware", "wrap a device protocol (Modbus / CGMiner /
7
+ HTTP / MQTT)", or anything creating or editing an mdk-contract.json, a
8
+ Worker Plugin, or telemetry/command handlers under backend/workers/.
9
+ metadata:
10
+ suite: mdk-developer-skill
11
+ mdk_version: "0.7.0"
12
+ license: Apache-2.0
13
+ ---
14
+
15
+ # Integrate a new MDK device Worker
16
+
17
+ A device Worker is a **Worker Plugin**: a package directory holding an
18
+ `mdk-contract.json` plus one small handler module per telemetry channel and
19
+ per command. It exports no module of its own. `WorkerRuntimeV2`
20
+ (`@tetherto/mdk-worker`) loads the directory and instantiates the plugin once
21
+ per device — handlers are plain `(params)` functions that read device context
22
+ from `require('@tetherto/mdk-worker/device')`. Workers are **site-agnostic**:
23
+ they know nothing about the site they run in, so everything here is testable
24
+ locally against a device mock before a Kernel ever exists.
25
+
26
+ The final package shape (this is `assets/worker-template/`, a verbatim copy of
27
+ `packages/cli/templates/worker/` — the same tree `mdk create worker` scaffolds):
28
+
29
+ ```
30
+ <your-worker>/
31
+ ├── mdk-contract.json # single source of truth (package root)
32
+ ├── package.json # no main/exports — the runtime loads the dir
33
+ ├── smoke.config.js # glue for scripts/worker-smoke.mjs
34
+ ├── src/
35
+ │ ├── client.js # vendor-protocol I/O via ambient device context
36
+ │ ├── telemetry/<name>.js # one file per telemetry channel
37
+ │ └── commands/<name>.js # one file per command
38
+ └── mock/server.js # vendor firmware simulator
39
+ ```
40
+
41
+ ## Workflow
42
+
43
+ ### 1. Start from the Worker template
44
+
45
+ The default starting point for every new Worker is `assets/worker-template/`
46
+ — a complete minimal Worker Plugin (contract + client + handlers + mock) with
47
+ no assumptions about your device category.
48
+
49
+ Do **not** model a new Worker on the MDK monorepo's shipped device families
50
+ (Whatsminer, Antminer, ABB, Seneca, …) by default — those still use the older
51
+ `plugin/` layout and are tightly coupled to the mining use cases they were
52
+ built for. Only when the user **explicitly asks** to follow the existing MDK
53
+ Worker families and their structure (e.g. contributing a Worker into this
54
+ monorepo, or deliberately mirroring a shipped family's protocol handling)
55
+ should you read [`references/device-families.md`](./references/device-families.md)
56
+ and copy from the named family instead.
57
+
58
+ ### 2. Scaffold
59
+
60
+ In a CLI-managed project, scaffold and register in one step:
61
+
62
+ ```bash
63
+ mdk create worker <name>
64
+ ```
65
+
66
+ This copies the CLI Worker template into `workers/<name>`, links it as an npm
67
+ workspace member (root `npm install`, skip with `--no-install`), and appends
68
+ it under `mdk.yaml` → `spec.workers` with a `mock: true` seed device (skip
69
+ with `--no-stack-entry`) — so it is runnable immediately with
70
+ `mdk run worker <name>` before you change a line.
71
+
72
+ Outside the CLI (e.g. contributing inside the monorepo, or no `mdk.yaml`
73
+ project), copy `assets/worker-template/` to your Worker's location instead.
74
+ Either way, rename the template-specific parts (contract metadata, client
75
+ methods, mock behavior). Remember to adapt `smoke.config.js` (mock boot,
76
+ device opts, sample command params) and `package.json` (name, description).
77
+ The plugin is location-independent — put the package wherever your project
78
+ keeps packages. (Only Workers contributed to the MDK monorepo itself follow
79
+ its `backend/workers/<category>/<family>/` layout.)
80
+
81
+ ### 3. Author `mdk-contract.json`
82
+
83
+ Start from [`assets/mdk-contract.template.json`](./assets/mdk-contract.template.json); the field-by-field guide is
84
+ [`references/contract-authoring.md`](./references/contract-authoring.md) — load it now. Non-negotiables:
85
+
86
+ - Every telemetry/command entry needs a `handler` path (e.g. `src/telemetry/<name>.js`)
87
+ — the loader hard-fails without it.
88
+ - **Every `number` command param must declare `min` and `max`.** The Kernel
89
+ enforces bounds only when declared (`ERR_PARAM_RANGE`); an undeclared bound
90
+ is an unprotected physical setpoint.
91
+ - Read-only devices (meters, sensors) declare `"commands": []`.
92
+ - `description` / `overview` / `constraints` are simultaneously machine docs
93
+ and AI operator context — write real semantics ("If 0, device is booting"),
94
+ and put safety thresholds in the contract, not in code comments.
95
+
96
+ ### 4. Validate the contract — loop until clean
97
+
98
+ ```
99
+ node <this-skill>/scripts/validate-contract.mjs <worker>/mdk-contract.json
100
+ ```
101
+
102
+ Validates against `references/mdk-contract.schema.json` plus semantic
103
+ checks (handler files exist, no duplicate names, numeric params bounded).
104
+ Fix and re-run until it exits 0. Handler files must exist first — stubs from
105
+ the template are enough at this stage.
106
+
107
+ ### 5. Build the mock, then the device client
108
+
109
+ Write `mock/server.js` first — a standalone simulator of the vendor firmware
110
+ API that knows nothing about MDK. The template's self-contained HTTP JSON
111
+ mock is the pattern to follow; adapt it to your device's protocol. (Workers
112
+ living inside the MDK monorepo may instead reuse its shared mock framework in
113
+ [`backend/workers/mock/`](../../../../../backend/workers/mock/README.md).) Then write `src/client.js`: plain vendor-protocol
114
+ I/O. Bind to ambient device context
115
+ (`require('@tetherto/mdk-worker/device')` → `{ opts, env, config, logger }`)
116
+ rather than exporting a `createClient(opts)` factory — the runtime loads the
117
+ module fresh per device. Throw `ERR_*` errors. No envelopes, no base classes.
118
+
119
+ ### 6. Wire the handlers
120
+
121
+ There is no `connect()` and no `plugin/index.js`. Each handler is
122
+ `async (params) => result` and talks to the device through `src/client.js`
123
+ (or by reading ambient context itself). Telemetry returns the channel value
124
+ (matching the contract's declared `type`); commands return a result object or
125
+ throw an `ERR_*` error. Full runtime semantics: [`references/worker-base-api.md`](references/worker-base-api.md).
126
+
127
+ Template pattern:
128
+
129
+ ```js
130
+ 'use strict'
131
+ const client = require('../client')
132
+ module.exports = async () => (await client.getSummary()).hashrate_ths
133
+ ```
134
+
135
+ ### 7. Test locally — the four-step loop
136
+
137
+ Load [`references/local-testing.md`](./references/local-testing.md) for the full procedure. In order:
138
+
139
+ 1. **Contract validation** — step 4 above (no runtime).
140
+ 2. **In-process smoke** — `node <this-skill>/scripts/worker-smoke.mjs <worker-dir>`
141
+ boots your plugin against your mock (via `smoke.config.js`), asserts every
142
+ declared telemetry channel returns a value of the declared type, and that
143
+ out-of-bounds command params are rejected. No Kernel, no DHT.
144
+ 3. **Standalone protocol check** — host the package on `WorkerRuntimeV2` with a
145
+ tiny caller and drive real envelopes through `handleRequest`.
146
+ 4. **Site integration** — add the Worker to a site stack (e.g.
147
+ [`examples/mvp-site/deploy/run-process.js`](../../../../../examples/mvp-site/deploy/run-process.js))
148
+ and confirm a real Kernel registers it end-to-end.
149
+
150
+ ### 8. Boot into a site
151
+
152
+ In a CLI-managed project this is just `mdk run worker <name>` (or `mdk run`
153
+ to boot the whole stack together) — see `mdk-deployment`. The rest of this
154
+ section is the manual path, for when nothing owns the runtime yet.
155
+
156
+ A caller process owns the runtime (the plugin package itself never does):
157
+
158
+ ```js
159
+ const { WorkerRuntimeV2 } = require('@tetherto/mdk-worker')
160
+ const runtime = new WorkerRuntimeV2(pkgDir, {
161
+ workerId: 'my-worker-1',
162
+ devices: [{ deviceId: 'dev-0', opts: { host: '127.0.0.1', port: 18080 } }],
163
+ kernelTopic // hex topic → DHT announce; omit for local-only
164
+ })
165
+ await runtime.start()
166
+ ```
167
+
168
+ The Worker joins the DHT topic; the Kernel pulls identity + capabilities and
169
+ starts scheduling telemetry/health. **No Kernel changes are ever needed to add
170
+ a Worker.**
171
+
172
+ ## Gotchas
173
+
174
+ - **Unidirectional protocol**: a Worker never calls the Kernel — it only
175
+ answers. If you think you need to push, you need a telemetry channel that
176
+ the Kernel pulls.
177
+ - **`deviceId` ownership is exclusive** per Worker; duplicate ids in
178
+ `opts.devices` throw `ERR_DEVICE_ID_DUPLICATE` at construction.
179
+ - One device offline must never affect siblings — failures stay inside that
180
+ device's instance (V2 has no boot-time probe; unreachable devices surface
181
+ as errors in telemetry payloads rather than `ERR_DEVICE_UNAVAILABLE`).
182
+ - Use `debug` / the ambient `logger`, never `console.log`, in Worker code.
183
+ Errors are `ERR_SCREAMING_SNAKE` strings.
184
+ - The published contract strips `handler` paths; everything else in the
185
+ contract is visible to sites and AI operators — write it accordingly.
186
+
187
+ ## Hand-off
188
+
189
+ This skill stops at a working Worker Plugin + contract. For prompts that also
190
+ ask to **show** the data in a UI:
191
+
192
+ | Next need | Skill |
193
+ | -------------------------------------------- | -------------------- |
194
+ | Aggregate / expose HTTP API over this Worker | `mdk-gateway-plugin` |
195
+ | Register Worker in `mdk.yaml` and run it | `mdk-deployment` |
196
+ | Dashboard page / tiles / charts | `mdk-ui-component` |
197
+
198
+ See the composite routing table in [`../mdk/SKILL.md`](../mdk/SKILL.md).
@@ -0,0 +1,45 @@
1
+ {
2
+ "metadata": {
3
+ "provider": "TODO-vendor-slug",
4
+ "deviceFamily": "TODO: miner | power-meter | sensor | container | minerpool",
5
+ "brand": "TODO-Brand",
6
+ "modelsSupported": ["TODO_MODEL"],
7
+ "overview": "TODO: what the worker controls, which vendor protocol it wraps, and any critical environmental constraint an AI operator must know."
8
+ },
9
+ "capabilities": {
10
+ "telemetry": [
11
+ {
12
+ "name": "temperature",
13
+ "unit": "C",
14
+ "type": "number",
15
+ "handler": "src/telemetry/temperature.js",
16
+ "description": "TODO: what this reads, plus semantic boundaries (e.g. 'If 0, device is booting')."
17
+ },
18
+ {
19
+ "name": "status_text",
20
+ "type": "string",
21
+ "handler": "src/telemetry/status-text.js",
22
+ "description": "TODO: enumerate the possible values and what each means."
23
+ }
24
+ ],
25
+ "commands": [
26
+ {
27
+ "name": "setSetpoint",
28
+ "description": "TODO: what this does physically.",
29
+ "constraints": "TODO: rate limits / approval requirements, or delete this field.",
30
+ "handler": "src/commands/set-setpoint.js",
31
+ "params": [
32
+ { "name": "value", "type": "number", "min": 0, "max": 100 }
33
+ ]
34
+ }
35
+ ],
36
+ "health": {
37
+ "supportedStates": ["OK", "OFFLINE"],
38
+ "alerts": [],
39
+ "troubleshooting": []
40
+ },
41
+ "errors": {
42
+ "ERR_DEVICE_CALL_FAILED": "The device API call failed or returned an error."
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,93 @@
1
+ # __WORKER_NAME__
2
+
3
+ An MDK **Worker Plugin** — the code that translates between MDK and a family of
4
+ physical devices. It ships only an [`mdk-contract.json`](./mdk-contract.json) (its capability manifest)
5
+ plus [`src/`](./src/) handler files, a device "vendor SDK" client, and a mock device
6
+ server. It exports no module of its own; it is **directory-loaded** by
7
+ `WorkerRuntimeV2`, which the MDK CLI constructs (`mdk run worker <name>`) — the
8
+ package never constructs or depends on the runtime itself.
9
+
10
+ This scaffold is a working example that talks to a hypothetical device over a
11
+ tiny HTTP JSON API. Replace the device client, mock, contract, and handlers with
12
+ your own device's protocol.
13
+
14
+ ## Layout
15
+
16
+ ```
17
+ mdk-contract.json # capability manifest (telemetry channels + commands)
18
+ src/client.js # your device "vendor SDK" — binds to this device once, per instance
19
+ src/telemetry/*.js # telemetry handlers (metrics read live from the device)
20
+ src/commands/*.js # command handlers (reboot, set-power-mode, ...)
21
+ mock/server.js # a standalone device simulator (createServer)
22
+ smoke.config.js # the device spec `mdk validate worker --smoke` checks against
23
+ ```
24
+
25
+ Handlers are plain `(params)` functions — they call `require('../client')` for
26
+ their device's client, which itself binds once (at `require` time) to the
27
+ ambient `@tetherto/mdk-worker/device` for this instance's connection opts.
28
+ There is one such module registry per configured device, so each handler
29
+ always talks to its own device.
30
+
31
+ ## Run it
32
+
33
+ Add the worker to your stack's `mdk.yaml` by **local path** and describe its
34
+ seed devices under `config`:
35
+
36
+ ```yaml
37
+ spec:
38
+ workers:
39
+ - name: __WORKER_NAME__
40
+ package: ./workers/__WORKER_NAME__
41
+ config:
42
+ mock: true # start mock/server.js per device (dev only)
43
+ devices:
44
+ - id: dev-0
45
+ opts: { host: 127.0.0.1, port: 18080, serial: DEV-0 }
46
+ ```
47
+
48
+ Then:
49
+
50
+ ```bash
51
+ mdk run worker __WORKER_NAME__ # boots the worker (+ its mocks with mock: true)
52
+ ```
53
+
54
+ With `mock: true` the CLI starts one simulator from [`mock/server.js`](./mock/server.js) per device
55
+ so the worker talks to a simulator instead of hardware. Drop `mock` (or set it
56
+ `false`) and point `opts.host`/`opts.port` at real devices for production.
57
+
58
+ ## Check it
59
+
60
+ ```bash
61
+ mdk validate worker __WORKER_NAME__ # the contract and handler files, without running them
62
+ mdk validate worker --path . --smoke # also runs every handler against the simulator
63
+ ```
64
+
65
+ `--smoke` starts the simulator through [`smoke.config.js`](./smoke.config.js), calls every telemetry
66
+ handler and each sample command it lists, then stops the simulator and expects every handler to fail
67
+ with an `ERR_*` error code. Add a sample to `commands` for each command you add, unless it interrupts
68
+ the device.
69
+
70
+ ## Builtin per-device health check
71
+
72
+ `WorkerRuntimeV2` answers a `telemetry.pull` query of `type: 'health'` for any
73
+ device automatically — no contract or handler changes needed. It works by
74
+ registering `health` as a plain handler in the same table any contract-declared
75
+ telemetry channel lives in, so it is dispatched with zero special-casing, the
76
+ same way `status`/`hashrate_rt`/etc. would be. That means it behaves exactly
77
+ like any other named channel:
78
+
79
+ - Requires a `deviceId` — a query with no `deviceId` returns
80
+ `{ error: 'ERR_DEVICE_ID_REQUIRED: health' }`, same as any other named channel.
81
+ - The response comes back in the standard named-channel shape,
82
+ `{ name: 'health', value: { status, id, opts, env, config, workerId } }` —
83
+ that device's own ambient config, useful for verifying that Gateway → Kernel
84
+ → Worker routing reaches the right device instance (e.g. a query addressed
85
+ to `dev-3` always comes back with `dev-3`'s own `opts`, never a sibling
86
+ device's).
87
+ - It also shows up as a `health` key inside the default `type: 'metrics'`
88
+ aggregate for every device, since that aggregate iterates the very same
89
+ handler table.
90
+
91
+ If your own contract declares a `health` telemetry channel, that one is used
92
+ instead — the builtin never overrides a plugin-declared handler of the same
93
+ name.
@@ -0,0 +1,26 @@
1
+ {
2
+ "metadata": {
3
+ "provider": "demo",
4
+ "deviceFamily": "device",
5
+ "brand": "DEMO",
6
+ "modelsSupported": ["DEMO_V1"],
7
+ "overview": "Template Worker Plugin — talks to a device's HTTP JSON API over src/client.js. Replace the client, mock, contract and handlers with your own device's protocol."
8
+ },
9
+ "capabilities": {
10
+ "telemetry": [
11
+ { "name": "hashrate_rt", "unit": "TH/s", "type": "number", "handler": "src/telemetry/hashrate-rt.js", "description": "Real-time throughput from the device's summary endpoint." },
12
+ { "name": "power", "unit": "W", "type": "number", "handler": "src/telemetry/power.js", "description": "Current power draw." },
13
+ { "name": "temperature", "unit": "C", "type": "number", "handler": "src/telemetry/temperature.js", "description": "Device temperature." },
14
+ { "name": "power_mode", "type": "string", "handler": "src/telemetry/power-mode.js", "description": "Active power mode: eco, normal or high." }
15
+ ],
16
+ "commands": [
17
+ { "name": "reboot", "handler": "src/commands/reboot.js", "description": "Restarts the device controller.", "constraints": "Do not call more than once per 5 minutes.", "params": [] },
18
+ { "name": "setPowerMode", "handler": "src/commands/set-power-mode.js", "description": "Changes the power mode: eco, normal or high (low is accepted as an alias for eco).", "params": [{ "name": "mode", "type": "string" }] }
19
+ ],
20
+ "health": { "supportedStates": ["OK", "OFFLINE"] },
21
+ "errors": {
22
+ "ERR_BAD_POWER_MODE": "The device rejected the requested power mode.",
23
+ "ERR_DEVICE_CALL_FAILED": "The device's HTTP API call failed or returned an error."
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,93 @@
1
+ 'use strict'
2
+
3
+ const http = require('http')
4
+ const debug = require('debug')('mdk:worker:__WORKER_NAME__:mock')
5
+
6
+ // A standalone device simulator: a small HTTP JSON API (GET /api/v3/summary,
7
+ // POST /api/v3/command) matching src/client.js. It knows nothing about MDK —
8
+ // it is the device-side surface the Worker Plugin translates against, the way
9
+ // a real device on the LAN would be. Replace with your own device's protocol.
10
+
11
+ const POWER_MODES = {
12
+ eco: { power: 0.8, hashrate: 0.85 },
13
+ normal: { power: 1, hashrate: 1 },
14
+ high: { power: 1.15, hashrate: 1.1 }
15
+ }
16
+
17
+ function jitter (value) {
18
+ return Math.round(value * (0.99 + Math.random() * 0.02) * 100) / 100
19
+ }
20
+
21
+ function createServer ({ host, port, serial, hashrateThs, powerW }) {
22
+ const state = {
23
+ serial: serial || 'DEV-0000',
24
+ baseHashrateThs: hashrateThs || 180,
25
+ basePowerW: powerW || 3400,
26
+ powerMode: 'normal',
27
+ bootTime: Date.now()
28
+ }
29
+
30
+ const summary = () => {
31
+ const scale = POWER_MODES[state.powerMode]
32
+ return {
33
+ serial: state.serial,
34
+ model: 'DEMO_V1',
35
+ firmware: 'v1.0.0',
36
+ uptime_s: Math.floor((Date.now() - state.bootTime) / 1000),
37
+ power_mode: state.powerMode,
38
+ hashrate_ths: jitter(state.baseHashrateThs * scale.hashrate),
39
+ power_w: jitter(state.basePowerW * scale.power),
40
+ board_temp_c: jitter(62 + (scale.power - 1) * 20)
41
+ }
42
+ }
43
+
44
+ const command = (body) => {
45
+ switch (body.cmd) {
46
+ case 'reboot':
47
+ state.bootTime = Date.now()
48
+ return { ok: true, rebooting: true }
49
+ case 'set-power-mode':
50
+ if (!POWER_MODES[body.mode]) throw new Error(`ERR_BAD_POWER_MODE: ${body.mode}`)
51
+ state.powerMode = body.mode
52
+ return { ok: true, power_mode: state.powerMode }
53
+ default:
54
+ throw new Error(`ERR_UNKNOWN_CMD: ${body.cmd}`)
55
+ }
56
+ }
57
+
58
+ const server = http.createServer((req, res) => {
59
+ const reply = (code, body) => {
60
+ res.writeHead(code, { 'content-type': 'application/json' })
61
+ res.end(JSON.stringify(body))
62
+ }
63
+
64
+ if (req.method === 'GET' && req.url === '/api/v3/summary') return reply(200, summary())
65
+
66
+ if (req.method === 'POST' && req.url === '/api/v3/command') {
67
+ let buf = ''
68
+ req.on('data', (chunk) => { buf += chunk })
69
+ req.on('end', () => {
70
+ try {
71
+ reply(200, command(JSON.parse(buf || '{}')))
72
+ } catch (err) {
73
+ reply(400, { ok: false, error: err.message })
74
+ }
75
+ })
76
+ return
77
+ }
78
+
79
+ reply(404, { ok: false, error: 'ERR_NOT_FOUND' })
80
+ })
81
+
82
+ server.listen(port, host || '127.0.0.1', () => {
83
+ debug('device mock %s listening on %s:%d', state.serial, host, port)
84
+ })
85
+
86
+ return {
87
+ server,
88
+ state,
89
+ exit () { server.close() }
90
+ }
91
+ }
92
+
93
+ module.exports = { createServer }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "__WORKER_NAME__",
3
+ "version": "0.12.1",
4
+ "description": "MDK Worker Plugin — an mdk-contract.json plus src/ handler files, loaded from this directory and instantiated per device by WorkerRuntimeV2 (see `mdk run worker`). It exports no module of its own.",
5
+ "keywords": [
6
+ "mdk",
7
+ "mdk-worker"
8
+ ],
9
+ "license": "Apache-2.0",
10
+ "engines": {
11
+ "node": ">=24"
12
+ },
13
+ "scripts": {
14
+ "mock": "node -e \"require('./mock/server').createServer({ host: '127.0.0.1', port: 18080, serial: 'DEV-0' })\""
15
+ },
16
+ "dependencies": {
17
+ "@tetherto/mdk-worker": "^0.12.1",
18
+ "debug": "4.4.1"
19
+ },
20
+ "devDependencies": {
21
+ "brittle": "3.18.0"
22
+ }
23
+ }
@@ -0,0 +1,41 @@
1
+ 'use strict'
2
+
3
+ // Smoke-harness glue for scripts/worker-smoke.mjs and `mdk validate worker
4
+ // --smoke`: setup() boots this worker's device mock and returns { config,
5
+ // commands, teardown }. Adapt all three when copying the template. `config`
6
+ // becomes device opts for WorkerRuntimeV2 (host/port for src/client.js).
7
+ //
8
+ // After teardown() every telemetry channel must fail with an ERR_*. A channel
9
+ // the worker serves from its own store (a `history` log, say) keeps answering
10
+ // instead: list it as `offlineTelemetry: ['history']` next to `teardown`.
11
+
12
+ const net = require('net')
13
+
14
+ const mock = require('./mock/server')
15
+
16
+ function freePort () {
17
+ return new Promise((resolve, reject) => {
18
+ const s = net.createServer()
19
+ s.once('error', reject)
20
+ s.listen(0, '127.0.0.1', () => {
21
+ const { port } = s.address()
22
+ s.close(() => resolve(port))
23
+ })
24
+ })
25
+ }
26
+
27
+ module.exports = {
28
+ setup: async () => {
29
+ const port = await freePort()
30
+ const handle = mock.createServer({ host: '127.0.0.1', port, serial: 'DEV-SMOKE' })
31
+
32
+ return {
33
+ config: { host: '127.0.0.1', port },
34
+ commands: {
35
+ reboot: {},
36
+ setPowerMode: { mode: 'eco' }
37
+ },
38
+ teardown: async () => { handle.exit() }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,47 @@
1
+ 'use strict'
2
+
3
+ const { opts, env, config, logger } = require('@tetherto/mdk-worker/device')
4
+
5
+ // The "vendor SDK" for your device's own HTTP JSON API. Plain device I/O — no
6
+ // MDK concepts, no base classes. This is all a plugin author writes to talk
7
+ // to their own device; replace it with your real protocol.
8
+ //
9
+ // The runtime loads this module fresh into a private registry per device, so
10
+ // binding directly to the ambient device module here — rather than exporting
11
+ // a `createClient(opts)` factory for a caller to invoke — already yields one
12
+ // client per device with no extra plumbing.
13
+ // Log only non-secret fields and key names: `env` carries credentials such as
14
+ // DEVICE_TOKEN, and `opts`/`config` may too — never log their values.
15
+ logger('config received: host=%s port=%s env keys=%o config keys=%o',
16
+ opts.host, opts.port, Object.keys(env || {}), Object.keys(config || {}))
17
+
18
+ const base = `http://${opts.host || '127.0.0.1'}:${opts.port}`
19
+ const auth = env.DEVICE_TOKEN ? { authorization: `Bearer ${env.DEVICE_TOKEN}` } : {}
20
+
21
+ const call = async (path, callOpts) => {
22
+ let res, body
23
+ try {
24
+ res = await fetch(base + path, { ...callOpts, headers: { ...auth, ...(callOpts && callOpts.headers) } })
25
+ body = await res.json()
26
+ } catch (err) {
27
+ // An unreachable device or a non-JSON reply: surface it as this plugin's
28
+ // own error code, not fetch's bare "fetch failed".
29
+ throw new Error(`ERR_DEVICE_CALL_FAILED: ${(err.cause && err.cause.code) || err.message}`)
30
+ }
31
+ if (!res.ok || body.ok === false) {
32
+ throw new Error(body.error || `ERR_DEVICE_CALL_FAILED: ${res.status}`)
33
+ }
34
+ return body
35
+ }
36
+
37
+ const command = (cmd, args) => call('/api/v3/command', {
38
+ method: 'POST',
39
+ headers: { 'content-type': 'application/json' },
40
+ body: JSON.stringify({ cmd, ...args })
41
+ })
42
+
43
+ module.exports = {
44
+ getSummary: () => call('/api/v3/summary'),
45
+ reboot: () => command('reboot'),
46
+ setPowerMode: (mode) => command('set-power-mode', { mode })
47
+ }
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const client = require('../client')
4
+
5
+ module.exports = async () => client.reboot()
@@ -0,0 +1,10 @@
1
+ 'use strict'
2
+
3
+ const client = require('../client')
4
+
5
+ module.exports = async (params) => {
6
+ // Example vendor-agnostic-to-firmware translation: this device's API calls
7
+ // low "eco". Adjust to your own device's vocabulary.
8
+ const mode = params.mode === 'low' ? 'eco' : params.mode
9
+ return client.setPowerMode(mode)
10
+ }
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const client = require('../client')
4
+
5
+ module.exports = async () => (await client.getSummary()).hashrate_ths
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const client = require('../client')
4
+
5
+ module.exports = async () => (await client.getSummary()).power_mode
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const client = require('../client')
4
+
5
+ module.exports = async () => (await client.getSummary()).power_w
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const client = require('../client')
4
+
5
+ module.exports = async () => (await client.getSummary()).board_temp_c