@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.
- package/README.md +39 -1
- package/native/dendrite-bridge-manifest.json +5 -4
- package/native/dendrite-bridge.json +1 -1
- package/native/include/axon_dendrite_bridge.h +18 -4
- package/native/libaxon_dendrite_bridge.so +0 -0
- package/package.json +9 -5
- package/runtime/easynet-runtime-rs-0.39.29-x86_64-unknown-linux-gnu.tar.gz +0 -0
- package/runtime/runtime-bridge-manifest.json +4 -4
- package/runtime/runtime-bridge.json +3 -3
- package/src/ability_lifecycle.d.ts +12 -1
- package/src/ability_lifecycle.js +117 -31
- package/src/capability_request.js +3 -1
- package/src/dendrite_bridge/bridge.d.ts +10 -2
- package/src/dendrite_bridge/bridge.js +75 -14
- package/src/dendrite_bridge/ffi.d.ts +4 -0
- package/src/dendrite_bridge/ffi.js +194 -18
- package/src/dendrite_bridge/types.d.ts +4 -0
- package/src/errors.js +9 -3
- package/src/index.d.ts +3 -3
- package/src/index.js +9 -10
- package/src/mcp/server.d.ts +24 -2
- package/src/mcp/server.js +218 -18
- package/src/mcp/server.test.js +100 -0
- package/src/presets/ability_dispatch/workflow.js +8 -30
- package/src/presets/remote_control/config.d.ts +3 -0
- package/src/presets/remote_control/config.js +22 -24
- package/src/presets/remote_control/descriptor.d.ts +36 -0
- package/src/presets/remote_control/descriptor.js +267 -11
- package/src/presets/remote_control/handlers.d.ts +8 -0
- package/src/presets/remote_control/handlers.js +230 -26
- package/src/presets/remote_control/kit.d.ts +4 -2
- package/src/presets/remote_control/kit.js +106 -1
- package/src/presets/remote_control/kit.test.js +994 -0
- package/src/presets/remote_control/orchestrator.d.ts +6 -0
- package/src/presets/remote_control/orchestrator.js +36 -1
- package/src/presets/remote_control/specs.js +217 -61
- package/src/receipt.js +6 -3
- package/runtime/easynet-runtime-rs-0.27.14-x86_64-unknown-linux-gnu.tar.gz +0 -0
|
@@ -1,24 +1,44 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
11
|
-
import {
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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()}_${
|
|
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
|
|
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;
|