@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,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`.