@easynet-run/node 0.27.14 → 0.39.29

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 (38) hide show
  1. package/README.md +39 -1
  2. package/native/dendrite-bridge-manifest.json +5 -4
  3. package/native/dendrite-bridge.json +1 -1
  4. package/native/include/axon_dendrite_bridge.h +18 -4
  5. package/native/libaxon_dendrite_bridge.so +0 -0
  6. package/package.json +9 -5
  7. package/runtime/easynet-runtime-rs-0.39.29-x86_64-unknown-linux-gnu.tar.gz +0 -0
  8. package/runtime/runtime-bridge-manifest.json +4 -4
  9. package/runtime/runtime-bridge.json +3 -3
  10. package/src/ability_lifecycle.d.ts +12 -1
  11. package/src/ability_lifecycle.js +117 -31
  12. package/src/capability_request.js +3 -1
  13. package/src/dendrite_bridge/bridge.d.ts +10 -2
  14. package/src/dendrite_bridge/bridge.js +75 -14
  15. package/src/dendrite_bridge/ffi.d.ts +4 -0
  16. package/src/dendrite_bridge/ffi.js +194 -18
  17. package/src/dendrite_bridge/types.d.ts +4 -0
  18. package/src/errors.js +9 -3
  19. package/src/index.d.ts +3 -3
  20. package/src/index.js +9 -10
  21. package/src/mcp/server.d.ts +24 -2
  22. package/src/mcp/server.js +218 -18
  23. package/src/mcp/server.test.js +100 -0
  24. package/src/presets/ability_dispatch/workflow.js +8 -30
  25. package/src/presets/remote_control/config.d.ts +3 -0
  26. package/src/presets/remote_control/config.js +22 -24
  27. package/src/presets/remote_control/descriptor.d.ts +36 -0
  28. package/src/presets/remote_control/descriptor.js +267 -11
  29. package/src/presets/remote_control/handlers.d.ts +8 -0
  30. package/src/presets/remote_control/handlers.js +230 -26
  31. package/src/presets/remote_control/kit.d.ts +4 -2
  32. package/src/presets/remote_control/kit.js +106 -1
  33. package/src/presets/remote_control/kit.test.js +994 -0
  34. package/src/presets/remote_control/orchestrator.d.ts +6 -0
  35. package/src/presets/remote_control/orchestrator.js +36 -1
  36. package/src/presets/remote_control/specs.js +217 -61
  37. package/src/receipt.js +6 -3
  38. package/runtime/easynet-runtime-rs-0.27.14-x86_64-unknown-linux-gnu.tar.gz +0 -0
@@ -1,24 +1,44 @@
1
- import { DEFAULT_VERSION, asString } from "./config.js";
1
+ // EasyNet Axon for AgentNet
2
+ // =========================
3
+ //
4
+ // File: sdk/node/src/presets/remote_control/descriptor.ts
5
+ // Description: Node remote-control ability package descriptor builders and parsers for deploy and package flows.
6
+ //
7
+ // Protocol Responsibility:
8
+ // - Encodes and decodes portable ability package payloads used by remote-control deploy/package tool flows.
9
+ // - Normalizes package, capability, tool, metadata, and signature fields before bridge/orchestrator calls.
10
+ //
11
+ // Implementation Approach:
12
+ // - Keeps parsing, sanitization, and default generation close to descriptor construction helpers.
13
+ // - Uses deterministic identifier/version/tag normalization so packaged abilities round-trip across SDKs.
14
+ //
15
+ // Usage Contract:
16
+ // - Callers must pass descriptor inputs that can be losslessly represented as JSON-compatible values.
17
+ // - Update descriptor rules here before changing remote-control packaging semantics elsewhere.
18
+ //
19
+ // Architectural Position:
20
+ // - Shared descriptor translation layer for the remote-control preset and ability-lifecycle integrations.
21
+ //
22
+ // Author: Silan.Hu
23
+ // Email: silan.hu@u.nus.edu
24
+ // Copyright (c) 2026-2027 easynet. All rights reserved.
25
+ import { DEFAULT_VERSION, asBool, asString } from "./config.js";
2
26
  export function resolveTenant(raw, fallback) {
3
27
  const value = asString(raw);
4
28
  return !value || value === "<nil>" ? fallback : value;
5
29
  }
6
30
  export function parseBool(raw) {
7
- if (typeof raw === "boolean") {
8
- return raw;
9
- }
10
- if (typeof raw === "number") {
11
- return raw !== 0;
12
- }
13
- const normalized = asString(raw).toLowerCase();
14
- return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on";
31
+ return asBool(raw);
15
32
  }
16
33
  export function randomSuffix(length = 2) {
17
34
  return Math.floor(Math.random() * 1e18).toString(36).slice(0, length);
18
35
  }
19
36
  export function sanitizeId(raw) {
20
37
  const token = asString(raw).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
21
- return token || "tool";
38
+ if (!token) {
39
+ throw new Error(`identifier contains no valid characters: ${JSON.stringify(raw)}`);
40
+ }
41
+ return token;
22
42
  }
23
43
  export function shellSingleQuote(raw) {
24
44
  return `'${raw.replace(/'/g, "'\"'\"'")}'`;
@@ -67,6 +87,21 @@ export function defaultOutputSchema() {
67
87
  required: ["entries"],
68
88
  };
69
89
  }
90
+ function mergeAdditionalMetadata(base, raw) {
91
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
92
+ return base;
93
+ }
94
+ const merged = { ...base };
95
+ for (const [rawKey, rawValue] of Object.entries(raw)) {
96
+ const key = asString(rawKey);
97
+ const value = asString(rawValue);
98
+ if (!key || !value || Object.prototype.hasOwnProperty.call(merged, key)) {
99
+ continue;
100
+ }
101
+ merged[key] = value;
102
+ }
103
+ return merged;
104
+ }
70
105
  export function serializeDescriptor(descriptor) {
71
106
  const payload = {
72
107
  "ability_name": descriptor.abilityName,
@@ -125,6 +160,16 @@ export function parseDescriptor(raw) {
125
160
  digest: asString(input.digest) || undefined,
126
161
  };
127
162
  }
163
+ /**
164
+ * Build an `AbilityPackageDescriptor` from handler arguments.
165
+ *
166
+ * Normalises names, generates a package ID, encodes the command template
167
+ * into Base64 package bytes, and serialises agent extension properties
168
+ * (instructions, input_examples, prerequisites, context_bindings, category)
169
+ * into the metadata map under the `mcp.*` prefix.
170
+ *
171
+ * @throws {Error} If `ability_name`, `command_template`, or `signature_base64` is missing.
172
+ */
128
173
  export function buildDescriptor(args, defaultSignature) {
129
174
  const abilityName = requireString(args, "ability_name");
130
175
  const commandTemplate = requireString(args, "command_template");
@@ -145,7 +190,7 @@ export function buildDescriptor(args, defaultSignature) {
145
190
  const outputSchema = typeof args.output_schema === "object" && !Array.isArray(args.output_schema)
146
191
  ? args.output_schema
147
192
  : defaultOutputSchema();
148
- const metadata = {
193
+ const baseMeta = {
149
194
  "mcp.tool_name": toolName,
150
195
  "mcp.description": description,
151
196
  "mcp.input_schema": JSON.stringify(inputSchema),
@@ -154,6 +199,25 @@ export function buildDescriptor(args, defaultSignature) {
154
199
  "ability.name": abilityName,
155
200
  "ability.version": version,
156
201
  };
202
+ const instructions = asString(args.instructions);
203
+ if (instructions)
204
+ baseMeta["mcp.instructions"] = instructions;
205
+ if (Array.isArray(args.input_examples) && args.input_examples.length > 0) {
206
+ baseMeta["mcp.input_examples"] = JSON.stringify(args.input_examples);
207
+ }
208
+ if (Array.isArray(args.prerequisites) && args.prerequisites.length > 0) {
209
+ const prerequisites = args.prerequisites.map((v) => asString(v)).filter(Boolean);
210
+ if (prerequisites.length > 0) {
211
+ baseMeta["mcp.prerequisites"] = JSON.stringify(prerequisites);
212
+ }
213
+ }
214
+ if (args.context_bindings && typeof args.context_bindings === "object" && !Array.isArray(args.context_bindings) && Object.keys(args.context_bindings).length > 0) {
215
+ baseMeta["mcp.context_bindings"] = JSON.stringify(args.context_bindings);
216
+ }
217
+ const category = asString(args.category);
218
+ if (category)
219
+ baseMeta["mcp.category"] = category;
220
+ const metadata = mergeAdditionalMetadata(baseMeta, args.metadata);
157
221
  const payload = {
158
222
  kind: "axon.ability.package.v1",
159
223
  ability_name: abilityName,
@@ -181,3 +245,195 @@ export function buildDescriptor(args, defaultSignature) {
181
245
  digest: asString(args.digest) || undefined,
182
246
  };
183
247
  }
248
+ // ---------------------------------------------------------------------------
249
+ // createAbilityDescriptor — build an AbilityDescriptorLite from MCP args
250
+ // ---------------------------------------------------------------------------
251
+ export function createAbilityDescriptor(args) {
252
+ const name = asString(args.name);
253
+ if (!name)
254
+ throw new Error("name is required");
255
+ const commandTemplate = asString(args.command_template);
256
+ if (!commandTemplate)
257
+ throw new Error("command_template is required");
258
+ const token = sanitizeId(name).replace(/-/g, "_");
259
+ const toolName = `ability_${token}`;
260
+ const inputExamples = Array.isArray(args.input_examples)
261
+ ? args.input_examples
262
+ : [];
263
+ const prerequisites = Array.isArray(args.prerequisites)
264
+ ? args.prerequisites.map((v) => asString(v)).filter(Boolean)
265
+ : [];
266
+ const contextBindings = args.context_bindings && typeof args.context_bindings === "object" && !Array.isArray(args.context_bindings)
267
+ ? Object.fromEntries(Object.entries(args.context_bindings).map(([k, v]) => [k, asString(v)]))
268
+ : {};
269
+ return {
270
+ name,
271
+ toolName,
272
+ description: asString(args.description) || `Ability ${name}`,
273
+ commandTemplate,
274
+ inputSchema: typeof args.input_schema === "object" && !Array.isArray(args.input_schema)
275
+ ? args.input_schema
276
+ : { type: "object", properties: {} },
277
+ outputSchema: typeof args.output_schema === "object" && !Array.isArray(args.output_schema)
278
+ ? args.output_schema
279
+ : { type: "object", properties: {} },
280
+ version: asString(args.version) || "1.0.0",
281
+ tags: asStringArray(args.tags),
282
+ resourceUri: asString(args.resource_uri) || `easynet:///r/org/${token}`,
283
+ instructions: asString(args.instructions),
284
+ inputExamples,
285
+ prerequisites,
286
+ contextBindings,
287
+ category: asString(args.category),
288
+ };
289
+ }
290
+ // ---------------------------------------------------------------------------
291
+ // exportAbilitySkill — generate SKILL.md and invoke.sh from a descriptor
292
+ // ---------------------------------------------------------------------------
293
+ const DEFAULT_AXON_PORT = 50051;
294
+ const SHELL_UNSAFE_RE = /[`$\\\"'\n\r;|&<>(){}!#\x00-\x1f]/;
295
+ function sanitizeShellValue(value, label) {
296
+ if (SHELL_UNSAFE_RE.test(value)) {
297
+ throw new Error(`${label} contains disallowed shell characters: ${value}`);
298
+ }
299
+ return value;
300
+ }
301
+ function generateExportInvokeScript(resourceUri, endpoint) {
302
+ const safeEndpoint = sanitizeShellValue(endpoint, "endpoint");
303
+ const safeUri = sanitizeShellValue(resourceUri, "resource_uri");
304
+ return `#!/usr/bin/env bash
305
+ set -euo pipefail
306
+ AXON_ENDPOINT="\${AXON_ENDPOINT:-${safeEndpoint}}"
307
+ TENANT="\${AXON_TENANT:-default}"
308
+ RESOURCE_URI="${safeUri}"
309
+ ARGS="\${1:-{}}"
310
+ curl -sS -X POST "\${AXON_ENDPOINT}/v1/invoke" \\
311
+ -H "Content-Type: application/json" \\
312
+ -d "{\\"tenant_id\\":\\"\${TENANT}\\",\\"resource_uri\\":\\"\${RESOURCE_URI}\\",\\"payload\\":\${ARGS}}"
313
+ `;
314
+ }
315
+ function pushSkillMetadata(lines, version, resourceUri) {
316
+ lines.push("metadata:");
317
+ lines.push(" author: easynet-axon");
318
+ lines.push(` version: "${version}"`);
319
+ lines.push(` axon-resource-uri: "${resourceUri}"`);
320
+ }
321
+ function generateExportAbilityMd(descriptor, target, token) {
322
+ const lines = [];
323
+ lines.push("---");
324
+ lines.push(`name: ${token}`);
325
+ lines.push(`description: ${descriptor.description}`);
326
+ lines.push("compatibility: Requires network access to Axon runtime");
327
+ pushSkillMetadata(lines, descriptor.version, descriptor.resourceUri);
328
+ if (target === "claude") {
329
+ lines.push("allowed-tools: Bash(*)");
330
+ }
331
+ else if (target === "openclaw") {
332
+ lines.push(" openclaw:");
333
+ lines.push(' emoji: "⚡"');
334
+ lines.push(" requires:");
335
+ lines.push(" network: true");
336
+ lines.push(" command-dispatch: tool");
337
+ }
338
+ lines.push("---");
339
+ lines.push("");
340
+ lines.push(`# ${descriptor.name}`);
341
+ lines.push("");
342
+ lines.push(descriptor.description);
343
+ lines.push("");
344
+ // Instructions
345
+ if (descriptor.instructions) {
346
+ lines.push(descriptor.instructions);
347
+ lines.push("");
348
+ }
349
+ // Prerequisites
350
+ if (descriptor.prerequisites.length > 0) {
351
+ lines.push("## Prerequisites");
352
+ lines.push("");
353
+ for (const prereq of descriptor.prerequisites) {
354
+ lines.push(`- ${prereq}`);
355
+ }
356
+ lines.push("");
357
+ }
358
+ lines.push("## Parameters");
359
+ lines.push("");
360
+ lines.push("| Name | Type | Required | Description |");
361
+ lines.push("|------|------|----------|-------------|");
362
+ const props = (descriptor.inputSchema.properties ?? {});
363
+ const required = (descriptor.inputSchema.required ?? []);
364
+ for (const [propName, schema] of Object.entries(props)) {
365
+ const propType = schema.type ?? "string";
366
+ const propDesc = schema.description ?? "";
367
+ const isRequired = required.includes(propName) ? "Yes" : "No";
368
+ lines.push(`| ${propName} | ${propType} | ${isRequired} | ${propDesc} |`);
369
+ }
370
+ // Examples
371
+ if (descriptor.inputExamples.length > 0) {
372
+ lines.push("");
373
+ lines.push("## Examples");
374
+ lines.push("");
375
+ for (let i = 0; i < descriptor.inputExamples.length; i++) {
376
+ lines.push(`**Example ${i + 1}:**`);
377
+ lines.push("");
378
+ lines.push("```json");
379
+ const example = descriptor.inputExamples[i];
380
+ if (typeof example === "string") {
381
+ lines.push(example);
382
+ }
383
+ else {
384
+ lines.push(JSON.stringify(example, null, 2));
385
+ }
386
+ lines.push("```");
387
+ lines.push("");
388
+ }
389
+ }
390
+ // Context Bindings
391
+ const bindingEntries = Object.entries(descriptor.contextBindings);
392
+ if (bindingEntries.length > 0) {
393
+ lines.push("## Context Bindings");
394
+ lines.push("");
395
+ lines.push("| Key | Value |");
396
+ lines.push("|-----|-------|");
397
+ for (const [key, value] of bindingEntries) {
398
+ lines.push(`| \`${key}\` | ${value} |`);
399
+ }
400
+ lines.push("");
401
+ }
402
+ lines.push("");
403
+ lines.push("## Invoke");
404
+ lines.push("");
405
+ lines.push("Run the bundled script with a JSON argument:");
406
+ lines.push("");
407
+ const skillDirVar = target === "claude" ? "CLAUDE_SKILL_DIR"
408
+ : target === "codex" ? "CODEX_SKILL_DIR"
409
+ : "SKILL_DIR";
410
+ lines.push("```bash");
411
+ lines.push(`\${${skillDirVar}}/scripts/invoke.sh '{"param": "value"}'`);
412
+ lines.push("```");
413
+ lines.push("");
414
+ // Axon Resource footer
415
+ lines.push("## Axon Resource");
416
+ lines.push("");
417
+ lines.push(`- **URI**: \`${descriptor.resourceUri}\``);
418
+ lines.push(`- **Version**: ${descriptor.version}`);
419
+ if (descriptor.category) {
420
+ lines.push(`- **Category**: ${descriptor.category}`);
421
+ }
422
+ lines.push("");
423
+ return lines.join("\n");
424
+ }
425
+ function parseAbilityTarget(raw) {
426
+ const normalized = raw.trim().toLowerCase();
427
+ if (normalized === "claude" || normalized === "codex" || normalized === "openclaw" || normalized === "agent_skills") {
428
+ return normalized;
429
+ }
430
+ return "agent_skills";
431
+ }
432
+ export function exportAbilitySkill(descriptor, target = "agent_skills", axonEndpoint) {
433
+ const token = descriptor.toolName;
434
+ const endpoint = axonEndpoint ?? `http://127.0.0.1:${DEFAULT_AXON_PORT}`;
435
+ const invokeScript = generateExportInvokeScript(descriptor.resourceUri, endpoint);
436
+ const abilityMd = generateExportAbilityMd(descriptor, target, token);
437
+ return { abilityMd, invokeScript, abilityName: token };
438
+ }
439
+ export { parseAbilityTarget };
@@ -4,9 +4,17 @@ export declare function resolveDescriptor(args: JsonRecord, fallbackSignature: s
4
4
  export declare function handleDiscoverNodes(orch: RemoteOrchestrator, tenant: string): JsonRecord;
5
5
  export declare function handleListRemoteTools(orch: RemoteOrchestrator, tenant: string, args: JsonRecord): JsonRecord;
6
6
  export declare function handleCallRemoteTool(orch: RemoteOrchestrator, tenant: string, args: JsonRecord): JsonRecord;
7
+ /** Open a streaming MCP tool call with validated tool_name and node_id. */
8
+ export declare function handleCallRemoteToolStream(orch: RemoteOrchestrator, _tenant: string, args: JsonRecord): ReturnType<RemoteOrchestrator["callMcpToolStream"]>;
7
9
  export declare function handleDisconnectDevice(orch: RemoteOrchestrator, tenant: string, args: JsonRecord): JsonRecord;
8
10
  export declare function handleUninstallAbility(orch: RemoteOrchestrator, tenant: string, args: JsonRecord): JsonRecord;
9
11
  export declare function handlePackageAbility(tenant: string, args: JsonRecord, signature: string): JsonRecord;
10
12
  export declare function handleDeployAbilityPackage(orch: RemoteOrchestrator, tenant: string, args: JsonRecord, signature: string): JsonRecord;
11
13
  export declare function handleDeployAbility(orch: RemoteOrchestrator, tenant: string, args: JsonRecord, signature: string): JsonRecord;
12
14
  export declare function handleExecuteCommand(orch: RemoteOrchestrator, tenant: string, args: JsonRecord, signature: string): JsonRecord;
15
+ export declare function handleDrainDevice(orch: RemoteOrchestrator, tenant: string, args: JsonRecord): JsonRecord;
16
+ export declare function handleCreateAbility(args: JsonRecord): JsonRecord;
17
+ export declare function handleExportAbilitySkill(args: JsonRecord): JsonRecord;
18
+ export declare function handleRedeployAbility(orch: RemoteOrchestrator, tenant: string, args: JsonRecord, signature: string): JsonRecord;
19
+ export declare function handleListAbilities(orch: RemoteOrchestrator, tenant: string, args: JsonRecord): JsonRecord;
20
+ export declare function handleForgetAll(orch: RemoteOrchestrator, tenant: string, args: JsonRecord): JsonRecord;
@@ -2,13 +2,29 @@
2
2
  // =========================
3
3
  //
4
4
  // File: sdk/node/src/presets/remote_control/handlers.ts
5
- // Description: MCP tool handlers for remote device control workflows.
5
+ // Description: Node remote-control MCP tool handlers for discovery, deployment, execution, and lifecycle workflows.
6
+ //
7
+ // Protocol Responsibility:
8
+ // - Publishes MCP-facing handlers for discovery, deployment, execution, and lifecycle control operations.
9
+ // - Maps untyped tool arguments into stable tenant-scoped orchestrator requests and MCP result payloads.
10
+ //
11
+ // Implementation Approach:
12
+ // - Performs validation and response shaping at the tool boundary while delegating side effects to orchestrator helpers.
13
+ // - Reuses descriptor/config utilities so remote-control behavior stays aligned across language SDKs.
14
+ //
15
+ // Usage Contract:
16
+ // - Intended to be called from MCP dispatch with untyped argument objects and preset runtime configuration.
17
+ // - Handler error payloads are part of the remote-control tool contract and should stay explicit and machine-readable.
18
+ //
19
+ // Architectural Position:
20
+ // - Preset execution layer between MCP server facades and bridge-backed orchestration helpers.
6
21
  //
7
22
  // Author: Silan.Hu
8
23
  // Email: silan.hu@u.nus.edu
9
24
  // Copyright (c) 2026-2027 easynet. All rights reserved.
10
- import { buildDescriptor, buildPythonSubprocessTemplate, parseBool, parseDescriptor, serializeDescriptor, } from "./descriptor.js";
11
- import { asString } from "./config.js";
25
+ import { randomBytes } from "node:crypto";
26
+ import { buildDescriptor, buildPythonSubprocessTemplate, createAbilityDescriptor, exportAbilitySkill, parseAbilityTarget, parseBool, parseDescriptor, serializeDescriptor, } from "./descriptor.js";
27
+ import { asBool, asNumber, asString, DEFAULT_MCP_TOOL_STREAM_TIMEOUT_MS } from "./config.js";
12
28
  // ---------------------------------------------------------------------------
13
29
  // Architecture: ability_lifecycle vs presets/remote_control
14
30
  //
@@ -32,6 +48,18 @@ const INVOCATION_COMPLETED = 5;
32
48
  export function resolveDescriptor(args, fallbackSignature) {
33
49
  return args.package ? parseDescriptor(args.package) : buildDescriptor(args, fallbackSignature);
34
50
  }
51
+ function abilityEntryFromTool(tool) {
52
+ const installId = String(tool.install_id ?? "").trim();
53
+ if (!installId) {
54
+ return null;
55
+ }
56
+ return {
57
+ tool_name: tool.tool_name ?? null,
58
+ description: tool.description ?? null,
59
+ capability_name: tool.capability_name ?? null,
60
+ install_id: installId,
61
+ };
62
+ }
35
63
  export function handleDiscoverNodes(orch, tenant) {
36
64
  const nodes = orch.listNodes("");
37
65
  return {
@@ -55,13 +83,11 @@ export function handleListRemoteTools(orch, tenant, args) {
55
83
  count: tools.length,
56
84
  node_id: nodeId,
57
85
  name_pattern: pattern,
58
- tools: tools.map((tool) => ({
59
- tool_name: tool.tool_name,
60
- description: tool.description,
61
- capability_name: tool.capability_name,
62
- available_nodes: tool.available_nodes,
63
- node_ids: tool.node_ids,
64
- })),
86
+ // Pass through the full tool entry from the runtime — including
87
+ // input_schema, output_schema, hints, instructions, examples,
88
+ // prerequisites, context_bindings, and category. Stripping
89
+ // fields here would prevent AI agents from understanding tools.
90
+ tools: tools.map((tool) => ({ ...tool })),
65
91
  };
66
92
  }
67
93
  export function handleCallRemoteTool(orch, tenant, args) {
@@ -79,6 +105,21 @@ export function handleCallRemoteTool(orch, tenant, args) {
79
105
  const failed = asNumber(call.state) !== INVOCATION_COMPLETED || asBool(call.is_error);
80
106
  return { ok: !failed, tenant_id: tenant, tool_name: toolName, node_id: nodeId, call };
81
107
  }
108
+ /** Open a streaming MCP tool call with validated tool_name and node_id. */
109
+ export function handleCallRemoteToolStream(orch, _tenant, args) {
110
+ const toolName = String(args.tool_name || "").trim();
111
+ const nodeId = String(args.node_id || "").trim();
112
+ if (!toolName || !nodeId) {
113
+ throw new Error(!toolName ? "tool_name is required" : "node_id is required");
114
+ }
115
+ const callArgs = typeof args.arguments === "object" && !Array.isArray(args.arguments)
116
+ ? args.arguments
117
+ : {};
118
+ const timeoutMs = asNumber(args.timeout_ms);
119
+ return orch.callMcpToolStream(toolName, nodeId, callArgs, {
120
+ timeoutMs: timeoutMs > 0 ? timeoutMs : DEFAULT_MCP_TOOL_STREAM_TIMEOUT_MS,
121
+ });
122
+ }
82
123
  export function handleDisconnectDevice(orch, tenant, args) {
83
124
  const nodeId = String(args.node_id || "").trim();
84
125
  if (!nodeId) {
@@ -149,7 +190,7 @@ export function handleDeployAbility(orch, tenant, args, signature) {
149
190
  if (!commandTemplate) {
150
191
  return { ok: false, tenant_id: tenant, node_id: nodeId, error: "command_template is required" };
151
192
  }
152
- const toolName = String(args.tool_name || `tool_${Date.now()}`).trim();
193
+ const toolName = String(args.tool_name || `tool_${Date.now()}_${randomHex(4)}`).trim();
153
194
  const descriptor = buildDescriptor({
154
195
  ...args,
155
196
  ability_name: toolName,
@@ -177,7 +218,7 @@ export function handleExecuteCommand(orch, tenant, args, signature) {
177
218
  if (!command)
178
219
  return { ok: false, tenant_id: tenant, node_id: nodeId, error: "command is required" };
179
220
  const shouldCleanup = args.cleanup == null ? true : parseBool(args.cleanup);
180
- const toolName = `cmd_${Date.now()}_${Math.floor(Math.random() * 1e4).toString(16)}`;
221
+ const toolName = `cmd_${Date.now()}_${randomHex(4)}`;
181
222
  const descriptor = buildDescriptor({
182
223
  ability_name: toolName,
183
224
  tool_name: toolName,
@@ -221,20 +262,6 @@ export function handleExecuteCommand(orch, tenant, args, signature) {
221
262
  };
222
263
  }
223
264
  }
224
- function asBool(raw) {
225
- if (typeof raw === "boolean")
226
- return raw;
227
- if (typeof raw === "number")
228
- return raw !== 0;
229
- const normalized = String(raw ?? "").trim().toLowerCase();
230
- return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on";
231
- }
232
- function asNumber(raw) {
233
- if (typeof raw === "number")
234
- return raw;
235
- const value = Number.parseInt(String(raw ?? "0"), 10);
236
- return Number.isFinite(value) ? value : 0;
237
- }
238
265
  function parseDeployFailure(error) {
239
266
  const message = error instanceof Error ? error.message : String(error ?? "");
240
267
  try {
@@ -277,3 +304,180 @@ function parseCleanupSummary(failure, orch, nodeId, shouldCleanup) {
277
304
  },
278
305
  ]);
279
306
  }
307
+ // ---------------------------------------------------------------------------
308
+ // DEVICE MANAGEMENT & ABILITY LIFECYCLE HANDLERS
309
+ // Ported from Rust SDK for cross-SDK parity.
310
+ // ---------------------------------------------------------------------------
311
+ export function handleDrainDevice(orch, tenant, args) {
312
+ const nodeId = String(args.node_id || "").trim();
313
+ if (!nodeId) {
314
+ return { ok: false, tenant_id: tenant, error: "node_id is required" };
315
+ }
316
+ const reason = String(args.reason || "").trim() || "drain_device: requested by agent";
317
+ const response = orch.drainNode(nodeId, reason);
318
+ return {
319
+ ok: true,
320
+ tenant_id: tenant,
321
+ node_id: nodeId,
322
+ status: "draining",
323
+ response,
324
+ };
325
+ }
326
+ export function handleCreateAbility(args) {
327
+ try {
328
+ const descriptor = createAbilityDescriptor(args);
329
+ return { ok: true, descriptor };
330
+ }
331
+ catch (error) {
332
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
333
+ }
334
+ }
335
+ export function handleExportAbilitySkill(args) {
336
+ try {
337
+ const descriptor = createAbilityDescriptor(args);
338
+ const targetStr = String(args.target || "").trim();
339
+ const target = parseAbilityTarget(targetStr);
340
+ const axonEndpoint = String(args.axon_endpoint || "").trim() || undefined;
341
+ const result = exportAbilitySkill(descriptor, target, axonEndpoint);
342
+ return {
343
+ ok: true,
344
+ ability_name: result.abilityName,
345
+ ability_md: result.abilityMd,
346
+ invoke_script: result.invokeScript,
347
+ };
348
+ }
349
+ catch (error) {
350
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
351
+ }
352
+ }
353
+ export function handleRedeployAbility(orch, tenant, args, signature) {
354
+ const nodeId = String(args.node_id || "").trim();
355
+ if (!nodeId) {
356
+ return { ok: false, tenant_id: tenant, error: "node_id is required" };
357
+ }
358
+ const toolName = String(args.tool_name || "").trim();
359
+ if (!toolName) {
360
+ return { ok: false, tenant_id: tenant, node_id: nodeId, error: "tool_name is required" };
361
+ }
362
+ const commandTemplate = String(args.command_template || "").trim();
363
+ if (!commandTemplate) {
364
+ return { ok: false, tenant_id: tenant, node_id: nodeId, error: "command_template is required" };
365
+ }
366
+ const description = String(args.description || "").trim() || `Redeployed ability: ${toolName}`;
367
+ // Preserve all original args (including agent extension fields like
368
+ // instructions, input_examples, prerequisites, context_bindings, category)
369
+ // and override only the required fields.
370
+ const descriptor = buildDescriptor({
371
+ ...args,
372
+ ability_name: toolName,
373
+ tool_name: toolName,
374
+ description,
375
+ command_template: commandTemplate,
376
+ }, signature);
377
+ const deployed = orch.deployAbilityPackage(descriptor, nodeId, true);
378
+ return {
379
+ ok: true,
380
+ tenant_id: tenant,
381
+ node_id: nodeId,
382
+ tool_name: toolName,
383
+ description,
384
+ status: "redeployed",
385
+ install_id: asString(deployed.install_id),
386
+ };
387
+ }
388
+ export function handleListAbilities(orch, tenant, args) {
389
+ const nodeId = String(args.node_id || "").trim();
390
+ if (!nodeId) {
391
+ return { ok: false, tenant_id: tenant, error: "node_id is required" };
392
+ }
393
+ const tools = orch.listMcpTools("", [], nodeId);
394
+ const abilities = [];
395
+ for (const tool of Array.isArray(tools) ? tools : []) {
396
+ const ability = abilityEntryFromTool(tool);
397
+ if (ability) {
398
+ abilities.push(ability);
399
+ }
400
+ }
401
+ return {
402
+ ok: true,
403
+ tenant_id: tenant,
404
+ node_id: nodeId,
405
+ count: abilities.length,
406
+ abilities,
407
+ };
408
+ }
409
+ export function handleForgetAll(orch, tenant, args) {
410
+ const nodeId = String(args.node_id || "").trim();
411
+ if (!nodeId) {
412
+ return { ok: false, tenant_id: tenant, error: "node_id is required" };
413
+ }
414
+ const confirmed = parseBool(args.confirm ?? false);
415
+ const dryRun = parseBool(args.dry_run ?? false);
416
+ if (!confirmed && !dryRun) {
417
+ return {
418
+ ok: false,
419
+ tenant_id: tenant,
420
+ node_id: nodeId,
421
+ error: "forget_all requires confirm: true (destructive operation)",
422
+ };
423
+ }
424
+ // List all abilities on the device
425
+ const tools = orch.listMcpTools("", [], nodeId);
426
+ const abilities = (Array.isArray(tools) ? tools : []);
427
+ if (dryRun) {
428
+ const wouldRemove = [];
429
+ const wouldFail = [];
430
+ for (const tool of abilities) {
431
+ const t = tool;
432
+ const installId = String(t.install_id || "").trim();
433
+ const name = String(t.tool_name || "unknown");
434
+ if (!installId) {
435
+ wouldFail.push({ tool_name: name, error: "missing install_id" });
436
+ }
437
+ else {
438
+ wouldRemove.push(name);
439
+ }
440
+ }
441
+ return {
442
+ ok: true,
443
+ tenant_id: tenant,
444
+ node_id: nodeId,
445
+ dry_run: true,
446
+ removed: wouldRemove,
447
+ removed_count: wouldRemove.length,
448
+ failed: wouldFail,
449
+ failed_count: wouldFail.length,
450
+ };
451
+ }
452
+ const removed = [];
453
+ const failed = [];
454
+ for (const tool of abilities) {
455
+ const t = tool;
456
+ const installId = String(t.install_id || "").trim();
457
+ const name = String(t.tool_name || "unknown");
458
+ if (!installId) {
459
+ failed.push({ tool_name: name, error: "missing install_id" });
460
+ continue;
461
+ }
462
+ try {
463
+ orch.uninstallAbility(nodeId, installId, "forget_all: requested by agent");
464
+ removed.push(name);
465
+ }
466
+ catch (error) {
467
+ failed.push({ tool_name: name, error: error instanceof Error ? error.message : String(error) });
468
+ }
469
+ }
470
+ return {
471
+ ok: true,
472
+ tenant_id: tenant,
473
+ node_id: nodeId,
474
+ dry_run: false,
475
+ removed,
476
+ removed_count: removed.length,
477
+ failed,
478
+ failed_count: failed.length,
479
+ };
480
+ }
481
+ function randomHex(bytes) {
482
+ return randomBytes(bytes).toString("hex");
483
+ }
@@ -1,4 +1,4 @@
1
- import type { McpToolProvider, McpToolResult } from "../../mcp/server.js";
1
+ import { type McpToolProvider, type McpToolResult, type McpToolStreamHandle } from "../../mcp/server.js";
2
2
  import { type RemoteControlRuntimeConfig } from "./config.js";
3
3
  import { type JsonRecord } from "./descriptor.js";
4
4
  import { type OrchestratorFactory } from "./orchestrator.js";
@@ -14,7 +14,9 @@ export declare class RemoteControlCaseKit implements McpToolProvider {
14
14
  static ensureNativeLibEnv(): void;
15
15
  static ensureRemoteControlNativeLibEnv(): void;
16
16
  toolSpecs(): Array<JsonRecord>;
17
- handleToolCall(name: string, args: JsonRecord): McpToolResult;
17
+ handleToolCall(name: string, args: JsonRecord): McpToolResult | Promise<McpToolResult>;
18
+ /** Handle streaming calls for `call_remote_tool_stream`. Returns null for unknown tools. */
19
+ handleToolCallStream(name: string, args: JsonRecord): McpToolStreamHandle | null;
18
20
  private dispatch;
19
21
  private withOrchestrator;
20
22
  private toolResult;