@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,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
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Package ↔ folder index
|
|
2
|
+
|
|
3
|
+
Load this before referencing any `@tetherto/mdk-*` package or repo path.
|
|
4
|
+
Use only names that appear here — inventing package names is the #1 drift
|
|
5
|
+
failure. Paths are relative to the monorepo root.
|
|
6
|
+
|
|
7
|
+
## Core ([`backend/core/`](../../../../../../backend/core/README.md))
|
|
8
|
+
|
|
9
|
+
| Package | Folder | What it is |
|
|
10
|
+
| ----------------------- | ------------------------------------------------------------------ | --------------------------------------------------------- |
|
|
11
|
+
| `@tetherto/mdk-core` | [`backend/core/mdk`](../../../../../../backend/core/mdk/README.md) | Umbrella boot glue (`getKernel`, `startGateway`, `waitForDiscovery`, default topic file helpers, `libStats` aggregation ops); no generic `startWorker`, each Worker package ships its own boot function |
|
|
12
|
+
| `@tetherto/mdk-kernel` | [`backend/core/kernel`](../../../../../../backend/core/kernel/README.md) | The Kernel: protocol layer + 8 orchestration modules |
|
|
13
|
+
| `@tetherto/mdk-worker` | [`backend/core/mdk-worker`](../../../../../../backend/core/mdk-worker/index.js) | Worker Runtime: `WorkerRuntime`, `loadPlugin`, contract schema |
|
|
14
|
+
| `@tetherto/mdk-gateway` | [`backend/core/gateway`](../../../../../../backend/core/gateway/README.md) | HTTP Gateway in front of the Kernel |
|
|
15
|
+
| `@tetherto/mdk-client` | [`backend/core/client`](../../../../../../backend/core/client/README.md) | Consumer client library |
|
|
16
|
+
| `@tetherto/mdk-mcp` | [`backend/core/mcp`](../../../../../../backend/core/mcp/README.md) | MCP server: exposes MDK data/actions to AI agents as tools |
|
|
17
|
+
|
|
18
|
+
Also under [`backend/core/`](../../../../../../backend/core/README.md): `plugins/` (Gateway plugin surface, no published package yet).
|
|
19
|
+
|
|
20
|
+
## Workers ([`backend/workers/`](../../../../../../backend/workers/README.md))
|
|
21
|
+
|
|
22
|
+
| Package | Folder | Device protocol |
|
|
23
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------ |
|
|
24
|
+
| `@tetherto/mdk-worker-avalon` | [`backend/workers/miners/avalon`](../../../../../../backend/workers/miners/avalon/README.md) | CGMiner ASCII over TCP |
|
|
25
|
+
| `@tetherto/mdk-worker-abb` | [`backend/workers/power-meter/abb`](../../../../../../backend/workers/power-meter/abb/README.md) | Modbus TCP |
|
|
26
|
+
| `@tetherto/mdk-worker-satec` | [`backend/workers/power-meter/satec`](../../../../../../backend/workers/power-meter/satec/README.md) | Modbus TCP |
|
|
27
|
+
| `@tetherto/mdk-worker-schneider` | [`backend/workers/power-meter/schneider`](../../../../../../backend/workers/power-meter/schneider/README.md) | Modbus TCP |
|
|
28
|
+
| `@tetherto/mdk-worker-seneca` | [`backend/workers/temperature/seneca`](../../../../../../backend/workers/temperature/seneca/README.md) | Modbus TCP |
|
|
29
|
+
| `@tetherto/mdk-worker-antspace` | [`backend/workers/containers/antspace`](../../../../../../backend/workers/containers/antspace/README.md) | HTTP JSON |
|
|
30
|
+
| `@tetherto/mdk-worker-bitdeer` | [`backend/workers/containers/bitdeer`](../../../../../../backend/workers/containers/bitdeer/README.md) | MQTT |
|
|
31
|
+
| `@tetherto/mdk-worker-f2pool` | [`backend/workers/minerpools/f2pool`](../../../../../../backend/workers/minerpools/f2pool/README.md) | Pool HTTP API |
|
|
32
|
+
| `@tetherto/mdk-worker-ocean` | [`backend/workers/minerpools/ocean`](../../../../../../backend/workers/minerpools/ocean/README.md) | Pool HTTP API |
|
|
33
|
+
| `@tetherto/mdk-worker-mock` | [`backend/workers/mock`](../../../../../../backend/workers/mock/README.md) | Shared device-mock framework (BaseMock, category mocks, transports) |
|
|
34
|
+
|
|
35
|
+
Whatsminer support is the external [`whatsminer-mdk-worker`](https://github.com/whatsminer/whatsminer-mdk-worker) contract plugin (CGMiner JSON over TCP, AES-encrypted), hosted directly on `WorkerRuntimeV2` via a thin per-deployment adapter (see `examples/mvp-site/backend/whatsminer-adapter.js`). Antminer support is likewise the external [`antminer-mdk-worker`](https://github.com/bitmain-official/antminer-mdk-worker) contract plugin (HTTP CGI, Digest auth), hosted the same way (see `examples/mvp-site/backend/antminer-adapter.js`).
|
|
36
|
+
|
|
37
|
+
Generated Worker docs: [`backend/workers/docs/supported-hardware.md`](../../../../../../backend/workers/docs/supported-hardware.md) and
|
|
38
|
+
[`backend/workers/docs/catalogue.json`](../../../../../../backend/workers/docs/catalogue.json) (from
|
|
39
|
+
[`backend/workers/scripts/generate-catalogue.js`](../../../../../../backend/workers/scripts/generate-catalogue.js)).
|
|
40
|
+
|
|
41
|
+
## UI ([`ui/packages/`](../../../../../../ui/packages/))
|
|
42
|
+
|
|
43
|
+
| Package | Folder | What it is |
|
|
44
|
+
| ----------------------------- | ------------------------------------------------------------------------------------ | ----------------------------- |
|
|
45
|
+
| `@tetherto/mdk-react-devkit` | [`ui/packages/react-devkit`](../../../../../../ui/packages/react-devkit/README.md) | React component toolkit; components under [`src/primitives/components/`](../../../../../../ui/packages/react-devkit/src/primitives/components/) and [`src/domain/components/`](../../../../../../ui/packages/react-devkit/src/domain/components/index.ts) |
|
|
46
|
+
| `@tetherto/mdk-react-adapter` | [`ui/packages/react-adapter`](../../../../../../ui/packages/react-adapter/README.md) | Hooks binding components to Worker/plugin data |
|
|
47
|
+
| `@tetherto/mdk-ui-foundation` | [`ui/packages/ui-foundation`](../../../../../../ui/packages/ui-foundation/README.md) | Styling base |
|
|
48
|
+
|
|
49
|
+
## Examples
|
|
50
|
+
|
|
51
|
+
| Folder | What it runs |
|
|
52
|
+
| ----------------------------------------------------------------------- | ----------------------- |
|
|
53
|
+
| [`examples/mvp-site/`](../../../../../../examples/mvp-site/README.md) | Whole stack under PM2: mocks + Kernel + multiple Workers + Gateway + MCP + UI ([`start.js`](../../../../../../examples/mvp-site/start.js)) |
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# MDK protocol
|
|
2
|
+
|
|
3
|
+
Load this before writing anything that builds, sends, or answers an envelope.
|
|
4
|
+
Source of truth: [`backend/core/kernel/lib/protocol/`](../../../../../../backend/core/kernel/lib/protocol/) ([`actions.js`](../../../../../../backend/core/kernel/lib/protocol/actions.js),
|
|
5
|
+
[`envelope.js`](../../../../../../backend/core/kernel/lib/protocol/envelope.js), [`schemas.js`](../../../../../../backend/core/kernel/lib/protocol/schemas.js)). Protocol version: `0.2.0`.
|
|
6
|
+
|
|
7
|
+
## The envelope
|
|
8
|
+
|
|
9
|
+
Every message between Kernel and Workers is exactly this object — nine fields,
|
|
10
|
+
no more:
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
{
|
|
14
|
+
id: 'uuid', // per-message id (crypto.randomUUID())
|
|
15
|
+
version: '0.2.0', // PROTOCOL_VERSION
|
|
16
|
+
type: 'request', // 'request' | 'response' | 'event'
|
|
17
|
+
action: 'telemetry.pull',// one of ACTIONS below
|
|
18
|
+
sender: 'kernel:shard-1',// sender identity string
|
|
19
|
+
target: null, // receiver identity, or null
|
|
20
|
+
deviceId: 'miner-42', // addressed device, or null for Worker-scoped
|
|
21
|
+
timestamp: 1750000000000,// Date.now()
|
|
22
|
+
payload: {} // ALL action-specific data goes here
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Never add transport-level fields — extend `payload`.** Envelopes are
|
|
27
|
+
validated structurally (`ERR_ENVELOPE_*`) and, per action, by payload
|
|
28
|
+
validators in [`schemas.js`](../../../../../../backend/core/kernel/lib/protocol/schemas.js) (`ERR_PAYLOAD_*`); unknown actions are rejected
|
|
29
|
+
with `ERR_ENVELOPE_ACTION_UNKNOWN`. On the wire an envelope is
|
|
30
|
+
JSON-serialized to a Buffer and sent over the `'mdk'` HRPC method.
|
|
31
|
+
|
|
32
|
+
Build responses with `buildResponse(requestEnvelope, action, payload, sender)`
|
|
33
|
+
— it flips `target` to the request's `sender` and carries `deviceId` over.
|
|
34
|
+
|
|
35
|
+
## Action set ([`actions.js`](../../../../../../backend/core/kernel/lib/protocol/actions.js))
|
|
36
|
+
|
|
37
|
+
| Direction | Request → Response | Purpose |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| Kernel → Worker | `identity.request` → `identity.response` | Worker id + device list |
|
|
40
|
+
| Kernel → Worker | `capability.request` → `capability.response` | Published `mdk-contract.json` (handler paths stripped) |
|
|
41
|
+
| Kernel → Worker (scheduled) | `telemetry.pull` → `telemetry.response` | Read metrics/channels |
|
|
42
|
+
| Kernel → Worker (scheduled) | `state.pull` → `state.response` | Device online/offline snapshot |
|
|
43
|
+
| Client → Kernel → Worker | `command.request` → `command.result` | Execute a device command |
|
|
44
|
+
| Client → Kernel | `command.status` → `command.status.response` | Query command lifecycle |
|
|
45
|
+
| Client → Kernel | `command.cancel` → `command.cancel.response` | Cancel a pending command |
|
|
46
|
+
| Kernel → Worker (scheduled) | `health.ping` → `health.pong` | Liveness |
|
|
47
|
+
| Gateway → Kernel only | `worker.list`, `device.capabilities`, `worker.terminate` | Kernel queries (never forwarded to Workers) |
|
|
48
|
+
| Gateway → Kernel | `action.push`, `action.push-batch`, `action.get`, `action.get-batch`, `action.query`, `action.vote`, `action.cancel-batch` | Write-action approval lifecycle |
|
|
49
|
+
| Kernel → Worker | `write.calls.request` → `write.calls.response` | Write-call resolution (only when worker-infra services are injected) |
|
|
50
|
+
|
|
51
|
+
Also defined there: `COMMAND_SCOPES` (`device` | `worker` | `rack`),
|
|
52
|
+
`MESSAGE_TYPES`, and `VALID_COMMAND_RESULT_STATUSES`
|
|
53
|
+
(the command state machine's states plus `REJECTED`).
|
|
54
|
+
|
|
55
|
+
## Reads: `telemetry.pull`
|
|
56
|
+
|
|
57
|
+
Payload shape: `{ query: { type, ...params } }`, `deviceId` in the envelope.
|
|
58
|
+
`type` selects what a Worker returns (dispatch in
|
|
59
|
+
[`backend/core/mdk-worker/lib/worker-runtime.js`](../../../../../../backend/core/mdk-worker/lib/worker-runtime.js) `_handleTelemetry`):
|
|
60
|
+
|
|
61
|
+
- `type: 'metrics'` (default) — runs **every** declared telemetry handler for
|
|
62
|
+
the addressed device: `{ deviceId, metrics: { <name>: value, … }, timestamp }`.
|
|
63
|
+
With no `deviceId`, returns `{ devices: [{ deviceId, metrics|error }…] }`.
|
|
64
|
+
- `type: 'list'` — `{ devices: [{ deviceId, status }] }` (no device needed).
|
|
65
|
+
- `type: '<channel>'` — any single declared telemetry name, with the rest of
|
|
66
|
+
`query` passed to the handler as params:
|
|
67
|
+
`{ query: { type: 'history', limit: 3 } }` → `{ name, value }`.
|
|
68
|
+
- Worker-infra built-in types (`logs`, `settings`, `stats`, `config`, …) are
|
|
69
|
+
served from injected services before plugin channels are consulted.
|
|
70
|
+
|
|
71
|
+
Per-field handler errors come back inside the payload
|
|
72
|
+
(`metrics.<name> = { error }`); an offline device yields
|
|
73
|
+
`ERR_DEVICE_UNAVAILABLE`, an unknown channel `ERR_UNKNOWN_QUERY_TYPE`.
|
|
74
|
+
|
|
75
|
+
## Writes: `command.request`
|
|
76
|
+
|
|
77
|
+
Payload: `{ commandId, command, params }`, `deviceId` in the envelope.
|
|
78
|
+
The **Kernel's dispatcher validates before dispatch**
|
|
79
|
+
([`backend/core/kernel/lib/modules/command-dispatcher/index.js`](../../../../../../backend/core/kernel/lib/modules/command-dispatcher/index.js)):
|
|
80
|
+
the command must appear in the Worker's declared capabilities
|
|
81
|
+
(`ERR_COMMAND_NOT_IN_CAPABILITIES`), params must match declared types
|
|
82
|
+
(`ERR_PARAM_TYPE`), and numeric params must be within declared `min`/`max`
|
|
83
|
+
(`ERR_PARAM_RANGE`). Bounds you do not declare are NOT enforced anywhere.
|
|
84
|
+
|
|
85
|
+
The Worker answers `command.result` with
|
|
86
|
+
`{ commandId, status: 'SUCCESS', result }` or
|
|
87
|
+
`{ commandId, status: 'FAILED', error }`. Handler exceptions become `FAILED`
|
|
88
|
+
with the error message — so throw `ERR_*`-prefixed errors from handlers.
|
|
89
|
+
|
|
90
|
+
## Error string convention
|
|
91
|
+
|
|
92
|
+
Errors travel as `SCREAMING_SNAKE_CASE` strings prefixed `ERR_`, optionally
|
|
93
|
+
suffixed with context after a colon (`ERR_DEVICE_NOT_FOUND: miner-42`). Device
|
|
94
|
+
error codes a Worker can emit belong in the contract's `capabilities.errors`
|
|
95
|
+
map.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mdk-create-app
|
|
3
|
+
description: >
|
|
4
|
+
Scaffold a new MDK App package ready to publish. Use whenever a task mentions
|
|
5
|
+
"create app", "scaffold app", "mdk create app", or generating an MDK App's
|
|
6
|
+
mdk-app.yaml, package.json, plugin/, and ui/ modules.
|
|
7
|
+
argument-hint: "<name> [--org <scope>] [--id <reverse-dns>] [--display <name>] [--devices <pattern>] [--verbs <read|read,write>]"
|
|
8
|
+
metadata:
|
|
9
|
+
suite: mdk-developer-skill
|
|
10
|
+
mdk_version: "0.12.1"
|
|
11
|
+
license: Apache-2.0
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Create an MDK App (`mdk-create-app`)
|
|
15
|
+
|
|
16
|
+
An MDK App is a self-contained, publishable npm package that bundles a backend plugin, a UI shell module, and an `mdk-app.yaml` manifest.
|
|
17
|
+
|
|
18
|
+
This skill governs the **creation and scaffolding phase** of an MDK App before it is added to a site or published.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. The Package Layout
|
|
23
|
+
|
|
24
|
+
Every scaffolded app follows the canonical structure:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
@tetherto/mdk-app-<name>@<version>
|
|
28
|
+
├── package.json # name, version, files, dependencies, peerDependencies
|
|
29
|
+
├── mdk-app.yaml # app manifest
|
|
30
|
+
├── plugin/ # backend plugin template
|
|
31
|
+
│ ├── mdk-plugin.json # route table (the Gateway serves it under /apps/<name> unless mounted)
|
|
32
|
+
│ ├── controllers/ # route handlers
|
|
33
|
+
│ └── lib/ # client helper used by the handlers
|
|
34
|
+
└── ui/ # built UI module with index entry point
|
|
35
|
+
└── index.js
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 2. The Strict `mdk-app.yaml` Specification
|
|
41
|
+
|
|
42
|
+
`mdk-app.yaml` defines the app's identity, permissions, and configuration. It contains these 5 keys, plus an optional
|
|
43
|
+
one-line `description`:
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
id: io.tether.sentinel # 1. Immutable identity, reverse-DNS
|
|
47
|
+
version: 0.1.0 # 2. Semver; must equal package.json version
|
|
48
|
+
displayName: Sentinel # 3. Shown in shell's sidebar and CLI
|
|
49
|
+
permissions: # 4. What the app asks the operator to approve (not enforced by the Kernel yet)
|
|
50
|
+
- resources: [devices]
|
|
51
|
+
verbs: [read] # Default: read (least privilege; [read, write] for controls/tuning)
|
|
52
|
+
resourceNames: ["*"] # Default: allow all devices ("*"), or scoped: family/brand/worker/device
|
|
53
|
+
config: # 5. Operational defaults (overridable in mdk.yaml)
|
|
54
|
+
driftThresholdPct: 10
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Critical Negative Constraints: What NOT to Add
|
|
58
|
+
* **DO NOT add extraneous top-level keys**: Never add `icon`, `routes`, `auth`, `mcp`, `devices`, `metadata`, or `entry`. The specification limits the manifest to these 5 keys plus `description`; `mdk validate app` reports any other key.
|
|
59
|
+
* **`config` must always be an object**: If the app has no default configuration parameters, write `config: {}`. Never leave it null, undefined, or an array.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 3. Edge Cases & Validation Rules
|
|
64
|
+
|
|
65
|
+
### Edge Case 1: Directory & Location Context
|
|
66
|
+
* **Standalone Repo Mode (Outside an MDK project)**: If the current directory is an empty folder or standalone repo (no `mdk.yaml` in current or parent directories), scaffold files **directly at the root of the current directory**:
|
|
67
|
+
`./package.json`, `./mdk-app.yaml`, `./plugin/`, `./ui/`.
|
|
68
|
+
* **Site Project Mode (Inside an MDK project)**: If an `mdk.yaml` exists in the current or parent directory, scaffold under `apps/<name>/`.
|
|
69
|
+
* **Distinction from Dashboard Shells**: An MDK App package (`@tetherto/mdk-app-<name>`) is a backend plugin + dynamic UI shell module. It is distinct from a standalone Vite dashboard shell created via `mdk create dashboard <name>` (which defaults to `apps/dashboard`). If developing an app inside a site repo alongside an existing dashboard, ensure distinct directory names (e.g. `apps/<name>-app` or keep the dashboard at `apps/dashboard`).
|
|
70
|
+
|
|
71
|
+
### Edge Case 2: RBAC Permissions & Device Hierarchy
|
|
72
|
+
* **Not enforced yet**: The Kernel does not enforce `permissions` today. They are declared for operator review, and the app
|
|
73
|
+
runtime's Kernel key is admitted with full access. Declare least privilege anyway, so review is meaningful.
|
|
74
|
+
* **Target Resources**: Declare apps **only on `resources: [devices]`**. Never set `resources: [workers]`, `[plugins]`, `[*]`, or `[kernel]`.
|
|
75
|
+
* **Verbs**:
|
|
76
|
+
* Default: `verbs: [read]` (least privilege; for monitoring, telemetry, and metrics dashboards).
|
|
77
|
+
* Control / Tuning apps (reboots, parameter tuning, commands): `verbs: [read, write]`.
|
|
78
|
+
* **Device Scoping (`resourceNames`)**: Defaults to `["*"]` to allow all devices. When scoped, it must follow the 4-part MDK device hierarchy:
|
|
79
|
+
`family / brand / worker / device`
|
|
80
|
+
* `*` — All devices across all families (Default: allow all).
|
|
81
|
+
* `miner/*` — All miners across all brands.
|
|
82
|
+
* `miner/antminer/*` — All Antminers across all workers.
|
|
83
|
+
* `miner/antminer/antminer-a/*` — Devices under worker `antminer-a`.
|
|
84
|
+
* `powermeter/*` — All power meters (note: backend family identifier is `powermeter`, not `power-meter`).
|
|
85
|
+
* `sensor/*`, `container/*` — Sensors and containers.
|
|
86
|
+
* **Multiple Device Families**: If an app needs access to multiple hardware types, declare separate scoped rule blocks:
|
|
87
|
+
```yaml
|
|
88
|
+
permissions:
|
|
89
|
+
- resources: [devices]
|
|
90
|
+
verbs: [read, write]
|
|
91
|
+
resourceNames: ["miner/*"]
|
|
92
|
+
- resources: [devices]
|
|
93
|
+
verbs: [read]
|
|
94
|
+
resourceNames: ["powermeter/*"]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Edge Case 3: Naming, Scoping & Version Parity
|
|
98
|
+
* **`<name>`**: Lowercase kebab-case slug (`^[a-z0-9][a-z0-9._-]*$`). No uppercase letters, spaces, or slashes.
|
|
99
|
+
* **`id`**: Immutable reverse-DNS format (`^[a-z][a-z0-9]*(\.[a-z0-9][a-z0-9-]*)+$`, e.g. `io.tether.<name>`).
|
|
100
|
+
* **`package.json` Name**: Defaults to `@tetherto/mdk-app-<name>` (or `@<scope>/mdk-app-<name>` if custom org specified).
|
|
101
|
+
* **Version Parity**: `package.json#version` and `mdk-app.yaml#version` **must match byte-for-byte** (defaults to `0.1.0`).
|
|
102
|
+
|
|
103
|
+
### Edge Case 4: Route Namespacing in `plugin/mdk-plugin.json`
|
|
104
|
+
* **The Gateway adds the prefix**: The Gateway serves every route of an app under `/apps/<name>`, where `<name>` is the app's
|
|
105
|
+
`spec.apps[].name` in `mdk.yaml`. Declare paths **without** that prefix:
|
|
106
|
+
* Example: `/status` in `mdk-plugin.json` is served at `/apps/sentinel/status`.
|
|
107
|
+
* Never write `/apps/<name>/...` in the route table; it would be served at `/apps/<name>/apps/<name>/...`.
|
|
108
|
+
* **Root mount**: An app whose client calls fixed root paths (the agent app's `/agent/sessions`, an identity app's `/oauth/...`)
|
|
109
|
+
sets `mount: /` on its `spec.apps` entry instead, and its paths are served as written. Prefixed is the default because it
|
|
110
|
+
keeps installed apps from colliding.
|
|
111
|
+
|
|
112
|
+
### Edge Case 5: Dynamic UI Shell Module Contract
|
|
113
|
+
* The UI entry point (`ui/index.js`) is read dynamically by the UI shell.
|
|
114
|
+
* It must export a default object with:
|
|
115
|
+
```javascript
|
|
116
|
+
export default {
|
|
117
|
+
id: 'io.tether.<name>',
|
|
118
|
+
displayName: '<DisplayName>',
|
|
119
|
+
routes: [],
|
|
120
|
+
panels: []
|
|
121
|
+
};
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 4. Execution Workflow
|
|
127
|
+
|
|
128
|
+
### Step 1: Collect App Definition (The Interview)
|
|
129
|
+
Collect or confirm the definition parameters with sensible fallbacks:
|
|
130
|
+
|
|
131
|
+
| Parameter | Prompt / Meaning | Default / Fallback |
|
|
132
|
+
| :--- | :--- | :--- |
|
|
133
|
+
| **`name`** | App slug (kebab-case) | Required (e.g. `sentinel`) |
|
|
134
|
+
| **`id`** | Reverse-DNS identity | `io.tether.<name>` |
|
|
135
|
+
| **`displayName`** | Human name for shell sidebar and CLI | TitleCase `<name>` |
|
|
136
|
+
| **`devices`** | Target hardware hierarchy (`resourceNames`) | `*` (allow all devices) |
|
|
137
|
+
| **`verbs`** | Access level needed | `read` (least privilege; `read, write` for controls) |
|
|
138
|
+
| **`config`** | Initial default settings | `{}` |
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### Step 2: Scaffold Files
|
|
143
|
+
|
|
144
|
+
Inside an MDK project, `mdk create app <name>` (`--org <scope>` for a scoped package name) is the canonical method: it
|
|
145
|
+
scaffolds `apps/<name>` with `mdk-app.yaml`, `plugin/mdk-plugin.json`, a client and one read-only route, links the package,
|
|
146
|
+
and adds it under `spec.apps` in `mdk.yaml`. Then adjust `id`, `permissions` and the routes as below, and check the result
|
|
147
|
+
with `mdk validate app <name>`.
|
|
148
|
+
|
|
149
|
+
Without the CLI (standalone repo mode, or a layout the template does not cover), create the files directly according to
|
|
150
|
+
the location context:
|
|
151
|
+
|
|
152
|
+
1. **`package.json`**:
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"name": "@tetherto/mdk-app-<name>",
|
|
156
|
+
"version": "0.1.0",
|
|
157
|
+
"files": [
|
|
158
|
+
"mdk-app.yaml",
|
|
159
|
+
"plugin/",
|
|
160
|
+
"ui/"
|
|
161
|
+
],
|
|
162
|
+
"dependencies": {},
|
|
163
|
+
"peerDependencies": {}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
2. **`mdk-app.yaml`**:
|
|
168
|
+
```yaml
|
|
169
|
+
id: io.tether.<name>
|
|
170
|
+
version: 0.1.0
|
|
171
|
+
displayName: <DisplayName>
|
|
172
|
+
permissions:
|
|
173
|
+
- resources: [devices]
|
|
174
|
+
verbs: [<verbs, default: read>]
|
|
175
|
+
resourceNames: ["<device-pattern, default: *>"]
|
|
176
|
+
config: {}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
3. **`plugin/mdk-plugin.json`**:
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"id": "io.tether.<name>",
|
|
183
|
+
"name": "<name>",
|
|
184
|
+
"routes": [
|
|
185
|
+
{
|
|
186
|
+
"id": "<name>.status",
|
|
187
|
+
"handler": "controllers/status.js",
|
|
188
|
+
"http": {
|
|
189
|
+
"method": "GET",
|
|
190
|
+
"path": "/status"
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
4. **`plugin/controllers/status.js`**:
|
|
198
|
+
```javascript
|
|
199
|
+
'use strict'
|
|
200
|
+
|
|
201
|
+
module.exports = async function status (req) {
|
|
202
|
+
return { status: 'OK', app: '<name>' }
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
5. **`ui/index.js`**:
|
|
207
|
+
```javascript
|
|
208
|
+
export default {
|
|
209
|
+
id: 'io.tether.<name>',
|
|
210
|
+
displayName: '<DisplayName>',
|
|
211
|
+
routes: [],
|
|
212
|
+
panels: []
|
|
213
|
+
};
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
*(`mdk create app` takes only `<name>`, `--org`, `--dir`, `--force`, `--no-install` and `--no-stack-entry` today; set `id`,
|
|
217
|
+
`displayName` and `permissions` in the generated `mdk-app.yaml` by hand.)*
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
### Step 3: Self-Audit Checklist
|
|
222
|
+
|
|
223
|
+
Before completing the task, the agent must verify:
|
|
224
|
+
* [ ] **Manifest Keys**: `mdk-app.yaml` contains ONLY `id`, `version`, `displayName`, `permissions`, `config`, and optionally `description`.
|
|
225
|
+
* [ ] **Version Parity**: `package.json` version equals `mdk-app.yaml` version.
|
|
226
|
+
* [ ] **RBAC Resources**: `permissions[].resources` contains strictly `[devices]`.
|
|
227
|
+
* [ ] **RBAC Verbs**: `permissions[].verbs` defaults to `[read]` (or `[read, write]`), containing only valid actions.
|
|
228
|
+
* [ ] **Device Hierarchy / Scope**: `permissions[].resourceNames` defaults to `["*"]` (allow all) or follows `family/brand/worker/device` (using `powermeter` for power meters).
|
|
229
|
+
* [ ] **Route Namespacing**: No route in `plugin/mdk-plugin.json` starts with `/apps/<name>/`; the Gateway adds that prefix.
|
|
230
|
+
* [ ] **Validation**: `mdk validate app <name>` passes (the app's name in `mdk.yaml`).
|
|
231
|
+
* [ ] **UI Contract**: `ui/index.js` exports an object with `id`, `displayName`, `routes`, and `panels`.
|