@easynet-run/node 0.27.14
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 +135 -0
- package/native/dendrite-bridge-manifest.json +38 -0
- package/native/dendrite-bridge.json +15 -0
- package/native/include/axon_dendrite_bridge.h +460 -0
- package/native/libaxon_dendrite_bridge.so +0 -0
- package/package.json +67 -0
- package/runtime/easynet-runtime-rs-0.27.14-x86_64-unknown-linux-gnu.tar.gz +0 -0
- package/runtime/runtime-bridge-manifest.json +20 -0
- package/runtime/runtime-bridge.json +9 -0
- package/src/ability_lifecycle.d.ts +140 -0
- package/src/ability_lifecycle.js +525 -0
- package/src/capability_request.d.ts +14 -0
- package/src/capability_request.js +247 -0
- package/src/dendrite_bridge/bridge.d.ts +98 -0
- package/src/dendrite_bridge/bridge.js +712 -0
- package/src/dendrite_bridge/ffi.d.ts +60 -0
- package/src/dendrite_bridge/ffi.js +139 -0
- package/src/dendrite_bridge/index.d.ts +3 -0
- package/src/dendrite_bridge/index.js +25 -0
- package/src/dendrite_bridge/types.d.ts +179 -0
- package/src/dendrite_bridge/types.js +23 -0
- package/src/dendrite_bridge.d.ts +1 -0
- package/src/dendrite_bridge.js +27 -0
- package/src/errors.d.ts +83 -0
- package/src/errors.js +146 -0
- package/src/index.d.ts +55 -0
- package/src/index.js +164 -0
- package/src/koffi.d.ts +34 -0
- package/src/mcp/server.d.ts +29 -0
- package/src/mcp/server.js +190 -0
- package/src/presets/ability_dispatch/args.d.ts +5 -0
- package/src/presets/ability_dispatch/args.js +36 -0
- package/src/presets/ability_dispatch/bundle.d.ts +7 -0
- package/src/presets/ability_dispatch/bundle.js +102 -0
- package/src/presets/ability_dispatch/media.d.ts +6 -0
- package/src/presets/ability_dispatch/media.js +48 -0
- package/src/presets/ability_dispatch/orchestrator.d.ts +21 -0
- package/src/presets/ability_dispatch/orchestrator.js +117 -0
- package/src/presets/ability_dispatch/workflow.d.ts +50 -0
- package/src/presets/ability_dispatch/workflow.js +333 -0
- package/src/presets/ability_dispatch.d.ts +1 -0
- package/src/presets/ability_dispatch.js +2 -0
- package/src/presets/remote_control/config.d.ts +16 -0
- package/src/presets/remote_control/config.js +63 -0
- package/src/presets/remote_control/descriptor.d.ts +34 -0
- package/src/presets/remote_control/descriptor.js +183 -0
- package/src/presets/remote_control/handlers.d.ts +12 -0
- package/src/presets/remote_control/handlers.js +279 -0
- package/src/presets/remote_control/kit.d.ts +22 -0
- package/src/presets/remote_control/kit.js +72 -0
- package/src/presets/remote_control/kit.test.js +87 -0
- package/src/presets/remote_control/orchestrator.d.ts +28 -0
- package/src/presets/remote_control/orchestrator.js +118 -0
- package/src/presets/remote_control/specs.d.ts +2 -0
- package/src/presets/remote_control/specs.js +152 -0
- package/src/presets/remote_control_case.d.ts +7 -0
- package/src/presets/remote_control_case.js +3 -0
- package/src/receipt.d.ts +46 -0
- package/src/receipt.js +98 -0
- package/src/tool_adapter.d.ts +90 -0
- package/src/tool_adapter.js +169 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
// sdk/node/src/ability_lifecycle.ts — Ability lifecycle API: create, deploy, and export as Agent Skills.
|
|
2
|
+
//
|
|
3
|
+
// Copyright (c) 2026-2027 easynet. All rights reserved.
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { webcrypto } from "node:crypto";
|
|
6
|
+
import { createServer, createConnection } from "node:net";
|
|
7
|
+
import { existsSync, mkdirSync, openSync } from "node:fs";
|
|
8
|
+
import { hostname as localHostname } from "node:os";
|
|
9
|
+
import { join, dirname } from "node:path";
|
|
10
|
+
import { env } from "node:process";
|
|
11
|
+
import { buildPythonSubprocessTemplate } from "./presets/remote_control/descriptor.js";
|
|
12
|
+
import { AxonConfigError, AxonBridgeError, AxonInvocationError, AxonPartialSuccessError } from "./errors.js";
|
|
13
|
+
import { DEFAULT_SIGNATURE } from "./presets/remote_control/config.js";
|
|
14
|
+
import { beginPhase, buildDeployTrace } from "./receipt.js";
|
|
15
|
+
/** Placeholder signature for ephemeral/temporary skill deployments.
|
|
16
|
+
* Canonical constant lives in presets/remote_control/config.ts.
|
|
17
|
+
*/
|
|
18
|
+
const EPHEMERAL_SIGNATURE = DEFAULT_SIGNATURE;
|
|
19
|
+
/** Default Axon runtime port. */
|
|
20
|
+
const DEFAULT_AXON_PORT = 50051;
|
|
21
|
+
/** Generate a random hex string for unique tool name suffixes. */
|
|
22
|
+
function randomHex(bytes) {
|
|
23
|
+
const arr = new Uint8Array(bytes);
|
|
24
|
+
webcrypto.getRandomValues(arr);
|
|
25
|
+
return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
function normalizeAbilityName(raw) {
|
|
31
|
+
const result = raw
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.replace(/[^a-z0-9_\-]/g, "-")
|
|
34
|
+
.replace(/-{2,}/g, "-")
|
|
35
|
+
.replace(/^-+|-+$/g, "");
|
|
36
|
+
return result || "ability";
|
|
37
|
+
}
|
|
38
|
+
function firstNonEmpty(...values) {
|
|
39
|
+
for (const value of values) {
|
|
40
|
+
const trimmed = value?.trim();
|
|
41
|
+
if (trimmed)
|
|
42
|
+
return trimmed;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Server lifecycle — start / connect / stop
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
const DEFAULT_LOG_DIR = join(env.HOME ?? env.USERPROFILE ?? "/tmp", ".easynet", "logs");
|
|
50
|
+
function cliLog(msg) {
|
|
51
|
+
const ts = new Date().toLocaleTimeString("en-GB", { hour12: false });
|
|
52
|
+
process.stderr.write(`[axon ${ts}] ${msg}\n`);
|
|
53
|
+
}
|
|
54
|
+
function findRuntimeBinary() {
|
|
55
|
+
const envBin = env.AXON_RUNTIME_BIN;
|
|
56
|
+
if (envBin && existsSync(envBin))
|
|
57
|
+
return envBin;
|
|
58
|
+
// Fall back to PATH lookup
|
|
59
|
+
return "axon-runtime";
|
|
60
|
+
}
|
|
61
|
+
function findFreePort() {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const srv = createServer();
|
|
64
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
65
|
+
const addr = srv.address();
|
|
66
|
+
if (addr && typeof addr === "object") {
|
|
67
|
+
const port = addr.port;
|
|
68
|
+
srv.close(() => resolve(port));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
srv.close(() => reject(new AxonBridgeError("failed to get free port")));
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
srv.on("error", reject);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function waitForPort(host, port, timeoutMs) {
|
|
78
|
+
const deadline = Date.now() + timeoutMs;
|
|
79
|
+
return new Promise((res) => {
|
|
80
|
+
function tryConnect() {
|
|
81
|
+
if (Date.now() > deadline) {
|
|
82
|
+
res(false);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const sock = createConnection({ host, port, timeout: 500 }, () => {
|
|
86
|
+
sock.destroy();
|
|
87
|
+
res(true);
|
|
88
|
+
});
|
|
89
|
+
sock.on("error", () => { sock.destroy(); setTimeout(tryConnect, 100); });
|
|
90
|
+
}
|
|
91
|
+
tryConnect();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function parseEndpoint(endpoint) {
|
|
95
|
+
const raw = endpoint.trim();
|
|
96
|
+
if (raw.startsWith("axon://")) {
|
|
97
|
+
const authority = raw.slice("axon://".length).split("/")[0];
|
|
98
|
+
if (!authority || authority === "localhost")
|
|
99
|
+
return { host: "127.0.0.1", port: DEFAULT_AXON_PORT };
|
|
100
|
+
if (authority.startsWith("localhost:"))
|
|
101
|
+
return { host: "127.0.0.1", port: parseInt(authority.split(":")[1], 10) || DEFAULT_AXON_PORT };
|
|
102
|
+
const idx = authority.lastIndexOf(":");
|
|
103
|
+
if (idx > 0)
|
|
104
|
+
return { host: authority.slice(0, idx), port: parseInt(authority.slice(idx + 1), 10) || DEFAULT_AXON_PORT };
|
|
105
|
+
return { host: authority, port: DEFAULT_AXON_PORT };
|
|
106
|
+
}
|
|
107
|
+
const url = raw.replace(/^https?:\/\//, "").split("/")[0];
|
|
108
|
+
const idx = url.lastIndexOf(":");
|
|
109
|
+
if (idx > 0)
|
|
110
|
+
return { host: url.slice(0, idx), port: parseInt(url.slice(idx + 1), 10) || DEFAULT_AXON_PORT };
|
|
111
|
+
return { host: url, port: DEFAULT_AXON_PORT };
|
|
112
|
+
}
|
|
113
|
+
function normalizeHubEndpoint(endpoint) {
|
|
114
|
+
const trimmed = endpoint?.trim();
|
|
115
|
+
if (!trimmed)
|
|
116
|
+
return undefined;
|
|
117
|
+
if (trimmed.startsWith("axon://")) {
|
|
118
|
+
const { host, port } = parseEndpoint(trimmed);
|
|
119
|
+
return `http://${host}:${port}`;
|
|
120
|
+
}
|
|
121
|
+
return trimmed;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Start or connect to an Axon runtime.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* // Auto-start a local server (zero config):
|
|
128
|
+
* const srv = await startServer();
|
|
129
|
+
*
|
|
130
|
+
* // Connect via axon:// transport URI:
|
|
131
|
+
* const srv = await startServer("axon://localhost");
|
|
132
|
+
* const srv = await startServer("axon://10.0.0.5:50084");
|
|
133
|
+
*
|
|
134
|
+
* // Federation mode — connect the local runtime to a Hub:
|
|
135
|
+
* const srv = await startServer(undefined, { hub: "axon://hub.easynet.run:50084" });
|
|
136
|
+
*/
|
|
137
|
+
export async function startServer(endpoint, options = {}) {
|
|
138
|
+
const timeoutMs = options.timeoutMs ?? 10000;
|
|
139
|
+
// Resolve from env if not provided
|
|
140
|
+
if (!endpoint) {
|
|
141
|
+
endpoint = env.EASYNET_AXON_ENDPOINT;
|
|
142
|
+
}
|
|
143
|
+
// --- Connect to existing server ---
|
|
144
|
+
if (endpoint) {
|
|
145
|
+
cliLog(`connecting to ${endpoint}`);
|
|
146
|
+
const { host, port } = parseEndpoint(endpoint);
|
|
147
|
+
const ok = await waitForPort(host, port, timeoutMs);
|
|
148
|
+
if (!ok)
|
|
149
|
+
throw new AxonBridgeError(`cannot reach server at ${endpoint}`);
|
|
150
|
+
cliLog(`connected to ${endpoint}`);
|
|
151
|
+
return { endpoint, process: null, logFile: null, stop() { } };
|
|
152
|
+
}
|
|
153
|
+
// --- Spawn local server ---
|
|
154
|
+
const port = await findFreePort();
|
|
155
|
+
const host = "127.0.0.1";
|
|
156
|
+
const bindAddr = `${host}:${port}`;
|
|
157
|
+
const endpointUrl = `http://${bindAddr}`;
|
|
158
|
+
const binary = findRuntimeBinary();
|
|
159
|
+
const logDir = DEFAULT_LOG_DIR;
|
|
160
|
+
const logFile = options.logFile ?? join(logDir, "axon-runtime.log");
|
|
161
|
+
mkdirSync(dirname(logFile), { recursive: true });
|
|
162
|
+
const logFd = openSync(logFile, "a");
|
|
163
|
+
const childEnv = {
|
|
164
|
+
...process.env,
|
|
165
|
+
AXON_BIND: bindAddr,
|
|
166
|
+
AXON_ENFORCE_MTLS: (options.insecure ?? true) ? "false" : "true",
|
|
167
|
+
};
|
|
168
|
+
const hubEndpoint = normalizeHubEndpoint(firstNonEmpty(options.hub, env.AXON_HUB));
|
|
169
|
+
if (hubEndpoint) {
|
|
170
|
+
childEnv.AXON_HUB = hubEndpoint;
|
|
171
|
+
childEnv.AXON_FEDERATION_TENANT = firstNonEmpty(options.hubTenant, env.AXON_FEDERATION_TENANT, "default");
|
|
172
|
+
childEnv.AXON_FEDERATION_LABEL = firstNonEmpty(options.hubLabel, env.AXON_FEDERATION_LABEL, localHostname());
|
|
173
|
+
const hubJoinToken = firstNonEmpty(options.hubJoinToken, env.AXON_HUB_JOIN_TOKEN, env.AXON_FEDERATION_JOIN_TOKEN);
|
|
174
|
+
if (hubJoinToken) {
|
|
175
|
+
childEnv.AXON_HUB_JOIN_TOKEN = hubJoinToken;
|
|
176
|
+
}
|
|
177
|
+
cliLog(`federation: will connect to hub at ${hubEndpoint}`);
|
|
178
|
+
}
|
|
179
|
+
cliLog(`starting axon-runtime on ${bindAddr} (log: ${logFile})`);
|
|
180
|
+
const child = spawn(binary, [], {
|
|
181
|
+
env: childEnv,
|
|
182
|
+
stdio: ["ignore", logFd, logFd],
|
|
183
|
+
});
|
|
184
|
+
const ready = await waitForPort(host, port, timeoutMs);
|
|
185
|
+
if (!ready) {
|
|
186
|
+
child.kill();
|
|
187
|
+
throw new AxonBridgeError(`axon-runtime not ready within ${timeoutMs}ms on ${bindAddr} (see ${logFile})`);
|
|
188
|
+
}
|
|
189
|
+
cliLog(`axon-runtime ready → ${endpointUrl}`);
|
|
190
|
+
return {
|
|
191
|
+
endpoint: endpointUrl,
|
|
192
|
+
process: child,
|
|
193
|
+
logFile,
|
|
194
|
+
stop() {
|
|
195
|
+
if (child.exitCode === null) {
|
|
196
|
+
cliLog(`stopping axon-runtime (pid ${child.pid})`);
|
|
197
|
+
child.kill("SIGTERM");
|
|
198
|
+
const timer = setTimeout(() => {
|
|
199
|
+
if (child.exitCode === null)
|
|
200
|
+
child.kill("SIGKILL");
|
|
201
|
+
}, 5000);
|
|
202
|
+
timer.unref();
|
|
203
|
+
child.once("exit", () => {
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
cliLog("axon-runtime stopped");
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// create
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
export function createAbility(opts) {
|
|
215
|
+
if (!opts.name?.trim())
|
|
216
|
+
throw new AxonConfigError("ability name cannot be empty");
|
|
217
|
+
if (!opts.commandTemplate?.trim())
|
|
218
|
+
throw new AxonConfigError("command_template cannot be empty");
|
|
219
|
+
const token = normalizeAbilityName(opts.name);
|
|
220
|
+
return {
|
|
221
|
+
name: opts.name,
|
|
222
|
+
description: opts.description,
|
|
223
|
+
commandTemplate: opts.commandTemplate,
|
|
224
|
+
inputSchema: opts.inputSchema ?? { type: "object", properties: {} },
|
|
225
|
+
outputSchema: opts.outputSchema ?? { type: "object", properties: {} },
|
|
226
|
+
version: opts.version ?? "1.0.0",
|
|
227
|
+
tags: opts.tags ?? [],
|
|
228
|
+
resourceUri: opts.resourceUri ?? `easynet:///r/org/${token}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// toToolSpec
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
export function toToolSpec(descriptor) {
|
|
235
|
+
const token = normalizeAbilityName(descriptor.name);
|
|
236
|
+
return {
|
|
237
|
+
name: token,
|
|
238
|
+
description: descriptor.description,
|
|
239
|
+
resourceUri: descriptor.resourceUri,
|
|
240
|
+
parameters: descriptor.inputSchema,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// exportAbility
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
export function exportAbility(descriptor, target = "agent_skills", axonEndpoint) {
|
|
247
|
+
const token = normalizeAbilityName(descriptor.name);
|
|
248
|
+
const endpoint = axonEndpoint ?? `http://127.0.0.1:${DEFAULT_AXON_PORT}`;
|
|
249
|
+
const invokeScript = generateInvokeScript(descriptor.resourceUri, endpoint);
|
|
250
|
+
const abilityMd = generateAbilityMd(descriptor, target, token);
|
|
251
|
+
return { abilityMd, invokeScript, abilityName: token };
|
|
252
|
+
}
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// deployToNode
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
/**
|
|
257
|
+
* Deploy an AbilityDescriptor to a node via the MCP deploy pipeline.
|
|
258
|
+
*
|
|
259
|
+
* The underlying bridge call executes a three-phase pipeline
|
|
260
|
+
* (Publish → Install → Activate). This function wraps the entire
|
|
261
|
+
* pipeline in a single {@link DeployTrace} receipt so callers get
|
|
262
|
+
* wall-clock timing and error codes even on failure.
|
|
263
|
+
*
|
|
264
|
+
* On failure an {@link AxonInvocationError} is thrown with its `trace`
|
|
265
|
+
* field set to the collected {@link DeployTrace}:
|
|
266
|
+
*
|
|
267
|
+
* ```ts
|
|
268
|
+
* try { await deployToNode(bridge, tenant, nodeId, desc, sig); }
|
|
269
|
+
* catch (e) { if (e instanceof AxonInvocationError) console.log(e.trace); }
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
export async function deployToNode(bridge, tenant, nodeId, descriptor, signature) {
|
|
273
|
+
const token = normalizeAbilityName(descriptor.name);
|
|
274
|
+
const abilityId = token;
|
|
275
|
+
const pkg = buildDeployPackage({
|
|
276
|
+
ability_name: descriptor.name,
|
|
277
|
+
tool_name: token,
|
|
278
|
+
description: descriptor.description,
|
|
279
|
+
command_template: descriptor.commandTemplate,
|
|
280
|
+
version: descriptor.version,
|
|
281
|
+
input_schema: descriptor.inputSchema,
|
|
282
|
+
output_schema: descriptor.outputSchema,
|
|
283
|
+
tags: descriptor.tags.length > 0 ? descriptor.tags : undefined,
|
|
284
|
+
}, signature);
|
|
285
|
+
const builder = beginPhase("deploy", tenant, nodeId, abilityId);
|
|
286
|
+
try {
|
|
287
|
+
const result = await bridge.deployAbilityPackage(tenant, nodeId, pkg);
|
|
288
|
+
const installId = String(result.install_id ?? "");
|
|
289
|
+
const receipt = builder.finishOk(installId || undefined);
|
|
290
|
+
const trace = buildDeployTrace([receipt]);
|
|
291
|
+
return { result, trace };
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
294
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
295
|
+
const receipt = builder.finishErr(err);
|
|
296
|
+
const trace = buildDeployTrace([receipt]);
|
|
297
|
+
throw new AxonInvocationError(err.message, { install_id: abilityId }, trace);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// listAbilities
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
export async function listAbilities(bridge, tenant, nodeId) {
|
|
304
|
+
const result = await bridge.listMcpTools(tenant, "", nodeId);
|
|
305
|
+
return (Array.isArray(result) ? result : []);
|
|
306
|
+
}
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// invokeAbility
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
export async function invokeAbility(bridge, tenant, nodeId, toolName, args = {}) {
|
|
311
|
+
return await bridge.callMcpToolWithArgs(tenant, toolName, nodeId, args);
|
|
312
|
+
}
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// uninstallAbility
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
export async function uninstallAbility(bridge, tenant, nodeId, installId, reason) {
|
|
317
|
+
return await bridge.uninstallAbilityWithReason(tenant, nodeId, installId, reason ?? "ability lifecycle: uninstall");
|
|
318
|
+
}
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// discoverNodes
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
export async function discoverNodes(bridge, tenant) {
|
|
323
|
+
const result = await bridge.listNodes(tenant, null);
|
|
324
|
+
return (Array.isArray(result) ? result : []);
|
|
325
|
+
}
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// executeCommand
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
export async function executeCommand(bridge, tenant, nodeId, command) {
|
|
330
|
+
const toolName = `cmd_${Date.now()}_${randomHex(4)}`;
|
|
331
|
+
const wrapped = buildPythonSubprocessTemplate(command);
|
|
332
|
+
const deployResult = await bridge.deployAbilityPackage(tenant, nodeId, {
|
|
333
|
+
ability_name: toolName,
|
|
334
|
+
tool_name: toolName,
|
|
335
|
+
description: `execute: ${command}`,
|
|
336
|
+
command_template: wrapped,
|
|
337
|
+
signature_base64: EPHEMERAL_SIGNATURE,
|
|
338
|
+
tags: ["ephemeral", "auto-cleanup"],
|
|
339
|
+
});
|
|
340
|
+
const result = await bridge.callMcpToolWithArgs(tenant, toolName, nodeId, {});
|
|
341
|
+
let cleanupOk = true;
|
|
342
|
+
let cleanupError;
|
|
343
|
+
const installId = String(deployResult.install_id ?? "");
|
|
344
|
+
if (installId) {
|
|
345
|
+
try {
|
|
346
|
+
await bridge.uninstallAbilityWithReason(tenant, nodeId, installId, "execute_command cleanup");
|
|
347
|
+
}
|
|
348
|
+
catch (e) {
|
|
349
|
+
process.stderr.write(`[axon] execute_command cleanup failed for ${installId}: ${e}\n`);
|
|
350
|
+
cleanupOk = false;
|
|
351
|
+
cleanupError = String(e);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return { ...result, cleanup: { ok: cleanupOk, error: cleanupError } };
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Remove all deployed abilities from a device.
|
|
358
|
+
* Requires `confirm = true` as a safety gate (destructive operation).
|
|
359
|
+
*
|
|
360
|
+
* When `options.dryRun` is `true`, the function lists all abilities that would
|
|
361
|
+
* be removed without performing any uninstalls. The `confirm` parameter is
|
|
362
|
+
* ignored for dry-run calls since no data is modified.
|
|
363
|
+
*
|
|
364
|
+
* @throws {AxonPartialSuccessError} When any uninstall fails (partial or total failure).
|
|
365
|
+
* @throws {AxonConfigError} When `confirm` is not `true` and `dryRun` is not set.
|
|
366
|
+
*/
|
|
367
|
+
export async function forgetAll(bridge, tenant, nodeId, confirm = false, options = {}) {
|
|
368
|
+
const dryRun = options.dryRun ?? false;
|
|
369
|
+
if (!confirm && !dryRun) {
|
|
370
|
+
throw new AxonConfigError("forget_all requires confirm = true (destructive operation)");
|
|
371
|
+
}
|
|
372
|
+
const tools = await listAbilities(bridge, tenant, nodeId);
|
|
373
|
+
// Dry-run mode: collect the list of abilities that would be removed
|
|
374
|
+
// without performing any uninstalls.
|
|
375
|
+
if (dryRun) {
|
|
376
|
+
const wouldRemove = [];
|
|
377
|
+
for (const tool of tools) {
|
|
378
|
+
const installId = String(tool.install_id ?? "");
|
|
379
|
+
const toolName = String(tool.tool_name ?? "");
|
|
380
|
+
if (!installId)
|
|
381
|
+
continue;
|
|
382
|
+
wouldRemove.push(toolName);
|
|
383
|
+
}
|
|
384
|
+
return { removed: wouldRemove, removed_count: wouldRemove.length, failed: [], failed_count: 0 };
|
|
385
|
+
}
|
|
386
|
+
const removed = [];
|
|
387
|
+
const failed = [];
|
|
388
|
+
for (const tool of tools) {
|
|
389
|
+
const installId = String(tool.install_id ?? "");
|
|
390
|
+
const toolName = String(tool.tool_name ?? "");
|
|
391
|
+
if (!installId)
|
|
392
|
+
continue;
|
|
393
|
+
try {
|
|
394
|
+
await bridge.uninstallAbilityWithReason(tenant, nodeId, installId, "forget_all");
|
|
395
|
+
removed.push(toolName);
|
|
396
|
+
}
|
|
397
|
+
catch (e) {
|
|
398
|
+
process.stderr.write(`[axon] forget_all: failed to uninstall ${toolName}: ${e}\n`);
|
|
399
|
+
failed.push({ tool_name: toolName, error: String(e) });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const result = { removed, removed_count: removed.length, failed, failed_count: failed.length };
|
|
403
|
+
if (failed.length > 0) {
|
|
404
|
+
throw new AxonPartialSuccessError(`forget_all: ${removed.length} succeeded, ${failed.length} failed`, removed.length, failed.length, { removed, failed, result });
|
|
405
|
+
}
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// disconnectDevice
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
export async function disconnectDevice(bridge, tenant, nodeId, reason) {
|
|
412
|
+
return await bridge.deregisterNode(tenant, nodeId, reason ?? "sdk: disconnect_device");
|
|
413
|
+
}
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// drainDevice
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
export async function drainDevice(bridge, tenant, nodeId, reason) {
|
|
418
|
+
return await bridge.drainNode(tenant, nodeId, reason ?? "sdk: drain_device");
|
|
419
|
+
}
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// listRemoteTools
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
export async function listRemoteTools(bridge, tenant, namePattern = "", nodeId = "") {
|
|
424
|
+
const result = await bridge.listMcpTools(tenant, namePattern, nodeId);
|
|
425
|
+
return (Array.isArray(result) ? result : []);
|
|
426
|
+
}
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
// buildDeployPackage
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
export function buildDeployPackage(args, signature) {
|
|
431
|
+
const abilityName = String(args.ability_name ?? "");
|
|
432
|
+
const toolName = String(args.tool_name ?? abilityName);
|
|
433
|
+
return {
|
|
434
|
+
ability_name: abilityName,
|
|
435
|
+
tool_name: toolName,
|
|
436
|
+
description: args.description ?? "",
|
|
437
|
+
command_template: args.command_template ?? "",
|
|
438
|
+
input_schema: args.input_schema ?? { type: "object", properties: {} },
|
|
439
|
+
output_schema: args.output_schema ?? { type: "object", properties: {} },
|
|
440
|
+
version: args.version ?? "1.0.0",
|
|
441
|
+
tags: args.tags ?? [],
|
|
442
|
+
signature_base64: signature,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
// deployPackage
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
export async function deployPackage(bridge, tenant, nodeId, packageDescriptor) {
|
|
449
|
+
return await bridge.deployAbilityPackage(tenant, nodeId, packageDescriptor);
|
|
450
|
+
}
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
// Internal generators
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
function generateInvokeScript(resourceUri, endpoint) {
|
|
455
|
+
return `#!/usr/bin/env bash
|
|
456
|
+
set -euo pipefail
|
|
457
|
+
AXON_ENDPOINT="\${AXON_ENDPOINT:-${endpoint}}"
|
|
458
|
+
TENANT="\${AXON_TENANT:-default}"
|
|
459
|
+
RESOURCE_URI="${resourceUri}"
|
|
460
|
+
ARGS="\${1:-{}}"
|
|
461
|
+
curl -sS -X POST "\${AXON_ENDPOINT}/v1/invoke" \\
|
|
462
|
+
-H "Content-Type: application/json" \\
|
|
463
|
+
-d "{\\"tenant_id\\":\\"\${TENANT}\\",\\"resource_uri\\":\\"\${RESOURCE_URI}\\",\\"payload\\":\${ARGS}}"
|
|
464
|
+
`;
|
|
465
|
+
}
|
|
466
|
+
function pushMetadata(lines, version, resourceUri) {
|
|
467
|
+
lines.push("metadata:");
|
|
468
|
+
lines.push(" author: easynet-axon");
|
|
469
|
+
lines.push(` version: "${version}"`);
|
|
470
|
+
lines.push(` axon-resource-uri: "${resourceUri}"`);
|
|
471
|
+
}
|
|
472
|
+
function generateAbilityMd(descriptor, target, token) {
|
|
473
|
+
const lines = [];
|
|
474
|
+
lines.push("---");
|
|
475
|
+
lines.push(`name: ${token}`);
|
|
476
|
+
lines.push(`description: ${descriptor.description}`);
|
|
477
|
+
lines.push("compatibility: Requires network access to Axon runtime");
|
|
478
|
+
pushMetadata(lines, descriptor.version, descriptor.resourceUri);
|
|
479
|
+
if (target === "claude") {
|
|
480
|
+
lines.push("allowed-tools: Bash(*)");
|
|
481
|
+
}
|
|
482
|
+
else if (target === "openclaw") {
|
|
483
|
+
lines.push(" openclaw:");
|
|
484
|
+
lines.push(' emoji: "⚡"');
|
|
485
|
+
lines.push(" requires:");
|
|
486
|
+
lines.push(" network: true");
|
|
487
|
+
lines.push(" command-dispatch: tool");
|
|
488
|
+
}
|
|
489
|
+
lines.push("---");
|
|
490
|
+
lines.push("");
|
|
491
|
+
lines.push(`# ${descriptor.name}`);
|
|
492
|
+
lines.push("");
|
|
493
|
+
lines.push(descriptor.description);
|
|
494
|
+
lines.push("");
|
|
495
|
+
lines.push("## Parameters");
|
|
496
|
+
lines.push("");
|
|
497
|
+
lines.push("| Name | Type | Required | Description |");
|
|
498
|
+
lines.push("|------|------|----------|-------------|");
|
|
499
|
+
const props = (descriptor.inputSchema.properties ?? {});
|
|
500
|
+
const required = (descriptor.inputSchema.required ?? []);
|
|
501
|
+
for (const [name, schema] of Object.entries(props)) {
|
|
502
|
+
const propType = schema.type ?? "string";
|
|
503
|
+
const propDesc = schema.description ?? "";
|
|
504
|
+
const isRequired = required.includes(name) ? "Yes" : "No";
|
|
505
|
+
lines.push(`| ${name} | ${propType} | ${isRequired} | ${propDesc} |`);
|
|
506
|
+
}
|
|
507
|
+
lines.push("");
|
|
508
|
+
lines.push("## Invoke");
|
|
509
|
+
lines.push("");
|
|
510
|
+
lines.push("Run the bundled script with a JSON argument:");
|
|
511
|
+
lines.push("");
|
|
512
|
+
const skillDirVar = target === "claude" ? "CLAUDE_SKILL_DIR"
|
|
513
|
+
: target === "codex" ? "CODEX_SKILL_DIR"
|
|
514
|
+
: "SKILL_DIR";
|
|
515
|
+
lines.push("```bash");
|
|
516
|
+
lines.push(`\${${skillDirVar}}/scripts/invoke.sh '{"param": "value"}'`);
|
|
517
|
+
lines.push("```");
|
|
518
|
+
lines.push("");
|
|
519
|
+
lines.push("## Axon Resource");
|
|
520
|
+
lines.push("");
|
|
521
|
+
lines.push(`- **URI**: \`${descriptor.resourceUri}\``);
|
|
522
|
+
lines.push(`- **Version**: ${descriptor.version}`);
|
|
523
|
+
lines.push("");
|
|
524
|
+
return lines.join("\n");
|
|
525
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class DendriteError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
import type { DendritePublishCapabilityOptions, DendriteInstallCapabilityOptions, DendriteDeployMcpListDirOptions, DendriteUpdateMcpListDirOptions, DendriteUninstallCapabilityOptions } from "./dendrite_bridge/types.js";
|
|
5
|
+
export declare function validatePublishCapabilityRequest(tenantId: string, packageId: string, capabilityName: string, options: DendritePublishCapabilityOptions): void;
|
|
6
|
+
export declare function buildPublishCapabilityPayload(tenantId: string, packageId: string, capabilityName: string, options: DendritePublishCapabilityOptions): Record<string, unknown>;
|
|
7
|
+
export declare function validateInstallCapabilityRequest(tenantId: string, nodeId: string, packageId: string, options: DendriteInstallCapabilityOptions): void;
|
|
8
|
+
export declare function buildInstallCapabilityPayload(tenantId: string, nodeId: string, packageId: string, options: DendriteInstallCapabilityOptions): Record<string, unknown>;
|
|
9
|
+
export declare function validateDeployMcpListDirRequest(tenantId: string, nodeId: string, options?: DendriteDeployMcpListDirOptions): void;
|
|
10
|
+
export declare function buildDeployMcpListDirPayload(tenantId: string, nodeId: string, options?: DendriteDeployMcpListDirOptions): Record<string, unknown>;
|
|
11
|
+
export declare function validateUpdateMcpListDirRequest(tenantId: string, nodeId: string, options?: DendriteUpdateMcpListDirOptions): void;
|
|
12
|
+
export declare function buildUpdateMcpListDirPayload(tenantId: string, nodeId: string, options?: DendriteUpdateMcpListDirOptions): Record<string, unknown>;
|
|
13
|
+
export declare function validateUninstallCapabilityRequest(tenantId: string, nodeId: string, installId: string): void;
|
|
14
|
+
export declare function buildUninstallCapabilityPayload(tenantId: string, nodeId: string, installId: string, options?: DendriteUninstallCapabilityOptions): Record<string, unknown>;
|