@kvell007/embed-labs-local-bridge 0.1.0-alpha.21 → 0.1.0-alpha.23

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 (50) hide show
  1. package/README.md +16 -11
  2. package/dist/index.d.ts +2 -1
  3. package/dist/index.js +125 -4
  4. package/package.json +8 -4
  5. package/dist/adapters/debug.d.ts +0 -2
  6. package/dist/adapters/debug.js +0 -124
  7. package/dist/adapters/debug.js.map +0 -1
  8. package/dist/adapters/deploy.d.ts +0 -5
  9. package/dist/adapters/deploy.js +0 -132
  10. package/dist/adapters/deploy.js.map +0 -1
  11. package/dist/adapters/flash.d.ts +0 -3
  12. package/dist/adapters/flash.js +0 -169
  13. package/dist/adapters/flash.js.map +0 -1
  14. package/dist/adapters/logo.d.ts +0 -2
  15. package/dist/adapters/logo.js +0 -108
  16. package/dist/adapters/logo.js.map +0 -1
  17. package/dist/adapters/network.d.ts +0 -22
  18. package/dist/adapters/network.js +0 -64
  19. package/dist/adapters/network.js.map +0 -1
  20. package/dist/adapters/rp2350-monitor.d.ts +0 -121
  21. package/dist/adapters/rp2350-monitor.js +0 -404
  22. package/dist/adapters/rp2350-monitor.js.map +0 -1
  23. package/dist/adapters/serial.d.ts +0 -3
  24. package/dist/adapters/serial.js +0 -133
  25. package/dist/adapters/serial.js.map +0 -1
  26. package/dist/adapters/taishanpi-live.d.ts +0 -163
  27. package/dist/adapters/taishanpi-live.js +0 -349
  28. package/dist/adapters/taishanpi-live.js.map +0 -1
  29. package/dist/adapters/uf2.d.ts +0 -1
  30. package/dist/adapters/uf2.js +0 -53
  31. package/dist/adapters/uf2.js.map +0 -1
  32. package/dist/adapters/usb.d.ts +0 -3
  33. package/dist/adapters/usb.js +0 -102
  34. package/dist/adapters/usb.js.map +0 -1
  35. package/dist/hardware.d.ts +0 -19
  36. package/dist/hardware.js +0 -417
  37. package/dist/hardware.js.map +0 -1
  38. package/dist/index.js.map +0 -1
  39. package/dist/jobs.d.ts +0 -4
  40. package/dist/jobs.js +0 -125
  41. package/dist/jobs.js.map +0 -1
  42. package/dist/process.d.ts +0 -13
  43. package/dist/process.js +0 -128
  44. package/dist/process.js.map +0 -1
  45. package/dist/server.d.ts +0 -7
  46. package/dist/server.js +0 -399
  47. package/dist/server.js.map +0 -1
  48. package/dist/tools.d.ts +0 -3
  49. package/dist/tools.js +0 -1254
  50. package/dist/tools.js.map +0 -1
package/dist/tools.js DELETED
@@ -1,1254 +0,0 @@
1
- import net from "node:net";
2
- import { fail, ok } from "@embed-labs/protocol";
3
- import { buildFlashPlan, captureRp2350MonitorLogic, decodeRp2350MonitorLogic, deployTaishanPiApplication, getRp2350MonitorStatus, getTaishanPiQmlRuntimeStatus, probeDevices, readTaishanPiCpuFrequency, readTaishanPiTemperature, rp2350MonitorCommandNeedsApproval, runRp2350MonitorCommand, scanTaishanPiBluetoothDevices, scanDebugTools, scanHardware, scanTaishanPiWifiNetworks, startTaishanPiQmlRuntime, validateTaishanPiDeployRequest } from "./hardware.js";
4
- import { startFlashJob } from "./jobs.js";
5
- const PROBE_REQUEST_FIELDS = new Set(["host", "ports", "serial_paths", "timeout_ms"]);
6
- const FLASH_REQUEST_FIELDS = new Set(["board_id", "variant_id", "hardware_profile_id", "profile_id", "image_dir", "artifact_path", "target_volume_path"]);
7
- const TAISHANPI_LIVE_FIELDS = new Set(["board_id", "variant_id", "host", "user", "timeout_seconds", "interface", "duration_seconds", "port", "force_restart"]);
8
- const RP2350_MONITOR_BASE_FIELDS = new Set(["transport", "serial_path", "host", "port", "baud", "timeout_ms", "monitor_root"]);
9
- const RP2350_MONITOR_STATUS_FIELDS = new Set([...RP2350_MONITOR_BASE_FIELDS, "include_caps"]);
10
- const RP2350_MONITOR_COMMAND_FIELDS = new Set([...RP2350_MONITOR_BASE_FIELDS, "command", "args"]);
11
- const RP2350_MONITOR_LOGIC_CAPTURE_FIELDS = new Set([
12
- ...RP2350_MONITOR_BASE_FIELDS,
13
- "pin_base",
14
- "pin_count",
15
- "sample_rate",
16
- "samples",
17
- "output_path",
18
- "pull",
19
- "release",
20
- "wait_timeout_ms",
21
- "read_timeout_ms",
22
- "pre_samples",
23
- "post_samples",
24
- "search_samples",
25
- "burst_count",
26
- "trigger_pin",
27
- "trigger_mode",
28
- "trigger_level",
29
- "trigger_mask",
30
- "trigger_value",
31
- "channel_names",
32
- "pin_pulls"
33
- ]);
34
- const RP2350_MONITOR_LOGIC_DECODE_FIELDS = new Set([
35
- "input_path",
36
- "decoder",
37
- "output_path",
38
- "capture_id",
39
- "start_sample",
40
- "end_sample",
41
- "pins",
42
- "rx_pin",
43
- "baud",
44
- "data_bits",
45
- "parity",
46
- "stop_bits",
47
- "invert",
48
- "sck_pin",
49
- "mosi_pin",
50
- "miso_pin",
51
- "cs_pin",
52
- "mode",
53
- "cs_active_high",
54
- "word_bits",
55
- "bit_order",
56
- "sda_pin",
57
- "scl_pin",
58
- "monitor_root"
59
- ]);
60
- export function listToolCapabilities() {
61
- return {
62
- observed_at: new Date().toISOString(),
63
- capabilities: [
64
- {
65
- capability_id: "debug.tools.scan",
66
- display_name: "Debug Tool Scan",
67
- description: "Scan OpenOCD, probe-rs, pyOCD, and J-Link command availability without opening a debug session.",
68
- runtime: "local_bridge",
69
- category: "debug",
70
- destructive: false,
71
- requires_approval: false,
72
- access_tier: "free_registered",
73
- free_for_registered_users: true,
74
- requires_registered_account: true,
75
- requires_server_workspace: false,
76
- allocates_server_workspace: false,
77
- allocates_server_resource_lease: false,
78
- meters: [],
79
- status: "available",
80
- input_schema: {
81
- type: "object",
82
- additionalProperties: false,
83
- properties: {}
84
- }
85
- },
86
- {
87
- capability_id: "device.scan",
88
- display_name: "Device Scan",
89
- description: "Detect USB, serial, UF2, Rockchip, and TaishanPi USB ECM devices visible to the local machine.",
90
- runtime: "local_bridge",
91
- category: "device",
92
- destructive: false,
93
- requires_approval: false,
94
- access_tier: "free_registered",
95
- free_for_registered_users: true,
96
- requires_registered_account: true,
97
- requires_server_workspace: false,
98
- allocates_server_workspace: false,
99
- allocates_server_resource_lease: false,
100
- meters: [],
101
- status: "available",
102
- input_schema: {
103
- type: "object",
104
- additionalProperties: false,
105
- properties: {}
106
- }
107
- },
108
- {
109
- capability_id: "device.probe",
110
- display_name: "Device Probe",
111
- description: "Probe selected TCP ports and serial paths with bounded timeouts.",
112
- runtime: "local_bridge",
113
- category: "device",
114
- destructive: false,
115
- requires_approval: false,
116
- access_tier: "free_registered",
117
- free_for_registered_users: true,
118
- requires_registered_account: true,
119
- requires_server_workspace: false,
120
- allocates_server_workspace: false,
121
- allocates_server_resource_lease: false,
122
- meters: [],
123
- status: "available",
124
- input_schema: {
125
- type: "object",
126
- additionalProperties: false,
127
- properties: {
128
- host: { type: "string" },
129
- ports: { type: "array", items: { type: "integer", minimum: 1, maximum: 65535 } },
130
- serial_paths: { type: "array", items: { type: "string" } },
131
- timeout_ms: { type: "number", minimum: 100, maximum: 5000 }
132
- }
133
- }
134
- },
135
- {
136
- capability_id: "wifi.scan",
137
- display_name: "TaishanPi WiFi Scan",
138
- description: "Scan nearby WiFi networks from the connected TaishanPi Linux runtime over SSH.",
139
- runtime: "local_bridge",
140
- category: "board",
141
- destructive: false,
142
- requires_approval: false,
143
- access_tier: "free_registered",
144
- free_for_registered_users: true,
145
- requires_registered_account: true,
146
- requires_server_workspace: false,
147
- allocates_server_workspace: false,
148
- allocates_server_resource_lease: false,
149
- meters: [],
150
- status: "available",
151
- input_schema: taishanPiLiveInputSchema({
152
- interface: { type: "string" }
153
- })
154
- },
155
- {
156
- capability_id: "bluetooth.scan",
157
- display_name: "TaishanPi Bluetooth Scan",
158
- description: "Scan nearby Bluetooth devices from the connected TaishanPi Linux runtime over SSH when a Bluetooth controller is initialized.",
159
- runtime: "local_bridge",
160
- category: "board",
161
- destructive: false,
162
- requires_approval: false,
163
- access_tier: "free_registered",
164
- free_for_registered_users: true,
165
- requires_registered_account: true,
166
- requires_server_workspace: false,
167
- allocates_server_workspace: false,
168
- allocates_server_resource_lease: false,
169
- meters: [],
170
- status: "available",
171
- input_schema: taishanPiLiveInputSchema({
172
- duration_seconds: { type: "number", minimum: 3, maximum: 30 }
173
- })
174
- },
175
- {
176
- capability_id: "chip.cpu.frequency",
177
- display_name: "TaishanPi CPU Frequency",
178
- description: "Read current TaishanPi CPU frequency values from the Linux runtime over SSH.",
179
- runtime: "local_bridge",
180
- category: "board",
181
- destructive: false,
182
- requires_approval: false,
183
- access_tier: "free_registered",
184
- free_for_registered_users: true,
185
- requires_registered_account: true,
186
- requires_server_workspace: false,
187
- allocates_server_workspace: false,
188
- allocates_server_resource_lease: false,
189
- meters: [],
190
- status: "available",
191
- input_schema: taishanPiLiveInputSchema()
192
- },
193
- {
194
- capability_id: "chip.temperature",
195
- display_name: "TaishanPi Temperature",
196
- description: "Read TaishanPi thermal-zone temperatures from the Linux runtime over SSH.",
197
- runtime: "local_bridge",
198
- category: "board",
199
- destructive: false,
200
- requires_approval: false,
201
- access_tier: "free_registered",
202
- free_for_registered_users: true,
203
- requires_registered_account: true,
204
- requires_server_workspace: false,
205
- allocates_server_workspace: false,
206
- allocates_server_resource_lease: false,
207
- meters: [],
208
- status: "available",
209
- input_schema: taishanPiLiveInputSchema()
210
- },
211
- {
212
- capability_id: "qml.runtime.status",
213
- display_name: "TaishanPi QML Runtime Status",
214
- description: "Check whether the TaishanPi QtQuick board runtime is installed and reachable.",
215
- runtime: "local_bridge",
216
- category: "board",
217
- destructive: false,
218
- requires_approval: false,
219
- access_tier: "free_registered",
220
- free_for_registered_users: true,
221
- requires_registered_account: true,
222
- requires_server_workspace: false,
223
- allocates_server_workspace: false,
224
- allocates_server_resource_lease: false,
225
- meters: [],
226
- status: "available",
227
- input_schema: taishanPiLiveInputSchema({
228
- port: { type: "number", minimum: 1, maximum: 65535 }
229
- })
230
- },
231
- {
232
- capability_id: "qml.runtime.start",
233
- display_name: "TaishanPi QML Runtime Start",
234
- description: "Start the TaishanPi QtQuick board runtime over SSH and verify that its TCP port is reachable.",
235
- runtime: "local_bridge",
236
- category: "board",
237
- destructive: false,
238
- requires_approval: false,
239
- access_tier: "free_registered",
240
- free_for_registered_users: true,
241
- requires_registered_account: true,
242
- requires_server_workspace: false,
243
- allocates_server_workspace: false,
244
- allocates_server_resource_lease: false,
245
- meters: [],
246
- status: "available",
247
- input_schema: taishanPiLiveInputSchema({
248
- port: { type: "number", minimum: 1, maximum: 65535 },
249
- force_restart: { type: "boolean" }
250
- })
251
- },
252
- {
253
- capability_id: "rp2350.monitor.status",
254
- display_name: "RP2350 Monitor Status",
255
- description: "Auto-detect or connect to RP2350-Monitor over USB CDC or Wi-Fi TCP and read status, logic analyzer caps, probe caps, pins, and channels.",
256
- runtime: "local_bridge",
257
- category: "board",
258
- destructive: false,
259
- requires_approval: false,
260
- access_tier: "free_registered",
261
- free_for_registered_users: true,
262
- requires_registered_account: true,
263
- requires_server_workspace: false,
264
- allocates_server_workspace: false,
265
- allocates_server_resource_lease: false,
266
- meters: [],
267
- status: "available",
268
- input_schema: rp2350MonitorBaseInputSchema({
269
- include_caps: { type: "boolean" }
270
- })
271
- },
272
- {
273
- capability_id: "rp2350.monitor.logic.capture",
274
- display_name: "RP2350 Monitor Logic Capture",
275
- description: "Capture GPIO logic-analyzer samples through RP2350-Monitor and write a compact local JSONL capture file for decoding or evidence.",
276
- runtime: "local_bridge",
277
- category: "board",
278
- destructive: false,
279
- requires_approval: false,
280
- access_tier: "free_registered",
281
- free_for_registered_users: true,
282
- requires_registered_account: true,
283
- requires_server_workspace: false,
284
- allocates_server_workspace: false,
285
- allocates_server_resource_lease: false,
286
- meters: [],
287
- status: "available",
288
- input_schema: rp2350MonitorBaseInputSchema({
289
- pin_base: { type: "number", minimum: 0, maximum: 47 },
290
- pin_count: { type: "number", minimum: 1, maximum: 16 },
291
- sample_rate: { type: "number", minimum: 1, maximum: 125000000 },
292
- samples: { type: "number", minimum: 1, maximum: 1048576 },
293
- output_path: { type: "string" },
294
- pull: { enum: ["none", "up", "down"] },
295
- release: { type: "boolean" },
296
- wait_timeout_ms: { type: "number", minimum: 1000, maximum: 120000 },
297
- read_timeout_ms: { type: "number", minimum: 1000, maximum: 120000 },
298
- pre_samples: { type: "number", minimum: 0 },
299
- post_samples: { type: "number", minimum: 0 },
300
- search_samples: { type: "number", minimum: 0 },
301
- burst_count: { type: "number", minimum: 1, maximum: 32 },
302
- trigger_pin: { type: "number", minimum: 0, maximum: 47 },
303
- trigger_mode: { enum: ["level", "rising", "falling", "pattern"] },
304
- trigger_level: { type: "boolean" },
305
- trigger_mask: { type: "number", minimum: 0 },
306
- trigger_value: { type: "number", minimum: 0 },
307
- channel_names: { type: "object", additionalProperties: { type: "string" } },
308
- pin_pulls: { type: "object", additionalProperties: { enum: ["none", "up", "down"] } }
309
- }, ["pin_base", "pin_count"])
310
- },
311
- {
312
- capability_id: "rp2350.monitor.logic.decode",
313
- display_name: "RP2350 Monitor Logic Decode",
314
- description: "Decode a local RP2350-Monitor JSONL logic capture as summary, edges, UART, SPI, or I2C without touching hardware.",
315
- runtime: "local_bridge",
316
- category: "board",
317
- destructive: false,
318
- requires_approval: false,
319
- access_tier: "free_registered",
320
- free_for_registered_users: true,
321
- requires_registered_account: true,
322
- requires_server_workspace: false,
323
- allocates_server_workspace: false,
324
- allocates_server_resource_lease: false,
325
- meters: [],
326
- status: "available",
327
- input_schema: {
328
- type: "object",
329
- additionalProperties: false,
330
- required: ["input_path"],
331
- properties: {
332
- input_path: { type: "string" },
333
- decoder: { enum: ["summary", "bursts", "edges", "uart", "spi", "i2c"] },
334
- output_path: { type: "string" },
335
- capture_id: { type: "number", minimum: 0 },
336
- start_sample: { type: "number", minimum: 0 },
337
- end_sample: { type: "number", minimum: 0 },
338
- pins: { type: "array", items: { type: "number", minimum: 0, maximum: 47 } },
339
- rx_pin: { type: "number", minimum: 0, maximum: 47 },
340
- baud: { type: "number", minimum: 1 },
341
- data_bits: { type: "number", minimum: 5, maximum: 9 },
342
- parity: { enum: ["none", "even", "odd"] },
343
- stop_bits: { type: "number", minimum: 1, maximum: 2 },
344
- invert: { type: "boolean" },
345
- sck_pin: { type: "number", minimum: 0, maximum: 47 },
346
- mosi_pin: { type: "number", minimum: 0, maximum: 47 },
347
- miso_pin: { type: "number", minimum: 0, maximum: 47 },
348
- cs_pin: { type: "number", minimum: 0, maximum: 47 },
349
- mode: { enum: [0, 1, 2, 3] },
350
- cs_active_high: { type: "boolean" },
351
- word_bits: { type: "number", minimum: 1, maximum: 32 },
352
- bit_order: { enum: ["msb", "lsb"] },
353
- sda_pin: { type: "number", minimum: 0, maximum: 47 },
354
- scl_pin: { type: "number", minimum: 0, maximum: 47 },
355
- monitor_root: { type: "string" }
356
- }
357
- }
358
- },
359
- {
360
- capability_id: "rp2350.monitor.command",
361
- display_name: "RP2350 Monitor Command",
362
- description: "Run an allowlisted RP2350-Monitor CLI command over USB CDC or Wi-Fi TCP. Mutating commands require approved=true.",
363
- runtime: "local_bridge",
364
- category: "board",
365
- destructive: true,
366
- requires_approval: true,
367
- access_tier: "free_registered",
368
- free_for_registered_users: true,
369
- requires_registered_account: true,
370
- requires_server_workspace: false,
371
- allocates_server_workspace: false,
372
- allocates_server_resource_lease: false,
373
- meters: [],
374
- status: "available",
375
- input_schema: rp2350MonitorBaseInputSchema({
376
- command: { type: "string" },
377
- args: { type: "array", items: { type: "string" }, maxItems: 48 }
378
- }, ["command"])
379
- },
380
- {
381
- capability_id: "flash.plan",
382
- display_name: "Flash Plan",
383
- description: "Create a local flash plan for a local image or firmware artifact without allocating a server workspace.",
384
- runtime: "local_bridge",
385
- category: "flash",
386
- destructive: false,
387
- requires_approval: false,
388
- access_tier: "free_registered",
389
- free_for_registered_users: true,
390
- requires_registered_account: true,
391
- requires_server_workspace: false,
392
- allocates_server_workspace: false,
393
- allocates_server_resource_lease: false,
394
- meters: [],
395
- status: "available",
396
- input_schema: {
397
- type: "object",
398
- additionalProperties: false,
399
- required: ["board_id"],
400
- properties: {
401
- board_id: { type: "string" },
402
- variant_id: { type: "string" },
403
- hardware_profile_id: { type: "string" },
404
- profile_id: { type: "string" },
405
- image_dir: { type: "string" },
406
- artifact_path: { type: "string" },
407
- target_volume_path: { type: "string" }
408
- }
409
- }
410
- },
411
- {
412
- capability_id: "flash.run",
413
- display_name: "Flash Run",
414
- description: "Run a local flash job for a local image or firmware artifact after explicit user approval.",
415
- runtime: "local_bridge",
416
- category: "flash",
417
- destructive: true,
418
- requires_approval: true,
419
- access_tier: "free_registered",
420
- free_for_registered_users: true,
421
- requires_registered_account: true,
422
- requires_server_workspace: false,
423
- allocates_server_workspace: false,
424
- allocates_server_resource_lease: false,
425
- meters: [],
426
- status: "available",
427
- input_schema: {
428
- type: "object",
429
- additionalProperties: false,
430
- required: ["board_id"],
431
- properties: {
432
- board_id: { type: "string" },
433
- variant_id: { type: "string" },
434
- hardware_profile_id: { type: "string" },
435
- profile_id: { type: "string" },
436
- image_dir: { type: "string" },
437
- artifact_path: { type: "string" },
438
- target_volume_path: { type: "string" }
439
- }
440
- }
441
- },
442
- {
443
- capability_id: "taishanpi.deploy",
444
- display_name: "TaishanPi Deploy",
445
- description: "Copy a local artifact to a TaishanPi runtime over SSH and optionally start it.",
446
- runtime: "local_bridge",
447
- category: "board",
448
- destructive: false,
449
- requires_approval: true,
450
- access_tier: "paid_resource",
451
- free_for_registered_users: false,
452
- requires_registered_account: true,
453
- requires_server_workspace: false,
454
- allocates_server_workspace: false,
455
- allocates_server_resource_lease: false,
456
- meters: ["local_tool_service_tokens"],
457
- status: "available",
458
- input_schema: {
459
- type: "object",
460
- additionalProperties: false,
461
- required: ["host", "artifact_path"],
462
- properties: {
463
- board_id: { const: "taishanpi" },
464
- host: { type: "string" },
465
- user: { type: "string" },
466
- artifact_path: { type: "string" },
467
- remote_path: { type: "string" },
468
- run: { type: "boolean" },
469
- timeout_seconds: { type: "number", minimum: 1 }
470
- }
471
- }
472
- }
473
- ]
474
- };
475
- }
476
- export async function invokeToolCapability(input) {
477
- const parsed = parseToolInvokeRequest(input);
478
- if (!parsed.ok) {
479
- return parsed;
480
- }
481
- const observedAt = new Date().toISOString();
482
- if (parsed.data.capability_id === "debug.tools.scan") {
483
- const inputError = emptyInputError(parsed.data.input, parsed.data.capability_id);
484
- if (inputError) {
485
- return fail("invalid_request", inputError);
486
- }
487
- return ok({ capability_id: parsed.data.capability_id, result: await scanDebugTools(), observed_at: observedAt });
488
- }
489
- if (parsed.data.capability_id === "device.scan") {
490
- const inputError = emptyInputError(parsed.data.input, parsed.data.capability_id);
491
- if (inputError) {
492
- return fail("invalid_request", inputError);
493
- }
494
- return ok({ capability_id: parsed.data.capability_id, result: await scanHardware(), observed_at: observedAt });
495
- }
496
- if (parsed.data.capability_id === "device.probe") {
497
- const probe = parseDeviceProbeInput(parsed.data.input ?? {});
498
- if ("error" in probe) {
499
- return fail("invalid_request", probe.error);
500
- }
501
- return ok({ capability_id: parsed.data.capability_id, result: await probeDevices(probe.request), observed_at: observedAt });
502
- }
503
- if (parsed.data.capability_id === "wifi.scan") {
504
- const live = parseTaishanPiLiveInput(parsed.data.input ?? {}, { interface: true });
505
- if ("error" in live) {
506
- return fail("invalid_request", live.error);
507
- }
508
- return ok({ capability_id: parsed.data.capability_id, result: await scanTaishanPiWifiNetworks(live.request), observed_at: observedAt });
509
- }
510
- if (parsed.data.capability_id === "bluetooth.scan") {
511
- const live = parseTaishanPiLiveInput(parsed.data.input ?? {}, { duration_seconds: true });
512
- if ("error" in live) {
513
- return fail("invalid_request", live.error);
514
- }
515
- return ok({ capability_id: parsed.data.capability_id, result: await scanTaishanPiBluetoothDevices(live.request), observed_at: observedAt });
516
- }
517
- if (parsed.data.capability_id === "chip.cpu.frequency") {
518
- const live = parseTaishanPiLiveInput(parsed.data.input ?? {});
519
- if ("error" in live) {
520
- return fail("invalid_request", live.error);
521
- }
522
- return ok({ capability_id: parsed.data.capability_id, result: await readTaishanPiCpuFrequency(live.request), observed_at: observedAt });
523
- }
524
- if (parsed.data.capability_id === "chip.temperature") {
525
- const live = parseTaishanPiLiveInput(parsed.data.input ?? {});
526
- if ("error" in live) {
527
- return fail("invalid_request", live.error);
528
- }
529
- return ok({ capability_id: parsed.data.capability_id, result: await readTaishanPiTemperature(live.request), observed_at: observedAt });
530
- }
531
- if (parsed.data.capability_id === "qml.runtime.status") {
532
- const live = parseTaishanPiLiveInput(parsed.data.input ?? {}, { port: true });
533
- if ("error" in live) {
534
- return fail("invalid_request", live.error);
535
- }
536
- return ok({ capability_id: parsed.data.capability_id, result: await getTaishanPiQmlRuntimeStatus(live.request), observed_at: observedAt });
537
- }
538
- if (parsed.data.capability_id === "qml.runtime.start") {
539
- const live = parseTaishanPiLiveInput(parsed.data.input ?? {}, { port: true, force_restart: true });
540
- if ("error" in live) {
541
- return fail("invalid_request", live.error);
542
- }
543
- return ok({ capability_id: parsed.data.capability_id, result: await startTaishanPiQmlRuntime(live.request), observed_at: observedAt });
544
- }
545
- if (parsed.data.capability_id === "rp2350.monitor.status") {
546
- const rpmon = parseRp2350MonitorStatusInput(parsed.data.input ?? {});
547
- if ("error" in rpmon) {
548
- return fail("invalid_request", rpmon.error);
549
- }
550
- try {
551
- return ok({ capability_id: parsed.data.capability_id, result: await getRp2350MonitorStatus(rpmon.request), observed_at: observedAt });
552
- }
553
- catch (error) {
554
- return fail("rp2350_monitor_failed", error instanceof Error ? error.message : String(error));
555
- }
556
- }
557
- if (parsed.data.capability_id === "rp2350.monitor.logic.capture") {
558
- const rpmon = parseRp2350MonitorLogicCaptureInput(parsed.data.input ?? {});
559
- if ("error" in rpmon) {
560
- return fail("invalid_request", rpmon.error);
561
- }
562
- try {
563
- return ok({ capability_id: parsed.data.capability_id, result: await captureRp2350MonitorLogic(rpmon.request), observed_at: observedAt });
564
- }
565
- catch (error) {
566
- return fail("rp2350_monitor_failed", error instanceof Error ? error.message : String(error));
567
- }
568
- }
569
- if (parsed.data.capability_id === "rp2350.monitor.logic.decode") {
570
- const rpmon = parseRp2350MonitorLogicDecodeInput(parsed.data.input ?? {});
571
- if ("error" in rpmon) {
572
- return fail("invalid_request", rpmon.error);
573
- }
574
- try {
575
- return ok({ capability_id: parsed.data.capability_id, result: await decodeRp2350MonitorLogic(rpmon.request), observed_at: observedAt });
576
- }
577
- catch (error) {
578
- return fail("rp2350_monitor_failed", error instanceof Error ? error.message : String(error));
579
- }
580
- }
581
- if (parsed.data.capability_id === "rp2350.monitor.command") {
582
- const rpmon = parseRp2350MonitorCommandInput(parsed.data.input ?? {});
583
- if ("error" in rpmon) {
584
- return fail("invalid_request", rpmon.error);
585
- }
586
- if (rp2350MonitorCommandNeedsApproval(rpmon.request.command) && parsed.data.approved !== true) {
587
- return fail("approval_required", `rp2350.monitor.command ${rpmon.request.command} requires approved=true because it can modify board, bus, Wi-Fi, probe, or GPIO state.`);
588
- }
589
- try {
590
- return ok({ capability_id: parsed.data.capability_id, result: await runRp2350MonitorCommand(rpmon.request), observed_at: observedAt });
591
- }
592
- catch (error) {
593
- return fail("rp2350_monitor_failed", error instanceof Error ? error.message : String(error));
594
- }
595
- }
596
- if (parsed.data.capability_id === "flash.plan") {
597
- const flash = parseFlashPlanInput(parsed.data.input ?? {});
598
- if ("error" in flash) {
599
- return fail("invalid_request", flash.error);
600
- }
601
- return ok({ capability_id: parsed.data.capability_id, result: await buildFlashPlan(flash.request), observed_at: observedAt });
602
- }
603
- if (parsed.data.capability_id === "flash.run") {
604
- if (parsed.data.approved !== true) {
605
- return fail("approval_required", "flash.run requires approved=true because it can write a local image or firmware artifact to a board.");
606
- }
607
- const flash = parseFlashPlanInput(parsed.data.input ?? {});
608
- if ("error" in flash) {
609
- return fail("invalid_request", flash.error);
610
- }
611
- return ok({ capability_id: parsed.data.capability_id, result: await startFlashJob({ ...flash.request, approved: true }), observed_at: observedAt });
612
- }
613
- if (parsed.data.capability_id === "taishanpi.deploy") {
614
- if (parsed.data.approved !== true) {
615
- return fail("approval_required", "taishanpi.deploy requires approved=true because it writes to a board over SSH.");
616
- }
617
- const deploy = validateTaishanPiDeployRequest(parsed.data.input ?? {});
618
- if ("error" in deploy) {
619
- return fail("invalid_request", deploy.error);
620
- }
621
- const deployed = await deployTaishanPiApplication(deploy.request);
622
- if ("ok" in deployed && deployed.ok === false) {
623
- return deployed;
624
- }
625
- return ok({ capability_id: parsed.data.capability_id, result: deployed, observed_at: observedAt });
626
- }
627
- return fail("tool_capability_not_found", `No local tool capability found for ${parsed.data.capability_id}.`);
628
- }
629
- function emptyInputError(input, capabilityId) {
630
- if (input === undefined || Object.keys(input).length === 0) {
631
- return undefined;
632
- }
633
- return `${capabilityId} does not accept input fields: ${Object.keys(input).join(", ")}.`;
634
- }
635
- function taishanPiLiveInputSchema(extraProperties = {}) {
636
- return {
637
- type: "object",
638
- additionalProperties: false,
639
- properties: {
640
- board_id: { const: "taishanpi" },
641
- variant_id: { type: "string" },
642
- host: { type: "string" },
643
- user: { type: "string" },
644
- timeout_seconds: { type: "number", minimum: 3, maximum: 60 },
645
- ...extraProperties
646
- }
647
- };
648
- }
649
- function rp2350MonitorBaseInputSchema(extraProperties = {}, required = []) {
650
- return {
651
- type: "object",
652
- additionalProperties: false,
653
- required,
654
- properties: {
655
- transport: { enum: ["auto", "serial", "tcp"] },
656
- serial_path: { type: "string" },
657
- host: { type: "string" },
658
- port: { type: "number", minimum: 1, maximum: 65535 },
659
- baud: { type: "number", minimum: 300, maximum: 4000000 },
660
- timeout_ms: { type: "number", minimum: 100, maximum: 120000 },
661
- monitor_root: { type: "string" },
662
- ...extraProperties
663
- }
664
- };
665
- }
666
- function parseRp2350MonitorStatusInput(input) {
667
- const base = parseRp2350MonitorBaseInput(input, RP2350_MONITOR_STATUS_FIELDS);
668
- if ("error" in base) {
669
- return base;
670
- }
671
- const body = input;
672
- if (body.include_caps !== undefined && typeof body.include_caps !== "boolean") {
673
- return { error: "include_caps must be a boolean when provided." };
674
- }
675
- return { request: { ...base.request, include_caps: typeof body.include_caps === "boolean" ? body.include_caps : undefined } };
676
- }
677
- function parseRp2350MonitorCommandInput(input) {
678
- const base = parseRp2350MonitorBaseInput(input, RP2350_MONITOR_COMMAND_FIELDS);
679
- if ("error" in base) {
680
- return base;
681
- }
682
- const body = input;
683
- const command = optionalString(body.command);
684
- if (!command) {
685
- return { error: "command is required." };
686
- }
687
- let args;
688
- if (body.args !== undefined) {
689
- if (!Array.isArray(body.args)) {
690
- return { error: "args must be an array of strings." };
691
- }
692
- const invalid = body.args.find((item) => typeof item !== "string" || item.length > 512);
693
- if (invalid !== undefined) {
694
- return { error: "args must contain strings up to 512 characters." };
695
- }
696
- if (body.args.length > 48) {
697
- return { error: "args may contain at most 48 items." };
698
- }
699
- args = body.args.map((item) => item.trim()).filter(Boolean);
700
- }
701
- return { request: { ...base.request, command, args } };
702
- }
703
- function parseRp2350MonitorLogicCaptureInput(input) {
704
- const base = parseRp2350MonitorBaseInput(input, RP2350_MONITOR_LOGIC_CAPTURE_FIELDS);
705
- if ("error" in base) {
706
- return base;
707
- }
708
- const body = input;
709
- const pinBase = requiredNumber(body.pin_base, "pin_base", 0, 47);
710
- if ("error" in pinBase) {
711
- return pinBase;
712
- }
713
- const pinCount = requiredNumber(body.pin_count, "pin_count", 1, 16);
714
- if ("error" in pinCount) {
715
- return pinCount;
716
- }
717
- const sampleRate = optionalNumber(body.sample_rate, "sample_rate", 1, 125_000_000);
718
- if ("error" in sampleRate) {
719
- return sampleRate;
720
- }
721
- const samples = optionalNumber(body.samples, "samples", 1, 1_048_576);
722
- if ("error" in samples) {
723
- return samples;
724
- }
725
- const waitTimeout = optionalNumber(body.wait_timeout_ms, "wait_timeout_ms", 1000, 120_000);
726
- if ("error" in waitTimeout) {
727
- return waitTimeout;
728
- }
729
- const readTimeout = optionalNumber(body.read_timeout_ms, "read_timeout_ms", 1000, 120_000);
730
- if ("error" in readTimeout) {
731
- return readTimeout;
732
- }
733
- const preSamples = optionalNumber(body.pre_samples, "pre_samples", 0, 1_048_576);
734
- if ("error" in preSamples) {
735
- return preSamples;
736
- }
737
- const postSamples = optionalNumber(body.post_samples, "post_samples", 0, 1_048_576);
738
- if ("error" in postSamples) {
739
- return postSamples;
740
- }
741
- const searchSamples = optionalNumber(body.search_samples, "search_samples", 0, 1_048_576);
742
- if ("error" in searchSamples) {
743
- return searchSamples;
744
- }
745
- const burstCount = optionalNumber(body.burst_count, "burst_count", 1, 32);
746
- if ("error" in burstCount) {
747
- return burstCount;
748
- }
749
- const triggerPin = optionalNumber(body.trigger_pin, "trigger_pin", 0, 47);
750
- if ("error" in triggerPin) {
751
- return triggerPin;
752
- }
753
- const triggerMask = optionalNumber(body.trigger_mask, "trigger_mask", 0, 0xffff_ffff);
754
- if ("error" in triggerMask) {
755
- return triggerMask;
756
- }
757
- const triggerValue = optionalNumber(body.trigger_value, "trigger_value", 0, 0xffff_ffff);
758
- if ("error" in triggerValue) {
759
- return triggerValue;
760
- }
761
- const pull = optionalEnum(body.pull, "pull", ["none", "up", "down"]);
762
- if ("error" in pull) {
763
- return pull;
764
- }
765
- const triggerMode = optionalEnum(body.trigger_mode, "trigger_mode", ["level", "rising", "falling", "pattern"]);
766
- if ("error" in triggerMode) {
767
- return triggerMode;
768
- }
769
- const channelNames = optionalStringMap(body.channel_names, "channel_names");
770
- if ("error" in channelNames) {
771
- return channelNames;
772
- }
773
- const pinPulls = optionalPullMap(body.pin_pulls, "pin_pulls");
774
- if ("error" in pinPulls) {
775
- return pinPulls;
776
- }
777
- if (body.release !== undefined && typeof body.release !== "boolean") {
778
- return { error: "release must be a boolean when provided." };
779
- }
780
- if (body.trigger_level !== undefined && typeof body.trigger_level !== "boolean") {
781
- return { error: "trigger_level must be a boolean when provided." };
782
- }
783
- return {
784
- request: {
785
- ...base.request,
786
- pin_base: pinBase.value,
787
- pin_count: pinCount.value,
788
- sample_rate: sampleRate.value,
789
- samples: samples.value,
790
- output_path: optionalString(body.output_path),
791
- pull: pull.value,
792
- release: typeof body.release === "boolean" ? body.release : undefined,
793
- wait_timeout_ms: waitTimeout.value,
794
- read_timeout_ms: readTimeout.value,
795
- pre_samples: preSamples.value,
796
- post_samples: postSamples.value,
797
- search_samples: searchSamples.value,
798
- burst_count: burstCount.value,
799
- trigger_pin: triggerPin.value,
800
- trigger_mode: triggerMode.value,
801
- trigger_level: typeof body.trigger_level === "boolean" ? body.trigger_level : undefined,
802
- trigger_mask: triggerMask.value,
803
- trigger_value: triggerValue.value,
804
- channel_names: channelNames.value,
805
- pin_pulls: pinPulls.value
806
- }
807
- };
808
- }
809
- function parseRp2350MonitorLogicDecodeInput(input) {
810
- if (!input || typeof input !== "object" || Array.isArray(input)) {
811
- return { error: "input must be a JSON object." };
812
- }
813
- const body = input;
814
- const unknownFields = Object.keys(body).filter((field) => !RP2350_MONITOR_LOGIC_DECODE_FIELDS.has(field));
815
- if (unknownFields.length > 0) {
816
- return { error: `Unsupported RP2350 Monitor logic decode field(s): ${unknownFields.join(", ")}.` };
817
- }
818
- const inputPath = optionalString(body.input_path);
819
- if (!inputPath) {
820
- return { error: "input_path is required." };
821
- }
822
- const decoder = optionalEnum(body.decoder, "decoder", ["summary", "bursts", "edges", "uart", "spi", "i2c"]);
823
- if ("error" in decoder) {
824
- return decoder;
825
- }
826
- const pins = optionalNumberArray(body.pins, "pins", 0, 47);
827
- if ("error" in pins) {
828
- return pins;
829
- }
830
- const captureId = optionalNumber(body.capture_id, "capture_id", 0, 1_000_000);
831
- if ("error" in captureId) {
832
- return captureId;
833
- }
834
- const startSample = optionalNumber(body.start_sample, "start_sample", 0, 1_000_000_000);
835
- if ("error" in startSample) {
836
- return startSample;
837
- }
838
- const endSample = optionalNumber(body.end_sample, "end_sample", 0, 1_000_000_000);
839
- if ("error" in endSample) {
840
- return endSample;
841
- }
842
- const rxPin = optionalNumber(body.rx_pin, "rx_pin", 0, 47);
843
- if ("error" in rxPin) {
844
- return rxPin;
845
- }
846
- const baud = optionalNumber(body.baud, "baud", 1, 20_000_000);
847
- if ("error" in baud) {
848
- return baud;
849
- }
850
- const dataBits = optionalNumber(body.data_bits, "data_bits", 5, 9);
851
- if ("error" in dataBits) {
852
- return dataBits;
853
- }
854
- const parity = optionalEnum(body.parity, "parity", ["none", "even", "odd"]);
855
- if ("error" in parity) {
856
- return parity;
857
- }
858
- const sckPin = optionalNumber(body.sck_pin, "sck_pin", 0, 47);
859
- if ("error" in sckPin) {
860
- return sckPin;
861
- }
862
- const mosiPin = optionalNumber(body.mosi_pin, "mosi_pin", 0, 47);
863
- if ("error" in mosiPin) {
864
- return mosiPin;
865
- }
866
- const misoPin = optionalNumber(body.miso_pin, "miso_pin", 0, 47);
867
- if ("error" in misoPin) {
868
- return misoPin;
869
- }
870
- const csPin = optionalNumber(body.cs_pin, "cs_pin", 0, 47);
871
- if ("error" in csPin) {
872
- return csPin;
873
- }
874
- const mode = optionalNumber(body.mode, "mode", 0, 3);
875
- if ("error" in mode) {
876
- return mode;
877
- }
878
- const wordBits = optionalNumber(body.word_bits, "word_bits", 1, 32);
879
- if ("error" in wordBits) {
880
- return wordBits;
881
- }
882
- const bitOrder = optionalEnum(body.bit_order, "bit_order", ["msb", "lsb"]);
883
- if ("error" in bitOrder) {
884
- return bitOrder;
885
- }
886
- const sdaPin = optionalNumber(body.sda_pin, "sda_pin", 0, 47);
887
- if ("error" in sdaPin) {
888
- return sdaPin;
889
- }
890
- const sclPin = optionalNumber(body.scl_pin, "scl_pin", 0, 47);
891
- if ("error" in sclPin) {
892
- return sclPin;
893
- }
894
- if (body.stop_bits !== undefined && (typeof body.stop_bits !== "number" || ![1, 1.5, 2].includes(body.stop_bits))) {
895
- return { error: "stop_bits must be 1, 1.5, or 2 when provided." };
896
- }
897
- if (body.invert !== undefined && typeof body.invert !== "boolean") {
898
- return { error: "invert must be a boolean when provided." };
899
- }
900
- if (body.cs_active_high !== undefined && typeof body.cs_active_high !== "boolean") {
901
- return { error: "cs_active_high must be a boolean when provided." };
902
- }
903
- return {
904
- request: {
905
- input_path: inputPath,
906
- decoder: decoder.value,
907
- output_path: optionalString(body.output_path),
908
- capture_id: captureId.value,
909
- start_sample: startSample.value,
910
- end_sample: endSample.value,
911
- pins: pins.value,
912
- rx_pin: rxPin.value,
913
- baud: baud.value,
914
- data_bits: dataBits.value,
915
- parity: parity.value,
916
- stop_bits: typeof body.stop_bits === "number" ? body.stop_bits : undefined,
917
- invert: typeof body.invert === "boolean" ? body.invert : undefined,
918
- sck_pin: sckPin.value,
919
- mosi_pin: mosiPin.value,
920
- miso_pin: misoPin.value,
921
- cs_pin: csPin.value,
922
- mode: mode.value,
923
- cs_active_high: typeof body.cs_active_high === "boolean" ? body.cs_active_high : undefined,
924
- word_bits: wordBits.value,
925
- bit_order: bitOrder.value,
926
- sda_pin: sdaPin.value,
927
- scl_pin: sclPin.value,
928
- monitor_root: optionalString(body.monitor_root)
929
- }
930
- };
931
- }
932
- function parseRp2350MonitorBaseInput(input, allowedFields) {
933
- if (!input || typeof input !== "object" || Array.isArray(input)) {
934
- return { error: "input must be a JSON object." };
935
- }
936
- const body = input;
937
- const unknownFields = Object.keys(body).filter((field) => !allowedFields.has(field));
938
- if (unknownFields.length > 0) {
939
- return { error: `Unsupported RP2350 Monitor field(s): ${unknownFields.join(", ")}.` };
940
- }
941
- const transport = optionalEnum(body.transport, "transport", ["auto", "serial", "tcp"]);
942
- if ("error" in transport) {
943
- return transport;
944
- }
945
- const host = optionalString(body.host);
946
- if (body.host !== undefined && (!host || !isValidProbeHost(host))) {
947
- return { error: "host must be a hostname or IP address without schemes, paths, spaces, or credentials." };
948
- }
949
- const port = optionalNumber(body.port, "port", 1, 65_535);
950
- if ("error" in port) {
951
- return port;
952
- }
953
- const baud = optionalNumber(body.baud, "baud", 300, 4_000_000);
954
- if ("error" in baud) {
955
- return baud;
956
- }
957
- const timeoutMs = optionalNumber(body.timeout_ms, "timeout_ms", 100, 120_000);
958
- if ("error" in timeoutMs) {
959
- return timeoutMs;
960
- }
961
- const serialPath = optionalString(body.serial_path);
962
- if (body.serial_path !== undefined && !serialPath) {
963
- return { error: "serial_path must be a non-empty string when provided." };
964
- }
965
- if (transport.value === "serial" && !serialPath) {
966
- return { error: "serial_path is required when transport=serial." };
967
- }
968
- return {
969
- request: {
970
- transport: transport.value,
971
- serial_path: serialPath,
972
- host,
973
- port: port.value,
974
- baud: baud.value,
975
- timeout_ms: timeoutMs.value,
976
- monitor_root: optionalString(body.monitor_root)
977
- }
978
- };
979
- }
980
- function parseToolInvokeRequest(input) {
981
- if (!input || typeof input !== "object" || Array.isArray(input)) {
982
- return fail("invalid_request", "Request body must be a JSON object.");
983
- }
984
- const body = input;
985
- const unknownFields = Object.keys(body).filter((field) => !["capability_id", "input", "approved"].includes(field));
986
- if (unknownFields.length > 0) {
987
- return fail("invalid_request", `Unsupported field(s): ${unknownFields.join(", ")}.`);
988
- }
989
- if (typeof body.capability_id !== "string" || !body.capability_id.trim()) {
990
- return fail("invalid_request", "capability_id is required.");
991
- }
992
- if (body.input !== undefined && (!body.input || typeof body.input !== "object" || Array.isArray(body.input))) {
993
- return fail("invalid_request", "input must be a JSON object when provided.");
994
- }
995
- if (body.approved !== undefined && typeof body.approved !== "boolean") {
996
- return fail("invalid_request", "approved must be a boolean when provided.");
997
- }
998
- return ok({
999
- capability_id: body.capability_id.trim(),
1000
- input: body.input,
1001
- approved: body.approved
1002
- });
1003
- }
1004
- function parseDeviceProbeInput(input) {
1005
- if (!input || typeof input !== "object" || Array.isArray(input)) {
1006
- return { error: "input must be a JSON object." };
1007
- }
1008
- const request = input;
1009
- const unknownFields = Object.keys(request).filter((field) => !PROBE_REQUEST_FIELDS.has(field));
1010
- if (unknownFields.length > 0) {
1011
- return { error: `Unsupported field(s): ${unknownFields.join(", ")}.` };
1012
- }
1013
- let host;
1014
- if (request.host !== undefined) {
1015
- if (typeof request.host !== "string") {
1016
- return { error: "host must be a hostname or IP address string." };
1017
- }
1018
- host = request.host.trim();
1019
- if (!isValidProbeHost(host)) {
1020
- return { error: "host must be a hostname or IP address without schemes, paths, spaces, or credentials." };
1021
- }
1022
- }
1023
- let ports;
1024
- if (request.ports !== undefined) {
1025
- if (!Array.isArray(request.ports)) {
1026
- return { error: "ports must be an array." };
1027
- }
1028
- const invalidPort = request.ports.find((port) => typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65_535);
1029
- if (invalidPort !== undefined) {
1030
- return { error: `Invalid TCP port: ${String(invalidPort)}.` };
1031
- }
1032
- ports = [...request.ports];
1033
- }
1034
- let serialPaths;
1035
- if (request.serial_paths !== undefined) {
1036
- if (!Array.isArray(request.serial_paths)) {
1037
- return { error: "serial_paths must be an array." };
1038
- }
1039
- const invalidPath = request.serial_paths.find((path) => typeof path !== "string" || !path.trim());
1040
- if (invalidPath !== undefined) {
1041
- return { error: "serial_paths must contain non-empty strings." };
1042
- }
1043
- serialPaths = request.serial_paths.map((path) => path.trim());
1044
- }
1045
- if (request.timeout_ms !== undefined && (typeof request.timeout_ms !== "number" || !Number.isFinite(request.timeout_ms) || request.timeout_ms <= 0)) {
1046
- return { error: "timeout_ms must be a positive number." };
1047
- }
1048
- const hasPorts = (ports?.length ?? 0) > 0;
1049
- const hasSerialPaths = (serialPaths?.length ?? 0) > 0;
1050
- if (!hasPorts && !hasSerialPaths) {
1051
- return { error: "At least one TCP port or serial path is required." };
1052
- }
1053
- if (hasPorts && !host) {
1054
- return { error: "host is required when ports are provided." };
1055
- }
1056
- return {
1057
- request: {
1058
- host,
1059
- ports,
1060
- serial_paths: serialPaths,
1061
- timeout_ms: typeof request.timeout_ms === "number" ? request.timeout_ms : undefined
1062
- }
1063
- };
1064
- }
1065
- function parseTaishanPiLiveInput(input, allowed = {}) {
1066
- if (!input || typeof input !== "object" || Array.isArray(input)) {
1067
- return { error: "input must be a JSON object." };
1068
- }
1069
- const body = input;
1070
- const allowedFields = new Set(["board_id", "variant_id", "host", "user", "timeout_seconds"]);
1071
- if (allowed.interface) {
1072
- allowedFields.add("interface");
1073
- }
1074
- if (allowed.duration_seconds) {
1075
- allowedFields.add("duration_seconds");
1076
- }
1077
- if (allowed.port) {
1078
- allowedFields.add("port");
1079
- }
1080
- if (allowed.force_restart) {
1081
- allowedFields.add("force_restart");
1082
- }
1083
- const unknownFields = Object.keys(body).filter((field) => !TAISHANPI_LIVE_FIELDS.has(field) || !allowedFields.has(field));
1084
- if (unknownFields.length > 0) {
1085
- return { error: `Unsupported TaishanPi live-probe field(s): ${unknownFields.join(", ")}.` };
1086
- }
1087
- if (body.board_id !== undefined && body.board_id !== "taishanpi") {
1088
- return { error: "board_id must be taishanpi when provided." };
1089
- }
1090
- if (body.variant_id !== undefined && (typeof body.variant_id !== "string" || !body.variant_id.trim())) {
1091
- return { error: "variant_id must be a non-empty string when provided." };
1092
- }
1093
- const host = optionalString(body.host);
1094
- if (body.host !== undefined && (!host || !isValidProbeHost(host))) {
1095
- return { error: "host must be a hostname or IP address without schemes, paths, spaces, or credentials." };
1096
- }
1097
- const user = optionalString(body.user);
1098
- if (body.user !== undefined && (!user || !/^[A-Za-z0-9._-]{1,64}$/.test(user))) {
1099
- return { error: "user must contain only letters, numbers, dots, underscores, or hyphens." };
1100
- }
1101
- const timeoutSeconds = optionalNumber(body.timeout_seconds, "timeout_seconds", 3, 60);
1102
- if ("error" in timeoutSeconds) {
1103
- return timeoutSeconds;
1104
- }
1105
- const liveInterface = optionalString(body.interface);
1106
- if (body.interface !== undefined && (!liveInterface || !/^[A-Za-z0-9_.:-]{1,64}$/.test(liveInterface))) {
1107
- return { error: "interface must be a safe network interface name." };
1108
- }
1109
- const durationSeconds = optionalNumber(body.duration_seconds, "duration_seconds", 3, 30);
1110
- if ("error" in durationSeconds) {
1111
- return durationSeconds;
1112
- }
1113
- const port = optionalNumber(body.port, "port", 1, 65_535);
1114
- if ("error" in port) {
1115
- return port;
1116
- }
1117
- if (body.force_restart !== undefined && typeof body.force_restart !== "boolean") {
1118
- return { error: "force_restart must be a boolean when provided." };
1119
- }
1120
- return {
1121
- request: {
1122
- board_id: "taishanpi",
1123
- variant_id: optionalString(body.variant_id),
1124
- host,
1125
- user,
1126
- timeout_seconds: timeoutSeconds.value,
1127
- interface: liveInterface,
1128
- duration_seconds: durationSeconds.value,
1129
- port: port.value,
1130
- force_restart: typeof body.force_restart === "boolean" ? body.force_restart : undefined
1131
- }
1132
- };
1133
- }
1134
- function parseFlashPlanInput(input) {
1135
- if (!input || typeof input !== "object" || Array.isArray(input)) {
1136
- return { error: "input must be a JSON object." };
1137
- }
1138
- const body = input;
1139
- const unknownFields = Object.keys(body).filter((field) => !FLASH_REQUEST_FIELDS.has(field));
1140
- if (unknownFields.length > 0) {
1141
- return { error: `Unsupported flash field(s): ${unknownFields.join(", ")}.` };
1142
- }
1143
- if (typeof body.board_id !== "string" || !body.board_id.trim()) {
1144
- return { error: "board_id is required." };
1145
- }
1146
- return {
1147
- request: {
1148
- board_id: body.board_id.trim(),
1149
- variant_id: optionalString(body.variant_id),
1150
- hardware_profile_id: optionalString(body.hardware_profile_id),
1151
- profile_id: optionalString(body.profile_id),
1152
- image_dir: optionalString(body.image_dir),
1153
- artifact_path: optionalString(body.artifact_path),
1154
- target_volume_path: optionalString(body.target_volume_path)
1155
- }
1156
- };
1157
- }
1158
- function requiredNumber(value, label, min, max) {
1159
- const parsed = optionalNumber(value, label, min, max);
1160
- if ("error" in parsed) {
1161
- return parsed;
1162
- }
1163
- if (parsed.value === undefined) {
1164
- return { error: `${label} is required.` };
1165
- }
1166
- return { value: parsed.value };
1167
- }
1168
- function optionalEnum(value, label, allowed) {
1169
- if (value === undefined) {
1170
- return {};
1171
- }
1172
- if (typeof value !== "string" || !allowed.includes(value)) {
1173
- return { error: `${label} must be one of ${allowed.join(", ")}.` };
1174
- }
1175
- return { value: value };
1176
- }
1177
- function optionalNumberArray(value, label, min, max) {
1178
- if (value === undefined) {
1179
- return {};
1180
- }
1181
- if (!Array.isArray(value)) {
1182
- return { error: `${label} must be an array of numbers.` };
1183
- }
1184
- const parsed = [];
1185
- for (const item of value) {
1186
- const numberValue = optionalNumber(item, label, min, max);
1187
- if ("error" in numberValue || numberValue.value === undefined) {
1188
- return { error: `${label} must contain numbers from ${min} through ${max}.` };
1189
- }
1190
- parsed.push(numberValue.value);
1191
- }
1192
- return { value: parsed };
1193
- }
1194
- function optionalStringMap(value, label) {
1195
- if (value === undefined) {
1196
- return {};
1197
- }
1198
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1199
- return { error: `${label} must be an object when provided.` };
1200
- }
1201
- const output = {};
1202
- for (const [key, item] of Object.entries(value)) {
1203
- if (!/^\d{1,2}$/.test(key) || typeof item !== "string" || !item.trim()) {
1204
- return { error: `${label} must map GPIO numbers to non-empty strings.` };
1205
- }
1206
- output[key] = item.trim();
1207
- }
1208
- return { value: output };
1209
- }
1210
- function optionalPullMap(value, label) {
1211
- if (value === undefined) {
1212
- return {};
1213
- }
1214
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1215
- return { error: `${label} must be an object when provided.` };
1216
- }
1217
- const output = {};
1218
- for (const [key, item] of Object.entries(value)) {
1219
- if (!/^\d{1,2}$/.test(key) || typeof item !== "string" || !["none", "up", "down"].includes(item)) {
1220
- return { error: `${label} must map GPIO numbers to none, up, or down.` };
1221
- }
1222
- output[key] = item;
1223
- }
1224
- return { value: output };
1225
- }
1226
- function optionalString(value) {
1227
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
1228
- }
1229
- function optionalNumber(value, label, min, max) {
1230
- if (value === undefined) {
1231
- return {};
1232
- }
1233
- if (typeof value !== "number" || !Number.isFinite(value)) {
1234
- return { error: `${label} must be a number when provided.` };
1235
- }
1236
- const normalized = Math.trunc(value);
1237
- if (normalized < min || normalized > max) {
1238
- return { error: `${label} must be from ${min} through ${max}.` };
1239
- }
1240
- return { value: normalized };
1241
- }
1242
- function isValidProbeHost(host) {
1243
- if (!host || host.length > 253 || /[\s\x00-\x1f\x7f]/.test(host)) {
1244
- return false;
1245
- }
1246
- if (host.includes("://") || host.includes("/") || host.includes("\\") || host.includes("@")) {
1247
- return false;
1248
- }
1249
- if (net.isIP(host) !== 0) {
1250
- return true;
1251
- }
1252
- return host.split(".").every((label) => /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/.test(label));
1253
- }
1254
- //# sourceMappingURL=tools.js.map