@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,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mdk-deployment
|
|
3
|
+
description: >
|
|
4
|
+
Deploy and run a working MDK stack from mdk.yaml. Use when the task mentions
|
|
5
|
+
"deploy", "run the stack", "start the Kernel + Workers + Gateway",
|
|
6
|
+
"register a plugin", "register an app", "mdk.yaml", or MDK environment configuration.
|
|
7
|
+
metadata:
|
|
8
|
+
suite: mdk-developer-skill
|
|
9
|
+
mdk_version: "0.5.0"
|
|
10
|
+
license: Apache-2.0
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Deploy and run an MDK stack
|
|
14
|
+
|
|
15
|
+
In CLI-managed projects (this folder), the stack is declared in `mdk.yaml` and
|
|
16
|
+
started with `mdk run`. Boot order matters: **Kernel → Workers → apps → Gateway/UI**.
|
|
17
|
+
|
|
18
|
+
## Project shape
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
<project>/
|
|
22
|
+
├── mdk.yaml # Stack spec (source of truth)
|
|
23
|
+
├── package.json # private root manifest; links workers/<name> and apps/<name> by file: deps
|
|
24
|
+
├── workers/<name>/ # Worker Plugin packages (mdk create worker)
|
|
25
|
+
├── apps/<name>/ # apps: mdk-app.yaml + plugin/ route table (mdk create app)
|
|
26
|
+
├── apps/dashboard/ # UI shell (mdk create dashboard)
|
|
27
|
+
└── .mdk/ # runtime state written by `mdk run` — gitignored, disposable
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Workers and apps are linked from the root manifest by relative `file:`
|
|
31
|
+
dependencies: one root `npm install` links every component into the project's
|
|
32
|
+
`node_modules`, which is how `mdk run` resolves them. The dashboard is
|
|
33
|
+
deliberately **not** linked — it links the MDK UI packages by `file:` path and
|
|
34
|
+
installs on its own, so its React is never hoisted alongside those links.
|
|
35
|
+
|
|
36
|
+
## `mdk.yaml` essentials
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
apiVersion: mdk/v1
|
|
40
|
+
kind: Stack
|
|
41
|
+
metadata:
|
|
42
|
+
name: <stack-name>
|
|
43
|
+
spec:
|
|
44
|
+
kernel:
|
|
45
|
+
port: 3848
|
|
46
|
+
gateway:
|
|
47
|
+
port: 3847
|
|
48
|
+
apps:
|
|
49
|
+
- name: <app-name> # ^[a-z][a-z0-9-]*$; routes served under /apps/<app-name>
|
|
50
|
+
package: <app-package-name> # e.g. fleet-summary, or @org/name
|
|
51
|
+
config: {} # overrides the mdk-app.yaml config defaults
|
|
52
|
+
# mount: / # serve at the Gateway root (required for @tetherto/mdk-app-agent)
|
|
53
|
+
mcp:
|
|
54
|
+
port: 3947 # optional — defaults to gateway.port + 100
|
|
55
|
+
workers:
|
|
56
|
+
- name: <worker-name>
|
|
57
|
+
package: ./workers/<worker-name> # local path, or an installed npm name
|
|
58
|
+
config:
|
|
59
|
+
mock: true
|
|
60
|
+
devices:
|
|
61
|
+
- id: dev-0
|
|
62
|
+
opts: { host: 127.0.0.1, port: 18080, serial: DEV-0 }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Both `mdk create worker <name>` and `mdk create app <name>` scaffold the
|
|
66
|
+
package **and** register it here automatically (Workers under
|
|
67
|
+
`spec.workers`, apps under `spec.apps`) — no manual
|
|
68
|
+
`package.json` linking step. `create worker` also seeds a stack-unique mock port/device id
|
|
69
|
+
per Worker, so scaffolding several in one project never collides. A published
|
|
70
|
+
package goes through `mdk add app <npm-or-github-source>` / `mdk add worker <source>`,
|
|
71
|
+
which installs it, validates it, and only then adds the entry. If you add a
|
|
72
|
+
package by hand instead, mirror that: `npm install` at the project root, append the
|
|
73
|
+
entry yourself, then check it with `mdk validate app <name>` / `mdk validate worker <name>`.
|
|
74
|
+
|
|
75
|
+
`spec.gateway.plugins` is rejected by the CLI; move each entry to `spec.apps` with a `name`.
|
|
76
|
+
|
|
77
|
+
## Run commands
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
mdk run # whole stack from mdk.yaml: kernel + workers + one process per app + gateway
|
|
81
|
+
mdk run kernel
|
|
82
|
+
mdk run gateway
|
|
83
|
+
mdk run worker <name> # e.g. mdk run worker demo-miner
|
|
84
|
+
mdk run app <name> # one app runtime, in the foreground
|
|
85
|
+
mdk run dashboard # the scaffolded UI shell's dev server
|
|
86
|
+
mdk run mcp # standalone MCP server for AI agents (needs Kernel running first)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`mdk run` with no target boots everything together; that's not a spec setting,
|
|
90
|
+
just which command you type. Use the per-component targets and separate
|
|
91
|
+
terminals when debugging one tier. Gateway port is `spec.gateway.port`
|
|
92
|
+
(example: `3847`); curl an app at `http://127.0.0.1:<gateway-port>/apps/<app-name>/api/...`
|
|
93
|
+
(or at `/api/...` when its entry sets `mount: /`). Each app runs in its own runtime process
|
|
94
|
+
and accepts calls only from the Gateway's key; the Gateway forwards each request to it.
|
|
95
|
+
|
|
96
|
+
Like the dashboard, `mcp` is never part of the default `all` target — always
|
|
97
|
+
start it explicitly, in its own terminal, after `mdk run kernel` (a standalone
|
|
98
|
+
`run mcp` has no in-process Kernel handle to reuse). It reads `spec.apps` and exposes each
|
|
99
|
+
declared app's routes as agent-callable tools, over `spec.mcp.port` if set,
|
|
100
|
+
else `spec.gateway.port + 100`. Routes not marked `"safety": "read-only"` are withheld
|
|
101
|
+
unless it is started with `--allow-write`.
|
|
102
|
+
|
|
103
|
+
Every caller publishes its Kernel key to `.mdk/trusted-keys/` (`gateway.pub`, `mcp.pub`,
|
|
104
|
+
`cli.pub`, `app-<name>.pub`, …) and the fail-closed Kernel admits it with full access: an app's
|
|
105
|
+
`permissions` in `mdk-app.yaml` are not enforced yet.
|
|
106
|
+
|
|
107
|
+
Ctrl+C / SIGTERM stops components in reverse boot order (Gateway → apps → Workers →
|
|
108
|
+
Kernel) and always exits, even if a component's own shutdown hangs.
|
|
109
|
+
|
|
110
|
+
### Verify
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
mdk status # environment + Kernel/Gateway/worker health, exit code encodes it
|
|
114
|
+
mdk status -o json | jq .health
|
|
115
|
+
curl -s http://127.0.0.1:3847/apps/<app-name>/api/<route> | jq .
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`mdk status` never starts, stops or repairs anything — it probes the Kernel
|
|
119
|
+
over HRPC and the Gateway over HTTP, reports declared-vs-registered Workers, and
|
|
120
|
+
counts an app as running while a live process holds its run lock.
|
|
121
|
+
Exit codes: `0` healthy, `4` precondition not met (bad Node, invalid
|
|
122
|
+
`mdk.yaml`, packages not installed), `5` stack not fully up.
|
|
123
|
+
|
|
124
|
+
UI: `mdk run dashboard` (or `cd apps/dashboard && npm run dev` directly) and
|
|
125
|
+
open the page route added by `mdk-ui-component`.
|
|
126
|
+
|
|
127
|
+
## Scaffolding helpers
|
|
128
|
+
|
|
129
|
+
| Need | Command |
|
|
130
|
+
| ------------------ | ------------------------------------------------------ |
|
|
131
|
+
| Guided `mdk.yaml` | `mdk onboard` |
|
|
132
|
+
| New Worker package | `mdk create worker <name>` → then `mdk-worker-plugin` |
|
|
133
|
+
| New app | `mdk create app <name>` (`mdk-create-app`) → then `mdk-gateway-plugin` for its routes |
|
|
134
|
+
| Published app | `mdk add app <npm-or-github-source>` |
|
|
135
|
+
| New dashboard app | `mdk create dashboard` |
|
|
136
|
+
|
|
137
|
+
The `create` commands run `npm install` by default (`--no-install` to skip) and, for
|
|
138
|
+
Workers/apps, install at the project root so the new package links into the
|
|
139
|
+
project rather than getting its own nested `node_modules`.
|
|
140
|
+
|
|
141
|
+
## Boot-order invariant
|
|
142
|
+
|
|
143
|
+
1. Kernel listening.
|
|
144
|
+
2. Workers announce on the DHT topic, or attach directly when run in the same process.
|
|
145
|
+
3. One runtime process per app (each builds its own Kernel client).
|
|
146
|
+
4. Gateway reads each app's route table and forwards requests to its runtime; an app not up yet answers `503`.
|
|
147
|
+
5. UI last — it only talks to the Gateway.
|
|
148
|
+
|
|
149
|
+
Starting the UI or an app before the Worker is online yields empty arrays,
|
|
150
|
+
not necessarily HTTP errors — treat an empty `devices: []` in an app
|
|
151
|
+
response as a deployment signal, not a bug.
|
|
152
|
+
|
|
153
|
+
## Hand-off
|
|
154
|
+
|
|
155
|
+
| Next need | Skill |
|
|
156
|
+
| ---------------------------------- | -------------------- |
|
|
157
|
+
| How many Workers / which host tier | `mdk-site-sizing` |
|
|
158
|
+
| New device Worker | `mdk-worker-plugin` |
|
|
159
|
+
| New app package | `mdk-create-app` |
|
|
160
|
+
| New aggregation API | `mdk-gateway-plugin` |
|
|
161
|
+
| New dashboard page | `mdk-ui-component` |
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mdk-gateway-plugin
|
|
3
|
+
description: >
|
|
4
|
+
Build the HTTP routes of an MDK app (route table + controllers, served
|
|
5
|
+
through the Gateway) over one or more MDK Workers. Use when the task
|
|
6
|
+
mentions "aggregate", "combine Workers",
|
|
7
|
+
"site summary / rollup", "plugin", "mdk-plugin.json", "Gateway endpoint",
|
|
8
|
+
"cross-worker endpoint", or when a UI needs live Worker data and no matching
|
|
9
|
+
`/api/...` route exists yet.
|
|
10
|
+
metadata:
|
|
11
|
+
suite: mdk-developer-skill
|
|
12
|
+
mdk_version: "0.5.0"
|
|
13
|
+
license: Apache-2.0
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Build the routes of an MDK app
|
|
17
|
+
|
|
18
|
+
The Gateway serves HTTP routes from **apps**. An app is a small Node package with an `mdk-app.yaml` manifest
|
|
19
|
+
and a route table (`mdk-plugin.json`, in `plugin/` or the package root); `mdk run` runs each app in its own
|
|
20
|
+
process and the Gateway forwards each request to it. Each route is a controller that queries Workers through
|
|
21
|
+
the app's own `@tetherto/mdk-client`, built once from `require('@tetherto/mdk-gateway/plugin')`.
|
|
22
|
+
|
|
23
|
+
Apps are the **server half** of any "show me X from my devices" request.
|
|
24
|
+
If the UI needs data that no existing route returns, build the app's route
|
|
25
|
+
first, then hand off to `mdk-ui-component`. Creating the package itself (manifest,
|
|
26
|
+
permissions, layout) is the [`mdk-create-app`](../mdk-create-app/SKILL.md) skill; this
|
|
27
|
+
skill owns the route table and controllers.
|
|
28
|
+
|
|
29
|
+
## When to use this skill
|
|
30
|
+
|
|
31
|
+
| Situation | Action |
|
|
32
|
+
| ----------------------------------------------------- | -------------------------- |
|
|
33
|
+
| UI / chart needs a metric and no Gateway route exists | Add a route here first |
|
|
34
|
+
| Aggregate / rollup / site summary" across devices | Add a route |
|
|
35
|
+
| Endpoint already exists (check `mdk.yaml` → `spec.apps`, or `mdk status`) | Skip — go straight to `mdk-ui-component` |
|
|
36
|
+
| Need a new device protocol | Wrong skill — use `mdk-worker-plugin` |
|
|
37
|
+
|
|
38
|
+
## Workflow
|
|
39
|
+
|
|
40
|
+
### 1. site capability discovery (mandatory)
|
|
41
|
+
|
|
42
|
+
Do **not** invent channel names, units, or device brands. Ground every field
|
|
43
|
+
in an installed Worker contract.
|
|
44
|
+
|
|
45
|
+
**Resolve each Worker's contract from `mdk.yaml` → `spec.workers[].package`:**
|
|
46
|
+
|
|
47
|
+
| `package` value | Contract path |
|
|
48
|
+
| --------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
49
|
+
| Local path (`./workers/<name>`, `../…`) | `<package>/mdk-contract.json` (or legacy `<package>/plugin/mdk-contract.json`) |
|
|
50
|
+
| npm name (`@tetherto/mdk-worker-avalon`, …) | `node_modules/<package>/mdk-contract.json` (or legacy `…/plugin/mdk-contract.json`) |
|
|
51
|
+
|
|
52
|
+
Also scan `workers/*/mdk-contract.json` and `workers/*/plugin/mdk-contract.json` for any local scaffolds not yet
|
|
53
|
+
listed in `mdk.yaml`. If an npm package is listed but missing under
|
|
54
|
+
`node_modules/`, run `npm install` first — do not invent the contract.
|
|
55
|
+
|
|
56
|
+
Then:
|
|
57
|
+
|
|
58
|
+
1. Open each resolved `mdk-contract.json`.
|
|
59
|
+
2. Read `capabilities.telemetry[]` / `commands[]` — note `name`, `type`, `unit`.
|
|
60
|
+
3. Read `metadata.brand` / `deviceFamily` / `provider` for how to recognize
|
|
61
|
+
the device family at runtime.
|
|
62
|
+
4. Confirm the Kernel actually has the Worker online (`mdk run worker <name>`
|
|
63
|
+
or `mdk status`) before assuming data flows.
|
|
64
|
+
|
|
65
|
+
`getCapabilities` returns the capability list only — not full contract
|
|
66
|
+
metadata (no `brand`/`provider`) — so the fingerprint you match against at
|
|
67
|
+
runtime is the telemetry/command **name set**, derived from the contract you
|
|
68
|
+
just read, not a hard-coded device list.
|
|
69
|
+
|
|
70
|
+
### 2. Scaffold the app
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
mdk create app <app-name>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Scaffolds and registers in one step (manifest rules: [`mdk-create-app`](../mdk-create-app/SKILL.md)):
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
apps/<app-name>/
|
|
80
|
+
├── package.json # name: <app-name> (or @org/<app-name> with --org)
|
|
81
|
+
├── mdk-app.yaml # id, version (= package.json), displayName, permissions, config defaults
|
|
82
|
+
└── plugin/
|
|
83
|
+
├── mdk-plugin.json # routes + OpenAPI-ish response schema
|
|
84
|
+
├── lib/client.js # the app's MDK client
|
|
85
|
+
└── controllers/
|
|
86
|
+
└── summary.js # async (req) => payload
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
It also links the package into the project's root `package.json` (root `npm install`,
|
|
90
|
+
skip with `--no-install`) and appends it under `mdk.yaml` →
|
|
91
|
+
`spec.apps` (skip with `--no-stack-entry`) — no manual
|
|
92
|
+
`package.json` linking step. Check it with `mdk validate app <app-name>`.
|
|
93
|
+
|
|
94
|
+
### 3. Author `mdk-plugin.json`
|
|
95
|
+
|
|
96
|
+
Every route needs:
|
|
97
|
+
|
|
98
|
+
- `id` — stable dotted id (`<domain>.<metric>`, e.g. `fleet.summary`)
|
|
99
|
+
- `handler` — relative path to the controller
|
|
100
|
+
- `http.method` + `http.path` — the UI will `fetch` this path
|
|
101
|
+
- `http.responses.200.content.application/json.schema` — real response shape
|
|
102
|
+
- `description`, `constraints`, `examples`, `errors`, `safety`
|
|
103
|
+
|
|
104
|
+
Path convention: `/api/<domain>/<resource>` (e.g. `/api/fleet/summary`). The Gateway serves it under
|
|
105
|
+
`/apps/<app-name>` (so `/apps/<app-name>/api/fleet/summary`) unless the app's `spec.apps` entry sets
|
|
106
|
+
`mount`; never write the `/apps/<app-name>` prefix into the route table yourself.
|
|
107
|
+
|
|
108
|
+
Full field notes: [`references/plugin-authoring.md`](./references/plugin-authoring.md).
|
|
109
|
+
|
|
110
|
+
### 4. Implement the controller
|
|
111
|
+
|
|
112
|
+
Build the client once per app, in a `lib/client.js` every controller requires — the
|
|
113
|
+
scaffold's `plugin/lib/client.js` is this exact pattern
|
|
114
|
+
(`createMdkClient(config)` over `require('@tetherto/mdk-gateway/plugin')`).
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
'use strict'
|
|
118
|
+
|
|
119
|
+
const mdkClient = require('../lib/client')
|
|
120
|
+
|
|
121
|
+
module.exports = async function myRoute (req) {
|
|
122
|
+
// mdkClient methods return the bare payload — never read `.payload`
|
|
123
|
+
const workersResp = await mdkClient.listWorkers()
|
|
124
|
+
const workers = (workersResp && workersResp.workers) || []
|
|
125
|
+
// … filter devices, pullTelemetry, shape response …
|
|
126
|
+
return { /* matches mdk-plugin.json schema */ }
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Typical [fan-out pattern](./references/controller-patterns.md):
|
|
131
|
+
|
|
132
|
+
1. `listWorkers()` → collect `deviceIds`.
|
|
133
|
+
2. `getCapabilities(deviceId)` → keep devices matching the contract fingerprint.
|
|
134
|
+
3. `pullTelemetry(deviceId, 'metrics')` → read `metrics.<channel>`.
|
|
135
|
+
4. Return a UI-friendly object (`unit`, totals, per-device rows).
|
|
136
|
+
|
|
137
|
+
### 5. Register in `mdk.yaml`
|
|
138
|
+
|
|
139
|
+
Already done if you scaffolded with `mdk create app` (step 2). Otherwise
|
|
140
|
+
add it by hand (`name` matches `^[a-z][a-z0-9-]*$`):
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
spec:
|
|
144
|
+
apps:
|
|
145
|
+
- name: <app-name>
|
|
146
|
+
package: <app-name>
|
|
147
|
+
config: {}
|
|
148
|
+
# mount: / # only when a client calls the routes at the Gateway root
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`spec.gateway.plugins` is rejected by the CLI; move any such entry to `spec.apps`.
|
|
152
|
+
Then restart (`mdk run`, or `mdk run app <app-name>` beside `mdk run gateway`).
|
|
153
|
+
Wiring and boot order details live in `mdk-deployment`.
|
|
154
|
+
|
|
155
|
+
### 6. Smoke-check the route
|
|
156
|
+
|
|
157
|
+
With the stack up:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
curl -s http://127.0.0.1:<gateway-port>/apps/<app-name>/api/<domain>/<resource> | jq .
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Confirm the JSON matches the schema and examples in `mdk-plugin.json`.
|
|
164
|
+
Only then build UI against it.
|
|
165
|
+
|
|
166
|
+
## Non-negotiable invariants
|
|
167
|
+
|
|
168
|
+
- **Never reference a Worker, channel, or field** that is not in an installed
|
|
169
|
+
contract. Aggregation must be site-grounded.
|
|
170
|
+
- **`mdkClient` returns bare payloads** — do not unwrap `.payload`.
|
|
171
|
+
- **Recognize device families from capability signatures** when
|
|
172
|
+
`getCapabilities` omits brand/metadata (common). Prefer telemetry name sets
|
|
173
|
+
over hard-coded `deviceId` lists.
|
|
174
|
+
- Controllers are **read-only by default**; set `"safety": "read-only"` unless
|
|
175
|
+
the route intentionally mutates (commands).
|
|
176
|
+
- Throw `ERR_SCREAMING_SNAKE` strings; declare them under `errors` in the route.
|
|
177
|
+
|
|
178
|
+
## Hand-off
|
|
179
|
+
|
|
180
|
+
| Next need | Skill |
|
|
181
|
+
| ------------------------------------- | ------------------------- |
|
|
182
|
+
| App manifest, permissions, packaging | `mdk-create-app` |
|
|
183
|
+
| Render this route in the dashboard | `mdk-ui-component` |
|
|
184
|
+
| Register / restart / verify the stack | `mdk-deployment` |
|
|
185
|
+
| Device does not exist yet | `mdk-worker-plugin` first |
|
|
186
|
+
|
|
187
|
+
## References
|
|
188
|
+
|
|
189
|
+
- [`references/plugin-authoring.md`](./references/plugin-authoring.md): `mdk-plugin.json` fields + a fully populated route example
|
|
190
|
+
- [`references/controller-patterns.md`](./references/controller-patterns.md): list / filter / `pullTelemetry` patterns
|
|
191
|
+
- [`../mdk/references/protocol.md`](../mdk/references/protocol.md): envelope / action set
|
|
192
|
+
- Shipped monorepo examples (when present): `backend/apps/demo/` (read-only fan-out), `backend/apps/agent/` (auth + stream routes)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Controller patterns
|
|
2
|
+
|
|
3
|
+
Build the client once per app, in a `lib/client.js` every controller requires. The
|
|
4
|
+
`plugin/lib/client.js` that `mdk create app` scaffolds is this exact pattern:
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
const { config } = require('@tetherto/mdk-gateway/plugin')
|
|
8
|
+
const { createMdkClient } = require('@tetherto/mdk-client')
|
|
9
|
+
module.exports = createMdkClient(config)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`config` carries the app's own Kernel identity (`kernelKey`, `kernelKeyPair`), so the client connects with the app
|
|
13
|
+
runtime's key.
|
|
14
|
+
|
|
15
|
+
Controllers are plain async functions that require it:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const mdkClient = require('../lib/client')
|
|
19
|
+
|
|
20
|
+
module.exports = async function routeName (req) {
|
|
21
|
+
// ...
|
|
22
|
+
return payload
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`mdkClient` is `@tetherto/mdk-client`. Methods return **bare payloads** — never `.payload`. It connects on first use and
|
|
27
|
+
memoizes the connection; a failure maps to `ERR_MDK_CLIENT_UNAVAILABLE` and resets so the next call retries — no `null`
|
|
28
|
+
check needed, just a `try`/`catch` around the call if you want to remap the error.
|
|
29
|
+
|
|
30
|
+
## Pattern: fan-out telemetry for a device family
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
const isTargetFamily = (capsPayload) => {
|
|
34
|
+
const telemetry =
|
|
35
|
+
(capsPayload && capsPayload.capabilities && capsPayload.capabilities.telemetry) ||
|
|
36
|
+
(capsPayload && capsPayload.telemetry) ||
|
|
37
|
+
[]
|
|
38
|
+
const names = new Set(telemetry.map((t) => t && t.name).filter(Boolean))
|
|
39
|
+
// Fingerprint derived from the installed contract — adjust per family
|
|
40
|
+
return names.has('<channel-1>') && names.has('<channel-2>')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = async function example (req) {
|
|
44
|
+
const workersResp = await mdkClient.listWorkers()
|
|
45
|
+
const workers = (workersResp && workersResp.workers) || []
|
|
46
|
+
const deviceIds = workers.flatMap((w) => w.deviceIds || [])
|
|
47
|
+
|
|
48
|
+
const rows = []
|
|
49
|
+
let total = 0
|
|
50
|
+
|
|
51
|
+
for (const deviceId of deviceIds) {
|
|
52
|
+
const caps = await mdkClient.getCapabilities(deviceId).catch(() => null)
|
|
53
|
+
if (!isTargetFamily(caps)) continue
|
|
54
|
+
|
|
55
|
+
const tel = await mdkClient.pullTelemetry(deviceId, 'metrics').catch(() => null)
|
|
56
|
+
const metrics = (tel && tel.metrics) || {}
|
|
57
|
+
const value = Number(metrics['<channel-1>']) // channel name from contract
|
|
58
|
+
if (!Number.isFinite(value)) continue
|
|
59
|
+
|
|
60
|
+
rows.push({ deviceId, value })
|
|
61
|
+
total += value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { unit: '<unit-from-contract>', total, devices: rows }
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Why capability fingerprints?
|
|
69
|
+
|
|
70
|
+
Kernel `getCapabilities` returns the capability list, not full
|
|
71
|
+
`mdk-contract.json` metadata (`brand`, `provider`, …). Matching on the
|
|
72
|
+
telemetry name set (and optionally command names) is the reliable filter.
|
|
73
|
+
Derive the set from the installed contract — do not guess.
|
|
74
|
+
|
|
75
|
+
### Channel → metrics key
|
|
76
|
+
|
|
77
|
+
`pullTelemetry(deviceId, 'metrics')` returns `{ metrics: { <channel>: value, ... } }`.
|
|
78
|
+
Keys match contract telemetry `name` fields exactly.
|
|
79
|
+
|
|
80
|
+
### Resilience
|
|
81
|
+
|
|
82
|
+
- `.catch(() => null)` per device so one offline device does not fail the route.
|
|
83
|
+
- Skip non-finite numbers instead of throwing.
|
|
84
|
+
- Empty `rows` is a valid 200 — the UI shows an empty state.
|
|
85
|
+
|
|
86
|
+
## Pattern: single-device read
|
|
87
|
+
|
|
88
|
+
When the route takes `deviceId` (query/body):
|
|
89
|
+
|
|
90
|
+
1. Validate `deviceId` is present.
|
|
91
|
+
2. Optionally `getCapabilities` to confirm the channel exists.
|
|
92
|
+
3. `pullTelemetry` once and return the shaped value.
|
|
93
|
+
|
|
94
|
+
## Errors
|
|
95
|
+
|
|
96
|
+
| Error | When |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `ERR_MDK_CLIENT_UNAVAILABLE` | `mdkClient` failed to connect (Kernel unreachable) |
|
|
99
|
+
| `ERR_<DOMAIN>_*` | Domain-specific; declare in `mdk-plugin.json` `errors` |
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# mdk-plugin.json authoring
|
|
2
|
+
|
|
3
|
+
## Package layout
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
apps/<name>/
|
|
7
|
+
├── package.json
|
|
8
|
+
├── mdk-app.yaml
|
|
9
|
+
└── plugin/
|
|
10
|
+
├── mdk-plugin.json
|
|
11
|
+
├── lib/client.js
|
|
12
|
+
└── controllers/<route>.js
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### package.json
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"name": "<name>",
|
|
20
|
+
"version": "0.1.0",
|
|
21
|
+
"private": true,
|
|
22
|
+
"description": "<one-line: what telemetry/commands this aggregates>"
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`mdk create app <name>` sets this for you (bare `<name>`, or `@org/<name>`
|
|
27
|
+
with `--org`) — `mdk.yaml` references whatever name lands here, the same way
|
|
28
|
+
it would a published package. `mdk-app.yaml`'s `version` must equal this
|
|
29
|
+
`version`; the [`mdk-create-app`](../../mdk-create-app/SKILL.md) skill owns the rest of that manifest.
|
|
30
|
+
|
|
31
|
+
### mdk-plugin.json — required route fields
|
|
32
|
+
|
|
33
|
+
| Field | Purpose |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `name` / `version` / `description` | Package identity (often mirrors package.json) |
|
|
36
|
+
| `routes[].id` | Stable id (`<domain>.<resource>`) |
|
|
37
|
+
| `routes[].handler` | Relative path, e.g. `./controllers/power.js` |
|
|
38
|
+
| `routes[].auth` | `true` makes the Gateway answer `401` unless its `authVerifier` returns an identity, which reaches the controller as `req._info`. The Gateway hands `permissions` to that verifier as `route.permissions` and does not interpret them itself |
|
|
39
|
+
| `routes[].http.method` | `GET` / `POST` / … |
|
|
40
|
+
| `routes[].http.path` | Path the UI fetches (`/api/...`), served under `/apps/<app-name>` unless the app is mounted elsewhere |
|
|
41
|
+
| `routes[].http.responses` | Document `200` schema + error statuses |
|
|
42
|
+
| `routes[].description` | What the route does (operator + LLM context) |
|
|
43
|
+
| `routes[].constraints` | Runtime preconditions (Kernel connected, channels present) |
|
|
44
|
+
| `routes[].examples` | At least one request/response example with realistic values |
|
|
45
|
+
| `routes[].errors` | `ERR_*` strings the controller may throw |
|
|
46
|
+
| `routes[].safety` | `"read-only"` unless the route mutates devices. `mdk run mcp` withholds every other route unless started with `--allow-write` |
|
|
47
|
+
|
|
48
|
+
## Response schema rules
|
|
49
|
+
|
|
50
|
+
- Schema properties must match what the controller **actually returns**.
|
|
51
|
+
- Prefer flat, UI-ready shapes:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"unit": "W",
|
|
56
|
+
"total": 3400,
|
|
57
|
+
"devices": [
|
|
58
|
+
{ "deviceId": "dev-0", "value": 3400 }
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- Include the contract `unit` as a field so the UI never hard-codes it.
|
|
64
|
+
- Name numeric fields with the unit suffix when helpful (`powerW`, not `power`
|
|
65
|
+
next to a separate `unit`), so cards can round without guessing.
|
|
66
|
+
|
|
67
|
+
## Fully populated route example
|
|
68
|
+
|
|
69
|
+
Every field from the table above, together, on one route:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"id": "example.metric",
|
|
74
|
+
"handler": "./controllers/metric.js",
|
|
75
|
+
"auth": false,
|
|
76
|
+
"http": {
|
|
77
|
+
"method": "GET",
|
|
78
|
+
"path": "/api/example/metric",
|
|
79
|
+
"responses": {
|
|
80
|
+
"200": {
|
|
81
|
+
"description": "Aggregated metric across matching devices.",
|
|
82
|
+
"content": {
|
|
83
|
+
"application/json": {
|
|
84
|
+
"schema": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"properties": {
|
|
87
|
+
"unit": { "type": "string" },
|
|
88
|
+
"total": { "type": "number" },
|
|
89
|
+
"devices": { "type": "array" }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"400": { "description": "MDK client unavailable." }
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"description": "Fans out telemetry.pull to matching devices and returns shaped rows.",
|
|
99
|
+
"constraints": [
|
|
100
|
+
"Requires the MDK client to be connected to a Kernel instance.",
|
|
101
|
+
"Devices must expose the contract channel(s) this route reads."
|
|
102
|
+
],
|
|
103
|
+
"examples": [
|
|
104
|
+
{
|
|
105
|
+
"description": "Example success payload",
|
|
106
|
+
"request": "GET /api/example/metric",
|
|
107
|
+
"response": { "unit": "W", "total": 0, "devices": [] }
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"errors": ["ERR_MDK_CLIENT_UNAVAILABLE"],
|
|
111
|
+
"safety": "read-only"
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Registering
|
|
116
|
+
|
|
117
|
+
`mdk create app <name>` does all of this: links the package into the project's
|
|
118
|
+
root `package.json` (root `npm install`) and appends it under `mdk.yaml` →
|
|
119
|
+
`spec.apps[]` → `{ name: <name>, package: <name>, config: {} }`. Scaffolding by
|
|
120
|
+
hand instead, do the same two steps yourself, run `mdk validate app <name>`,
|
|
121
|
+
then restart with `mdk run`.
|