@easynet-run/node 0.36.9 → 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 +29 -3
- package/native/dendrite-bridge-manifest.json +3 -3
- package/native/dendrite-bridge.json +1 -1
- package/native/libaxon_dendrite_bridge.so +0 -0
- package/package.json +5 -3
- 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 +11 -1
- package/src/ability_lifecycle.js +106 -26
- package/src/dendrite_bridge/bridge.d.ts +1 -0
- package/src/dendrite_bridge/bridge.js +21 -9
- package/src/dendrite_bridge/ffi.d.ts +1 -0
- package/src/dendrite_bridge/ffi.js +192 -18
- package/src/index.d.ts +1 -1
- package/src/index.js +9 -10
- package/src/mcp/server.test.js +55 -17
- package/src/presets/ability_dispatch/workflow.js +8 -30
- package/src/presets/remote_control/config.js +6 -22
- package/src/presets/remote_control/descriptor.d.ts +36 -0
- package/src/presets/remote_control/descriptor.js +243 -4
- package/src/presets/remote_control/handlers.d.ts +6 -0
- package/src/presets/remote_control/handlers.js +208 -9
- package/src/presets/remote_control/kit.js +37 -1
- package/src/presets/remote_control/kit.test.js +545 -0
- package/src/presets/remote_control/orchestrator.d.ts +1 -0
- package/src/presets/remote_control/orchestrator.js +27 -0
- package/src/presets/remote_control/specs.js +137 -0
- package/runtime/easynet-runtime-rs-0.36.9-x86_64-unknown-linux-gnu.tar.gz +0 -0
|
@@ -2,7 +2,22 @@
|
|
|
2
2
|
// =========================
|
|
3
3
|
//
|
|
4
4
|
// File: sdk/node/src/presets/remote_control/descriptor.ts
|
|
5
|
-
// Description:
|
|
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.
|
|
6
21
|
//
|
|
7
22
|
// Author: Silan.Hu
|
|
8
23
|
// Email: silan.hu@u.nus.edu
|
|
@@ -20,7 +35,10 @@ export function randomSuffix(length = 2) {
|
|
|
20
35
|
}
|
|
21
36
|
export function sanitizeId(raw) {
|
|
22
37
|
const token = asString(raw).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
23
|
-
|
|
38
|
+
if (!token) {
|
|
39
|
+
throw new Error(`identifier contains no valid characters: ${JSON.stringify(raw)}`);
|
|
40
|
+
}
|
|
41
|
+
return token;
|
|
24
42
|
}
|
|
25
43
|
export function shellSingleQuote(raw) {
|
|
26
44
|
return `'${raw.replace(/'/g, "'\"'\"'")}'`;
|
|
@@ -142,6 +160,16 @@ export function parseDescriptor(raw) {
|
|
|
142
160
|
digest: asString(input.digest) || undefined,
|
|
143
161
|
};
|
|
144
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
|
+
*/
|
|
145
173
|
export function buildDescriptor(args, defaultSignature) {
|
|
146
174
|
const abilityName = requireString(args, "ability_name");
|
|
147
175
|
const commandTemplate = requireString(args, "command_template");
|
|
@@ -162,7 +190,7 @@ export function buildDescriptor(args, defaultSignature) {
|
|
|
162
190
|
const outputSchema = typeof args.output_schema === "object" && !Array.isArray(args.output_schema)
|
|
163
191
|
? args.output_schema
|
|
164
192
|
: defaultOutputSchema();
|
|
165
|
-
const
|
|
193
|
+
const baseMeta = {
|
|
166
194
|
"mcp.tool_name": toolName,
|
|
167
195
|
"mcp.description": description,
|
|
168
196
|
"mcp.input_schema": JSON.stringify(inputSchema),
|
|
@@ -170,7 +198,26 @@ export function buildDescriptor(args, defaultSignature) {
|
|
|
170
198
|
"axon.exec.command": commandTemplate,
|
|
171
199
|
"ability.name": abilityName,
|
|
172
200
|
"ability.version": version,
|
|
173
|
-
}
|
|
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);
|
|
174
221
|
const payload = {
|
|
175
222
|
kind: "axon.ability.package.v1",
|
|
176
223
|
ability_name: abilityName,
|
|
@@ -198,3 +245,195 @@ export function buildDescriptor(args, defaultSignature) {
|
|
|
198
245
|
digest: asString(args.digest) || undefined,
|
|
199
246
|
};
|
|
200
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 };
|
|
@@ -12,3 +12,9 @@ export declare function handlePackageAbility(tenant: string, args: JsonRecord, s
|
|
|
12
12
|
export declare function handleDeployAbilityPackage(orch: RemoteOrchestrator, tenant: string, args: JsonRecord, signature: string): JsonRecord;
|
|
13
13
|
export declare function handleDeployAbility(orch: RemoteOrchestrator, tenant: string, args: JsonRecord, signature: string): JsonRecord;
|
|
14
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,28 @@
|
|
|
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
25
|
import { randomBytes } from "node:crypto";
|
|
11
|
-
import { buildDescriptor, buildPythonSubprocessTemplate, parseBool, parseDescriptor, serializeDescriptor, } from "./descriptor.js";
|
|
26
|
+
import { buildDescriptor, buildPythonSubprocessTemplate, createAbilityDescriptor, exportAbilitySkill, parseAbilityTarget, parseBool, parseDescriptor, serializeDescriptor, } from "./descriptor.js";
|
|
12
27
|
import { asBool, asNumber, asString, DEFAULT_MCP_TOOL_STREAM_TIMEOUT_MS } from "./config.js";
|
|
13
28
|
// ---------------------------------------------------------------------------
|
|
14
29
|
// Architecture: ability_lifecycle vs presets/remote_control
|
|
@@ -33,6 +48,18 @@ const INVOCATION_COMPLETED = 5;
|
|
|
33
48
|
export function resolveDescriptor(args, fallbackSignature) {
|
|
34
49
|
return args.package ? parseDescriptor(args.package) : buildDescriptor(args, fallbackSignature);
|
|
35
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
|
+
}
|
|
36
63
|
export function handleDiscoverNodes(orch, tenant) {
|
|
37
64
|
const nodes = orch.listNodes("");
|
|
38
65
|
return {
|
|
@@ -56,13 +83,11 @@ export function handleListRemoteTools(orch, tenant, args) {
|
|
|
56
83
|
count: tools.length,
|
|
57
84
|
node_id: nodeId,
|
|
58
85
|
name_pattern: pattern,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
node_ids: tool.node_ids,
|
|
65
|
-
})),
|
|
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 })),
|
|
66
91
|
};
|
|
67
92
|
}
|
|
68
93
|
export function handleCallRemoteTool(orch, tenant, args) {
|
|
@@ -279,6 +304,180 @@ function parseCleanupSummary(failure, orch, nodeId, shouldCleanup) {
|
|
|
279
304
|
},
|
|
280
305
|
]);
|
|
281
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
|
+
}
|
|
282
481
|
function randomHex(bytes) {
|
|
283
482
|
return randomBytes(bytes).toString("hex");
|
|
284
483
|
}
|
|
@@ -1,7 +1,31 @@
|
|
|
1
|
+
// EasyNet Axon for AgentNet
|
|
2
|
+
// =========================
|
|
3
|
+
//
|
|
4
|
+
// File: sdk/node/src/presets/remote_control/kit.ts
|
|
5
|
+
// Description: Node `RemoteControlCaseKit` MCP provider that wires handlers, tool specs, and managed tool streams.
|
|
6
|
+
//
|
|
7
|
+
// Protocol Responsibility:
|
|
8
|
+
// - Assembles remote-control tool specs, handler dispatch, and orchestrator lifecycle into one MCP provider surface.
|
|
9
|
+
// - Owns bridge or orchestrator cleanup for unary and streaming tool calls so callers do not leak resources.
|
|
10
|
+
//
|
|
11
|
+
// Implementation Approach:
|
|
12
|
+
// - Keeps entrypoints thin by delegating validation and business logic to focused handler/orchestrator modules.
|
|
13
|
+
// - Caches or scopes transport resources according to the SDK runtime model while preserving per-tenant correctness.
|
|
14
|
+
//
|
|
15
|
+
// Usage Contract:
|
|
16
|
+
// - Use this as the preset-level integration point when exposing EasyNet remote-control tools over MCP.
|
|
17
|
+
// - Factory injection points should preserve the same request/response semantics as the default orchestrator.
|
|
18
|
+
//
|
|
19
|
+
// Architectural Position:
|
|
20
|
+
// - Preset composition boundary above handler modules and below case/example entrypoints.
|
|
21
|
+
//
|
|
22
|
+
// Author: Silan.Hu
|
|
23
|
+
// Email: silan.hu@u.nus.edu
|
|
24
|
+
// Copyright (c) 2026-2027 easynet. All rights reserved.
|
|
1
25
|
import { consumeStream } from "../../mcp/server.js";
|
|
2
26
|
import { ensureRemoteControlNativeLibEnv, ensureNativeLibEnv, loadConfigFromEnv, loadRemoteControlConfigFromEnv, } from "./config.js";
|
|
3
27
|
import { resolveTenant } from "./descriptor.js";
|
|
4
|
-
import { handleCallRemoteTool, handleCallRemoteToolStream, handleDeployAbility, handleDeployAbilityPackage, handleDiscoverNodes, handleDisconnectDevice, handleExecuteCommand, handleListRemoteTools, handlePackageAbility, handleUninstallAbility, } from "./handlers.js";
|
|
28
|
+
import { handleCallRemoteTool, handleCallRemoteToolStream, handleCreateAbility, handleDeployAbility, handleDeployAbilityPackage, handleDiscoverNodes, handleDisconnectDevice, handleDrainDevice, handleExecuteCommand, handleExportAbilitySkill, handleForgetAll, handleListAbilities, handleListRemoteTools, handlePackageAbility, handleUninstallAbility, handleRedeployAbility, } from "./handlers.js";
|
|
5
29
|
import { remoteControlToolSpecs } from "./specs.js";
|
|
6
30
|
import { buildOrchestrator } from "./orchestrator.js";
|
|
7
31
|
/**
|
|
@@ -118,6 +142,18 @@ export class RemoteControlCaseKit {
|
|
|
118
142
|
return this.toolResultWithPayload(handleDeployAbility(orchestrator, tenant, args, this.config.signatureBase64));
|
|
119
143
|
case "execute_command":
|
|
120
144
|
return this.toolResultWithPayload(handleExecuteCommand(orchestrator, tenant, args, this.config.signatureBase64));
|
|
145
|
+
case "drain_device":
|
|
146
|
+
return this.toolResultWithPayload(handleDrainDevice(orchestrator, tenant, args));
|
|
147
|
+
case "build_ability_descriptor":
|
|
148
|
+
return this.toolResultWithPayload(handleCreateAbility(args));
|
|
149
|
+
case "export_ability_skill":
|
|
150
|
+
return this.toolResultWithPayload(handleExportAbilitySkill(args));
|
|
151
|
+
case "redeploy_ability":
|
|
152
|
+
return this.toolResultWithPayload(handleRedeployAbility(orchestrator, tenant, args, this.config.signatureBase64));
|
|
153
|
+
case "list_abilities":
|
|
154
|
+
return this.toolResultWithPayload(handleListAbilities(orchestrator, tenant, args));
|
|
155
|
+
case "forget_all":
|
|
156
|
+
return this.toolResultWithPayload(handleForgetAll(orchestrator, tenant, args));
|
|
121
157
|
default:
|
|
122
158
|
return this.toolResult(true, { ok: false, tenant_id: tenant, error: `unknown tool: ${name}` });
|
|
123
159
|
}
|