@mcoda/mswarm 0.1.0 → 0.1.45
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/LICENSE +21 -0
- package/README.md +112 -8
- package/dist/runtime.d.ts +153 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +701 -30
- package/dist/runtime.js.map +1 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +274 -15
- package/dist/server.js.map +1 -1
- package/package.json +36 -6
package/dist/runtime.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
3
|
+
import { hostname, homedir, platform, userInfo } from "node:os";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
6
|
const DEFAULT_GATEWAY_BASE_URL = "http://127.0.0.1:8080";
|
|
7
|
+
const DEFAULT_SETUP_GATEWAY_BASE_URL = "https://api.mswarm.org";
|
|
6
8
|
const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434";
|
|
7
9
|
const DEFAULT_LISTEN_HOST = "127.0.0.1";
|
|
8
10
|
const DEFAULT_LISTEN_PORT = 18083;
|
|
9
11
|
const DEFAULT_HEARTBEAT_INTERVAL_SECONDS = 30;
|
|
10
|
-
const DEFAULT_REQUEST_TIMEOUT_MS =
|
|
12
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 10000;
|
|
11
13
|
const DEFAULT_MCODA_BIN = "mcoda";
|
|
12
14
|
const DEFAULT_MCODA_LIST_ARGS = ["agent", "list", "--json", "--refresh-health"];
|
|
13
15
|
const DEFAULT_COMMAND_MAX_BUFFER = 16 * 1024 * 1024;
|
|
16
|
+
const DEFAULT_JOB_POLL_WAIT_MS = 25000;
|
|
17
|
+
const SERVICE_LABEL = "com.mcoda.mswarm.self-hosted-node";
|
|
18
|
+
const SYSTEMD_SERVICE_NAME = "mswarm-self-hosted-node.service";
|
|
19
|
+
const WINDOWS_TASK_NAME = "MswarmSelfHostedNode";
|
|
20
|
+
const WINDOWS_WRAPPER_SCRIPT_NAME = "mswarm-self-hosted-node.ps1";
|
|
14
21
|
function optionalText(value) {
|
|
15
22
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
16
23
|
}
|
|
@@ -53,6 +60,48 @@ function parseDiscoveryMode(value) {
|
|
|
53
60
|
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
54
61
|
return normalized === "ollama" ? "ollama" : "mcoda";
|
|
55
62
|
}
|
|
63
|
+
function parseRelayMode(value) {
|
|
64
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
65
|
+
return normalized === "direct" ? "direct" : "outbound";
|
|
66
|
+
}
|
|
67
|
+
function normalizeLocalName(value) {
|
|
68
|
+
return (value
|
|
69
|
+
.trim()
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
72
|
+
.replace(/^-+|-+$/g, "")
|
|
73
|
+
.slice(0, 80) || "local-node");
|
|
74
|
+
}
|
|
75
|
+
export function resolveDefaultServerName() {
|
|
76
|
+
return normalizeLocalName(hostname() || "local-node");
|
|
77
|
+
}
|
|
78
|
+
function defaultMachineIdPath() {
|
|
79
|
+
return join(homedir(), ".mswarm", "self-hosted-node", "machine.id");
|
|
80
|
+
}
|
|
81
|
+
function parseCliOptions(argv) {
|
|
82
|
+
const options = {};
|
|
83
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
84
|
+
const token = argv[index];
|
|
85
|
+
if (!token.startsWith("--")) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const equalsIndex = token.indexOf("=");
|
|
89
|
+
if (equalsIndex > 0) {
|
|
90
|
+
options[token.slice(2, equalsIndex)] = token.slice(equalsIndex + 1);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const key = token.slice(2);
|
|
94
|
+
const next = argv[index + 1];
|
|
95
|
+
if (next && !next.startsWith("--")) {
|
|
96
|
+
options[key] = next;
|
|
97
|
+
index += 1;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
options[key] = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return options;
|
|
104
|
+
}
|
|
56
105
|
function trimTrailingSlash(value) {
|
|
57
106
|
return value.replace(/\/+$/g, "");
|
|
58
107
|
}
|
|
@@ -62,6 +111,26 @@ function defaultStatePath() {
|
|
|
62
111
|
function defaultRuntimeTokenPath() {
|
|
63
112
|
return join(homedir(), ".mswarm", "self-hosted-node", "node.key");
|
|
64
113
|
}
|
|
114
|
+
export async function readOrCreateSelfHostedMachineId(machineIdPath = defaultMachineIdPath()) {
|
|
115
|
+
try {
|
|
116
|
+
const existing = (await readFile(machineIdPath, "utf8")).trim();
|
|
117
|
+
if (existing) {
|
|
118
|
+
return existing;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error.code !== "ENOENT") {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const machineId = randomUUID();
|
|
127
|
+
await mkdir(dirname(machineIdPath), { recursive: true });
|
|
128
|
+
await writeFile(machineIdPath, `${machineId}\n`, { encoding: "utf8", mode: 0o600 });
|
|
129
|
+
return machineId;
|
|
130
|
+
}
|
|
131
|
+
export function machineFingerprintFromId(machineId) {
|
|
132
|
+
return `sha256:${createHash("sha256").update(machineId.trim()).digest("hex")}`;
|
|
133
|
+
}
|
|
65
134
|
function isVisionModel(modelName, family) {
|
|
66
135
|
const normalized = `${modelName} ${family || ""}`.toLowerCase();
|
|
67
136
|
return normalized.includes("llava") || normalized.includes("vision") || normalized.includes("bakllava");
|
|
@@ -198,6 +267,374 @@ export async function writeSelfHostedRuntimeToken(tokenPath, runtimeToken) {
|
|
|
198
267
|
await mkdir(dirname(tokenPath), { recursive: true });
|
|
199
268
|
await writeFile(tokenPath, `${runtimeToken.trim()}\n`, { encoding: "utf8", mode: 0o600 });
|
|
200
269
|
}
|
|
270
|
+
function escapeXml(value) {
|
|
271
|
+
return value
|
|
272
|
+
.replace(/&/g, "&")
|
|
273
|
+
.replace(/</g, "<")
|
|
274
|
+
.replace(/>/g, ">")
|
|
275
|
+
.replace(/"/g, """)
|
|
276
|
+
.replace(/'/g, "'");
|
|
277
|
+
}
|
|
278
|
+
function quoteSystemdValue(value) {
|
|
279
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
280
|
+
}
|
|
281
|
+
function serviceLogDir(homeDir) {
|
|
282
|
+
return join(homeDir, ".mswarm", "self-hosted-node");
|
|
283
|
+
}
|
|
284
|
+
function serviceEnvironment(config, env, homeDir) {
|
|
285
|
+
const values = {
|
|
286
|
+
HOME: env.HOME || homeDir,
|
|
287
|
+
PATH: env.PATH || env.Path || env.path || "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin",
|
|
288
|
+
MSWARM_GATEWAY_BASE_URL: config.gatewayBaseUrl,
|
|
289
|
+
MSWARM_SELF_HOSTED_NODE_STATE_PATH: config.statePath,
|
|
290
|
+
MSWARM_SELF_HOSTED_NODE_KEY_PATH: config.runtimeTokenPath,
|
|
291
|
+
MSWARM_SELF_HOSTED_RELAY_MODE: config.relayMode || "outbound",
|
|
292
|
+
MSWARM_SELF_HOSTED_DIRECT_BASE_URL: config.directBaseUrl || null,
|
|
293
|
+
MSWARM_SELF_HOSTED_DISCOVERY_MODE: config.discoveryMode,
|
|
294
|
+
MSWARM_SELF_HOSTED_MCODA_BIN: config.mcodaBin,
|
|
295
|
+
MSWARM_SELF_HOSTED_MCODA_LIST_ARGS: config.mcodaListArgs.join(","),
|
|
296
|
+
MSWARM_SELF_HOSTED_OLLAMA_BASE_URL: config.ollamaBaseUrl,
|
|
297
|
+
MSWARM_SELF_HOSTED_NODE_VERSION: config.nodeVersion,
|
|
298
|
+
MSWARM_SELF_HOSTED_EXPOSE_ALL_MODELS: config.exposeAllModels ? "true" : "false",
|
|
299
|
+
MSWARM_SELF_HOSTED_MODEL_ALLOWLIST: config.modelAllowlist.join(","),
|
|
300
|
+
MSWARM_SELF_HOSTED_MODEL_BLOCKLIST: config.modelBlocklist.join(","),
|
|
301
|
+
MSWARM_SELF_HOSTED_HEARTBEAT_INTERVAL_SECONDS: String(config.heartbeatIntervalSeconds),
|
|
302
|
+
MSWARM_SELF_HOSTED_REQUEST_TIMEOUT_MS: String(config.requestTimeoutMs)
|
|
303
|
+
};
|
|
304
|
+
return Object.fromEntries(Object.entries(values).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
|
|
305
|
+
}
|
|
306
|
+
function buildLaunchdPlist(input) {
|
|
307
|
+
const env = Object.entries(input.env)
|
|
308
|
+
.map(([key, value]) => ` <key>${escapeXml(key)}</key>\n <string>${escapeXml(value)}</string>`)
|
|
309
|
+
.join("\n");
|
|
310
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
311
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
312
|
+
<plist version="1.0">
|
|
313
|
+
<dict>
|
|
314
|
+
<key>Label</key>
|
|
315
|
+
<string>${escapeXml(input.label)}</string>
|
|
316
|
+
<key>ProgramArguments</key>
|
|
317
|
+
<array>
|
|
318
|
+
<string>${escapeXml(input.nodePath)}</string>
|
|
319
|
+
<string>${escapeXml(input.commandPath)}</string>
|
|
320
|
+
<string>start</string>
|
|
321
|
+
</array>
|
|
322
|
+
<key>EnvironmentVariables</key>
|
|
323
|
+
<dict>
|
|
324
|
+
${env}
|
|
325
|
+
</dict>
|
|
326
|
+
<key>RunAtLoad</key>
|
|
327
|
+
<true/>
|
|
328
|
+
<key>KeepAlive</key>
|
|
329
|
+
<true/>
|
|
330
|
+
<key>StandardOutPath</key>
|
|
331
|
+
<string>${escapeXml(input.logPath)}</string>
|
|
332
|
+
<key>StandardErrorPath</key>
|
|
333
|
+
<string>${escapeXml(input.errorLogPath)}</string>
|
|
334
|
+
</dict>
|
|
335
|
+
</plist>
|
|
336
|
+
`;
|
|
337
|
+
}
|
|
338
|
+
function buildSystemdUserService(input) {
|
|
339
|
+
const env = Object.entries(input.env)
|
|
340
|
+
.map(([key, value]) => `Environment=${quoteSystemdValue(`${key}=${value}`)}`)
|
|
341
|
+
.join("\n");
|
|
342
|
+
return `[Unit]
|
|
343
|
+
Description=mswarm self-hosted node
|
|
344
|
+
After=network-online.target
|
|
345
|
+
Wants=network-online.target
|
|
346
|
+
|
|
347
|
+
[Service]
|
|
348
|
+
Type=simple
|
|
349
|
+
ExecStart=${quoteSystemdValue(input.nodePath)} ${quoteSystemdValue(input.commandPath)} start
|
|
350
|
+
Restart=always
|
|
351
|
+
RestartSec=5
|
|
352
|
+
StandardOutput=append:${input.logPath}
|
|
353
|
+
StandardError=append:${input.errorLogPath}
|
|
354
|
+
${env}
|
|
355
|
+
|
|
356
|
+
[Install]
|
|
357
|
+
WantedBy=default.target
|
|
358
|
+
`;
|
|
359
|
+
}
|
|
360
|
+
function quotePowerShellValue(value) {
|
|
361
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
362
|
+
}
|
|
363
|
+
function quoteWindowsCommandArg(value) {
|
|
364
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
365
|
+
}
|
|
366
|
+
function buildWindowsTaskWrapperScript(input) {
|
|
367
|
+
const env = Object.entries(input.env)
|
|
368
|
+
.map(([key, value]) => `$env:${key} = ${quotePowerShellValue(value)}`)
|
|
369
|
+
.join("\n");
|
|
370
|
+
return `$ErrorActionPreference = 'Continue'
|
|
371
|
+
|
|
372
|
+
$logPath = ${quotePowerShellValue(input.logPath)}
|
|
373
|
+
$errorLogPath = ${quotePowerShellValue(input.errorLogPath)}
|
|
374
|
+
$nodePath = ${quotePowerShellValue(input.nodePath)}
|
|
375
|
+
$commandArguments = @(${quotePowerShellValue(input.commandPath)}, 'start')
|
|
376
|
+
|
|
377
|
+
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $logPath) | Out-Null
|
|
378
|
+
|
|
379
|
+
${env}
|
|
380
|
+
|
|
381
|
+
while ($true) {
|
|
382
|
+
$startedAt = Get-Date -Format o
|
|
383
|
+
Add-Content -Path $logPath -Value "[$startedAt] starting mswarm self-hosted node"
|
|
384
|
+
try {
|
|
385
|
+
& $nodePath @commandArguments >> $logPath 2>> $errorLogPath
|
|
386
|
+
$exitCode = if ($null -eq $LASTEXITCODE) { 0 } else { $LASTEXITCODE }
|
|
387
|
+
$endedAt = Get-Date -Format o
|
|
388
|
+
Add-Content -Path $errorLogPath -Value "[$endedAt] mswarm self-hosted node exited with code $exitCode; restarting in 5 seconds"
|
|
389
|
+
} catch {
|
|
390
|
+
$failedAt = Get-Date -Format o
|
|
391
|
+
Add-Content -Path $errorLogPath -Value "[$failedAt] mswarm self-hosted node wrapper error: $_"
|
|
392
|
+
}
|
|
393
|
+
Start-Sleep -Seconds 5
|
|
394
|
+
}
|
|
395
|
+
`;
|
|
396
|
+
}
|
|
397
|
+
function buildWindowsTaskRegistrationCommand(wrapperPath) {
|
|
398
|
+
const actionArguments = `-NoProfile -ExecutionPolicy Bypass -File ${quoteWindowsCommandArg(wrapperPath)}`;
|
|
399
|
+
const settings = "New-ScheduledTaskSettingsSet " +
|
|
400
|
+
"-AllowStartIfOnBatteries " +
|
|
401
|
+
"-DontStopIfGoingOnBatteries " +
|
|
402
|
+
"-StartWhenAvailable " +
|
|
403
|
+
"-MultipleInstances IgnoreNew " +
|
|
404
|
+
"-RestartCount 999 " +
|
|
405
|
+
"-RestartInterval (New-TimeSpan -Minutes 1) " +
|
|
406
|
+
"-ExecutionTimeLimit (New-TimeSpan -Seconds 0)";
|
|
407
|
+
return [
|
|
408
|
+
`Stop-ScheduledTask -TaskName ${quotePowerShellValue(WINDOWS_TASK_NAME)} -ErrorAction SilentlyContinue`,
|
|
409
|
+
`$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument ${quotePowerShellValue(actionArguments)}`,
|
|
410
|
+
"$trigger = New-ScheduledTaskTrigger -AtLogOn",
|
|
411
|
+
`$settings = ${settings}`,
|
|
412
|
+
`Register-ScheduledTask -TaskName ${quotePowerShellValue(WINDOWS_TASK_NAME)} -Action $action -Trigger $trigger -Settings $settings -Description 'mswarm self-hosted node daemon' -Force | Out-Null`
|
|
413
|
+
].join("; ");
|
|
414
|
+
}
|
|
415
|
+
function windowsTaskCommand(command) {
|
|
416
|
+
return `${command} -TaskName ${quotePowerShellValue(WINDOWS_TASK_NAME)}`;
|
|
417
|
+
}
|
|
418
|
+
function launchdDomain() {
|
|
419
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : userInfo().uid;
|
|
420
|
+
return `gui/${uid}`;
|
|
421
|
+
}
|
|
422
|
+
export function resolveSelfHostedNodeServiceLayout(input = {}) {
|
|
423
|
+
const targetPlatform = input.platform || platform();
|
|
424
|
+
const homeDir = input.homeDir || homedir();
|
|
425
|
+
const logDir = serviceLogDir(homeDir);
|
|
426
|
+
const logPath = join(logDir, "daemon.log");
|
|
427
|
+
const errorLogPath = join(logDir, "daemon.err.log");
|
|
428
|
+
if (targetPlatform === "darwin") {
|
|
429
|
+
return {
|
|
430
|
+
platform: targetPlatform,
|
|
431
|
+
manager: "launchd",
|
|
432
|
+
serviceName: SERVICE_LABEL,
|
|
433
|
+
servicePath: join(homeDir, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`),
|
|
434
|
+
logPath,
|
|
435
|
+
errorLogPath
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
if (targetPlatform === "linux") {
|
|
439
|
+
return {
|
|
440
|
+
platform: targetPlatform,
|
|
441
|
+
manager: "systemd",
|
|
442
|
+
serviceName: SYSTEMD_SERVICE_NAME,
|
|
443
|
+
servicePath: join(homeDir, ".config", "systemd", "user", SYSTEMD_SERVICE_NAME),
|
|
444
|
+
logPath,
|
|
445
|
+
errorLogPath
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
if (targetPlatform === "win32") {
|
|
449
|
+
return {
|
|
450
|
+
platform: targetPlatform,
|
|
451
|
+
manager: "windows-task-scheduler",
|
|
452
|
+
serviceName: WINDOWS_TASK_NAME,
|
|
453
|
+
servicePath: join(logDir, WINDOWS_WRAPPER_SCRIPT_NAME),
|
|
454
|
+
logPath,
|
|
455
|
+
errorLogPath
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
throw new Error(`Persistent service control is not supported on ${targetPlatform}`);
|
|
459
|
+
}
|
|
460
|
+
function serviceControlResult(layout, action, result, ok = true, message) {
|
|
461
|
+
return {
|
|
462
|
+
manager: layout.manager,
|
|
463
|
+
serviceName: layout.serviceName,
|
|
464
|
+
servicePath: layout.servicePath,
|
|
465
|
+
logPath: layout.logPath,
|
|
466
|
+
errorLogPath: layout.errorLogPath,
|
|
467
|
+
action,
|
|
468
|
+
ok,
|
|
469
|
+
stdout: result.stdout,
|
|
470
|
+
stderr: result.stderr,
|
|
471
|
+
...(message ? { message } : {})
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
async function runServiceCommand(runner, command, args, timeoutMs) {
|
|
475
|
+
return runner(command, args, { timeoutMs, maxBuffer: DEFAULT_COMMAND_MAX_BUFFER });
|
|
476
|
+
}
|
|
477
|
+
export async function installSelfHostedNodeService(config, options) {
|
|
478
|
+
const homeDir = options.homeDir || homedir();
|
|
479
|
+
const layout = resolveSelfHostedNodeServiceLayout({ platform: options.platform, homeDir });
|
|
480
|
+
const logDir = serviceLogDir(homeDir);
|
|
481
|
+
const env = serviceEnvironment(config, options.env || process.env, homeDir);
|
|
482
|
+
const nodePath = options.nodePath || process.execPath;
|
|
483
|
+
const runner = options.runner || defaultCommandRunner;
|
|
484
|
+
await mkdir(logDir, { recursive: true });
|
|
485
|
+
if (layout.platform === "darwin") {
|
|
486
|
+
await mkdir(dirname(layout.servicePath), { recursive: true });
|
|
487
|
+
await writeFile(layout.servicePath, buildLaunchdPlist({
|
|
488
|
+
label: SERVICE_LABEL,
|
|
489
|
+
nodePath,
|
|
490
|
+
commandPath: options.commandPath,
|
|
491
|
+
logPath: layout.logPath,
|
|
492
|
+
errorLogPath: layout.errorLogPath,
|
|
493
|
+
env
|
|
494
|
+
}), "utf8");
|
|
495
|
+
const domain = launchdDomain();
|
|
496
|
+
await runner("launchctl", ["bootout", `${domain}/${SERVICE_LABEL}`], {
|
|
497
|
+
timeoutMs: config.requestTimeoutMs,
|
|
498
|
+
maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
|
|
499
|
+
}).catch(() => undefined);
|
|
500
|
+
await runner("launchctl", ["bootstrap", domain, layout.servicePath], {
|
|
501
|
+
timeoutMs: config.requestTimeoutMs,
|
|
502
|
+
maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
|
|
503
|
+
});
|
|
504
|
+
await runner("launchctl", ["enable", `${domain}/${SERVICE_LABEL}`], {
|
|
505
|
+
timeoutMs: config.requestTimeoutMs,
|
|
506
|
+
maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
|
|
507
|
+
}).catch(() => undefined);
|
|
508
|
+
await runner("launchctl", ["kickstart", "-k", `${domain}/${SERVICE_LABEL}`], {
|
|
509
|
+
timeoutMs: config.requestTimeoutMs,
|
|
510
|
+
maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
|
|
511
|
+
});
|
|
512
|
+
return { ...layout, started: true };
|
|
513
|
+
}
|
|
514
|
+
if (layout.platform === "linux") {
|
|
515
|
+
await mkdir(dirname(layout.servicePath), { recursive: true });
|
|
516
|
+
await writeFile(layout.servicePath, buildSystemdUserService({
|
|
517
|
+
nodePath,
|
|
518
|
+
commandPath: options.commandPath,
|
|
519
|
+
logPath: layout.logPath,
|
|
520
|
+
errorLogPath: layout.errorLogPath,
|
|
521
|
+
env
|
|
522
|
+
}), "utf8");
|
|
523
|
+
await runner("systemctl", ["--user", "daemon-reload"], {
|
|
524
|
+
timeoutMs: config.requestTimeoutMs,
|
|
525
|
+
maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
|
|
526
|
+
});
|
|
527
|
+
await runner("systemctl", ["--user", "enable", SYSTEMD_SERVICE_NAME], {
|
|
528
|
+
timeoutMs: config.requestTimeoutMs,
|
|
529
|
+
maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
|
|
530
|
+
});
|
|
531
|
+
await runner("loginctl", ["enable-linger", options.env?.USER || options.env?.USERNAME || userInfo().username], {
|
|
532
|
+
timeoutMs: config.requestTimeoutMs,
|
|
533
|
+
maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
|
|
534
|
+
}).catch(() => undefined);
|
|
535
|
+
await runner("systemctl", ["--user", "restart", SYSTEMD_SERVICE_NAME], {
|
|
536
|
+
timeoutMs: config.requestTimeoutMs,
|
|
537
|
+
maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
|
|
538
|
+
});
|
|
539
|
+
return { ...layout, started: true };
|
|
540
|
+
}
|
|
541
|
+
if (layout.platform === "win32") {
|
|
542
|
+
await writeFile(layout.servicePath, buildWindowsTaskWrapperScript({
|
|
543
|
+
nodePath,
|
|
544
|
+
commandPath: options.commandPath,
|
|
545
|
+
logPath: layout.logPath,
|
|
546
|
+
errorLogPath: layout.errorLogPath,
|
|
547
|
+
env
|
|
548
|
+
}), "utf8");
|
|
549
|
+
await runner("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", buildWindowsTaskRegistrationCommand(layout.servicePath)], {
|
|
550
|
+
timeoutMs: config.requestTimeoutMs,
|
|
551
|
+
maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
|
|
552
|
+
});
|
|
553
|
+
await runner("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", windowsTaskCommand("Start-ScheduledTask")], {
|
|
554
|
+
timeoutMs: config.requestTimeoutMs,
|
|
555
|
+
maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
|
|
556
|
+
});
|
|
557
|
+
return { ...layout, started: true };
|
|
558
|
+
}
|
|
559
|
+
throw new Error(`Persistent service install is not supported on ${layout.platform}`);
|
|
560
|
+
}
|
|
561
|
+
export async function controlSelfHostedNodeService(action, options = {}) {
|
|
562
|
+
const layout = resolveSelfHostedNodeServiceLayout(options);
|
|
563
|
+
const runner = options.runner || defaultCommandRunner;
|
|
564
|
+
const timeoutMs = options.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS;
|
|
565
|
+
try {
|
|
566
|
+
if (layout.manager === "launchd") {
|
|
567
|
+
const domain = launchdDomain();
|
|
568
|
+
const serviceTarget = `${domain}/${SERVICE_LABEL}`;
|
|
569
|
+
if (action === "stop") {
|
|
570
|
+
const result = await runServiceCommand(runner, "launchctl", ["bootout", serviceTarget], timeoutMs);
|
|
571
|
+
return serviceControlResult(layout, action, result);
|
|
572
|
+
}
|
|
573
|
+
if (action === "status") {
|
|
574
|
+
const result = await runServiceCommand(runner, "launchctl", ["print", serviceTarget], timeoutMs);
|
|
575
|
+
return serviceControlResult(layout, action, result);
|
|
576
|
+
}
|
|
577
|
+
if (action === "restart") {
|
|
578
|
+
await runServiceCommand(runner, "launchctl", ["bootout", serviceTarget], timeoutMs).catch(() => undefined);
|
|
579
|
+
}
|
|
580
|
+
await runServiceCommand(runner, "launchctl", ["bootstrap", domain, layout.servicePath], timeoutMs).catch(() => undefined);
|
|
581
|
+
await runServiceCommand(runner, "launchctl", ["enable", serviceTarget], timeoutMs).catch(() => undefined);
|
|
582
|
+
const result = await runServiceCommand(runner, "launchctl", ["kickstart", "-k", serviceTarget], timeoutMs);
|
|
583
|
+
return serviceControlResult(layout, action, result);
|
|
584
|
+
}
|
|
585
|
+
if (layout.manager === "systemd") {
|
|
586
|
+
const systemdAction = action === "status" ? "status" : action;
|
|
587
|
+
const args = action === "status"
|
|
588
|
+
? ["--user", "status", "--no-pager", SYSTEMD_SERVICE_NAME]
|
|
589
|
+
: ["--user", systemdAction, SYSTEMD_SERVICE_NAME];
|
|
590
|
+
const result = await runServiceCommand(runner, "systemctl", args, timeoutMs);
|
|
591
|
+
return serviceControlResult(layout, action, result);
|
|
592
|
+
}
|
|
593
|
+
if (layout.manager === "windows-task-scheduler") {
|
|
594
|
+
const command = action === "status"
|
|
595
|
+
? [
|
|
596
|
+
`$task = Get-ScheduledTask -TaskName ${quotePowerShellValue(WINDOWS_TASK_NAME)} -ErrorAction Stop`,
|
|
597
|
+
`$info = Get-ScheduledTaskInfo -TaskName ${quotePowerShellValue(WINDOWS_TASK_NAME)} -ErrorAction Stop`,
|
|
598
|
+
"[pscustomobject]@{TaskName=$task.TaskName;State=$task.State;LastRunTime=$info.LastRunTime;LastTaskResult=$info.LastTaskResult} | ConvertTo-Json -Compress"
|
|
599
|
+
].join("; ")
|
|
600
|
+
: action === "restart"
|
|
601
|
+
? `${windowsTaskCommand("Stop-ScheduledTask")} -ErrorAction SilentlyContinue; ${windowsTaskCommand("Start-ScheduledTask")}`
|
|
602
|
+
: windowsTaskCommand(action === "start" ? "Start-ScheduledTask" : "Stop-ScheduledTask");
|
|
603
|
+
const result = await runServiceCommand(runner, "powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command], timeoutMs);
|
|
604
|
+
return serviceControlResult(layout, action, result);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
if (action === "status") {
|
|
609
|
+
return serviceControlResult(layout, action, { stdout: "", stderr: "" }, false, error instanceof Error ? error.message : String(error));
|
|
610
|
+
}
|
|
611
|
+
throw error;
|
|
612
|
+
}
|
|
613
|
+
throw new Error(`Persistent service control is not supported on ${layout.platform}`);
|
|
614
|
+
}
|
|
615
|
+
export async function uninstallSelfHostedNodeService(options = {}) {
|
|
616
|
+
const layout = resolveSelfHostedNodeServiceLayout(options);
|
|
617
|
+
const runner = options.runner || defaultCommandRunner;
|
|
618
|
+
const timeoutMs = options.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS;
|
|
619
|
+
if (layout.manager === "launchd") {
|
|
620
|
+
await runServiceCommand(runner, "launchctl", ["bootout", `${launchdDomain()}/${SERVICE_LABEL}`], timeoutMs).catch(() => undefined);
|
|
621
|
+
await rm(layout.servicePath, { force: true });
|
|
622
|
+
return serviceControlResult(layout, "uninstall", { stdout: "", stderr: "" });
|
|
623
|
+
}
|
|
624
|
+
if (layout.manager === "systemd") {
|
|
625
|
+
await runServiceCommand(runner, "systemctl", ["--user", "disable", "--now", SYSTEMD_SERVICE_NAME], timeoutMs).catch(() => undefined);
|
|
626
|
+
await rm(layout.servicePath, { force: true });
|
|
627
|
+
await runServiceCommand(runner, "systemctl", ["--user", "daemon-reload"], timeoutMs).catch(() => undefined);
|
|
628
|
+
return serviceControlResult(layout, "uninstall", { stdout: "", stderr: "" });
|
|
629
|
+
}
|
|
630
|
+
if (layout.manager === "windows-task-scheduler") {
|
|
631
|
+
const command = `${windowsTaskCommand("Stop-ScheduledTask")} -ErrorAction SilentlyContinue; Unregister-ScheduledTask -TaskName ${quotePowerShellValue(WINDOWS_TASK_NAME)} -Confirm:$false -ErrorAction SilentlyContinue`;
|
|
632
|
+
await runServiceCommand(runner, "powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command], timeoutMs).catch(() => undefined);
|
|
633
|
+
await rm(layout.servicePath, { force: true });
|
|
634
|
+
return serviceControlResult(layout, "uninstall", { stdout: "", stderr: "" });
|
|
635
|
+
}
|
|
636
|
+
throw new Error(`Persistent service uninstall is not supported on ${layout.platform}`);
|
|
637
|
+
}
|
|
201
638
|
export async function readSelfHostedNodeConfig(env = process.env) {
|
|
202
639
|
const statePath = optionalText(env.MSWARM_SELF_HOSTED_NODE_STATE_PATH) || defaultStatePath();
|
|
203
640
|
const runtimeTokenPath = optionalText(env.MSWARM_SELF_HOSTED_NODE_KEY_PATH) || defaultRuntimeTokenPath();
|
|
@@ -215,11 +652,15 @@ export async function readSelfHostedNodeConfig(env = process.env) {
|
|
|
215
652
|
return {
|
|
216
653
|
gatewayBaseUrl: trimTrailingSlash(gatewayBaseUrl),
|
|
217
654
|
nodeId,
|
|
655
|
+
serverName: optionalText(env.MSWARM_SELF_HOSTED_SERVER_NAME) || state.server_name || null,
|
|
656
|
+
relayMode: parseRelayMode(env.MSWARM_SELF_HOSTED_RELAY_MODE || state.relay_mode),
|
|
657
|
+
machineFingerprint: state.machine_fingerprint || null,
|
|
658
|
+
directBaseUrl: optionalText(env.MSWARM_SELF_HOSTED_DIRECT_BASE_URL) || state.direct_base_url || null,
|
|
218
659
|
enrollmentToken: optionalText(env.MSWARM_SELF_HOSTED_ENROLLMENT_TOKEN),
|
|
219
660
|
runtimeToken: optionalText(env.MSWARM_SELF_HOSTED_RUNTIME_TOKEN) || persistedRuntimeToken || state.runtime_token || null,
|
|
220
|
-
discoveryMode: parseDiscoveryMode(env.MSWARM_SELF_HOSTED_DISCOVERY_MODE),
|
|
221
|
-
mcodaBin: optionalText(env.MSWARM_SELF_HOSTED_MCODA_BIN) || DEFAULT_MCODA_BIN,
|
|
222
|
-
mcodaListArgs: parseArgs(env.MSWARM_SELF_HOSTED_MCODA_LIST_ARGS, DEFAULT_MCODA_LIST_ARGS),
|
|
661
|
+
discoveryMode: parseDiscoveryMode(env.MSWARM_SELF_HOSTED_DISCOVERY_MODE || state.discovery_mode),
|
|
662
|
+
mcodaBin: optionalText(env.MSWARM_SELF_HOSTED_MCODA_BIN) || state.mcoda_bin || DEFAULT_MCODA_BIN,
|
|
663
|
+
mcodaListArgs: parseArgs(env.MSWARM_SELF_HOSTED_MCODA_LIST_ARGS || state.mcoda_list_args, DEFAULT_MCODA_LIST_ARGS),
|
|
223
664
|
ollamaBaseUrl: trimTrailingSlash(ollamaBaseUrl),
|
|
224
665
|
statePath,
|
|
225
666
|
runtimeTokenPath,
|
|
@@ -227,12 +668,60 @@ export async function readSelfHostedNodeConfig(env = process.env) {
|
|
|
227
668
|
optionalText(env.MSWARM_SELF_HOSTED_RELAY_SIGNING_SECRET),
|
|
228
669
|
listenHost: optionalText(env.MSWARM_SELF_HOSTED_LISTEN_HOST) || DEFAULT_LISTEN_HOST,
|
|
229
670
|
listenPort: parsePositiveInteger(env.MSWARM_SELF_HOSTED_LISTEN_PORT, DEFAULT_LISTEN_PORT),
|
|
230
|
-
nodeVersion: optionalText(env.MSWARM_SELF_HOSTED_NODE_VERSION) || "0.1.
|
|
671
|
+
nodeVersion: optionalText(env.MSWARM_SELF_HOSTED_NODE_VERSION) || state.node_version || "0.1.1",
|
|
231
672
|
heartbeatIntervalSeconds: parsePositiveInteger(env.MSWARM_SELF_HOSTED_HEARTBEAT_INTERVAL_SECONDS, state.heartbeat_interval_seconds || DEFAULT_HEARTBEAT_INTERVAL_SECONDS),
|
|
673
|
+
requestTimeoutMs: parsePositiveInteger(env.MSWARM_SELF_HOSTED_REQUEST_TIMEOUT_MS, state.request_timeout_ms || DEFAULT_REQUEST_TIMEOUT_MS),
|
|
674
|
+
exposeAllModels: parseBoolean(env.MSWARM_SELF_HOSTED_EXPOSE_ALL_MODELS, typeof state.expose_all_models === "boolean" ? state.expose_all_models : false),
|
|
675
|
+
modelAllowlist: parseList(env.MSWARM_SELF_HOSTED_MODEL_ALLOWLIST || state.model_allowlist),
|
|
676
|
+
modelBlocklist: parseList(env.MSWARM_SELF_HOSTED_MODEL_BLOCKLIST || state.model_blocklist)
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
export async function readOwnerSetupConfig(argv = process.argv.slice(3), env = process.env) {
|
|
680
|
+
const options = parseCliOptions(argv);
|
|
681
|
+
const statePath = optionalText(env.MSWARM_SELF_HOSTED_NODE_STATE_PATH) || defaultStatePath();
|
|
682
|
+
const runtimeTokenPath = optionalText(env.MSWARM_SELF_HOSTED_NODE_KEY_PATH) || defaultRuntimeTokenPath();
|
|
683
|
+
const gatewayBaseUrl = optionalText(options.gateway) ||
|
|
684
|
+
optionalText(env.MSWARM_GATEWAY_BASE_URL) ||
|
|
685
|
+
DEFAULT_SETUP_GATEWAY_BASE_URL;
|
|
686
|
+
const apiKey = optionalText(options["api-key"]) || optionalText(env.MSWARM_API_KEY) || "";
|
|
687
|
+
if (!apiKey) {
|
|
688
|
+
throw new Error("--api-key or MSWARM_API_KEY is required");
|
|
689
|
+
}
|
|
690
|
+
const relayMode = parseRelayMode(options.mode || env.MSWARM_SELF_HOSTED_RELAY_MODE);
|
|
691
|
+
const directBaseUrl = optionalText(options["direct-url"]) || optionalText(env.MSWARM_SELF_HOSTED_DIRECT_BASE_URL);
|
|
692
|
+
if (relayMode === "direct" && !directBaseUrl) {
|
|
693
|
+
throw new Error("--direct-url is required when --mode direct is used");
|
|
694
|
+
}
|
|
695
|
+
if (relayMode === "outbound" && directBaseUrl) {
|
|
696
|
+
throw new Error("--direct-url can only be used with --mode direct");
|
|
697
|
+
}
|
|
698
|
+
const ollamaBaseUrl = optionalText(env.MSWARM_SELF_HOSTED_OLLAMA_BASE_URL) ||
|
|
699
|
+
optionalText(env.OLLAMA_HOST) ||
|
|
700
|
+
DEFAULT_OLLAMA_BASE_URL;
|
|
701
|
+
const allowlist = parseList(options.allow || env.MSWARM_SELF_HOSTED_MODEL_ALLOWLIST);
|
|
702
|
+
const blocklist = parseList(options.block || env.MSWARM_SELF_HOSTED_MODEL_BLOCKLIST);
|
|
703
|
+
return {
|
|
704
|
+
apiKey,
|
|
705
|
+
gatewayBaseUrl: trimTrailingSlash(gatewayBaseUrl),
|
|
706
|
+
serverName: normalizeLocalName(optionalText(options["server-name"]) ||
|
|
707
|
+
optionalText(env.MSWARM_SELF_HOSTED_SERVER_NAME) ||
|
|
708
|
+
resolveDefaultServerName()),
|
|
709
|
+
relayMode,
|
|
710
|
+
directBaseUrl,
|
|
711
|
+
discoveryMode: parseDiscoveryMode(env.MSWARM_SELF_HOSTED_DISCOVERY_MODE),
|
|
712
|
+
statePath,
|
|
713
|
+
runtimeTokenPath,
|
|
714
|
+
machineIdPath: optionalText(env.MSWARM_SELF_HOSTED_MACHINE_ID_PATH) || defaultMachineIdPath(),
|
|
715
|
+
mcodaBin: optionalText(env.MSWARM_SELF_HOSTED_MCODA_BIN) || DEFAULT_MCODA_BIN,
|
|
716
|
+
mcodaListArgs: parseArgs(env.MSWARM_SELF_HOSTED_MCODA_LIST_ARGS, DEFAULT_MCODA_LIST_ARGS),
|
|
717
|
+
ollamaBaseUrl: trimTrailingSlash(ollamaBaseUrl),
|
|
718
|
+
nodeVersion: optionalText(env.MSWARM_SELF_HOSTED_NODE_VERSION) || "0.1.1",
|
|
719
|
+
heartbeatIntervalSeconds: parsePositiveInteger(env.MSWARM_SELF_HOSTED_HEARTBEAT_INTERVAL_SECONDS, DEFAULT_HEARTBEAT_INTERVAL_SECONDS),
|
|
232
720
|
requestTimeoutMs: parsePositiveInteger(env.MSWARM_SELF_HOSTED_REQUEST_TIMEOUT_MS, DEFAULT_REQUEST_TIMEOUT_MS),
|
|
233
|
-
exposeAllModels: parseBoolean(env.MSWARM_SELF_HOSTED_EXPOSE_ALL_MODELS, false),
|
|
234
|
-
modelAllowlist:
|
|
235
|
-
modelBlocklist:
|
|
721
|
+
exposeAllModels: options["expose-all"] === true || parseBoolean(env.MSWARM_SELF_HOSTED_EXPOSE_ALL_MODELS, false),
|
|
722
|
+
modelAllowlist: allowlist,
|
|
723
|
+
modelBlocklist: blocklist,
|
|
724
|
+
start: options.start === true
|
|
236
725
|
};
|
|
237
726
|
}
|
|
238
727
|
export function mapOllamaModelToSelfHostedModel(model, config) {
|
|
@@ -380,10 +869,6 @@ async function defaultCommandRunner(command, args, options) {
|
|
|
380
869
|
});
|
|
381
870
|
}
|
|
382
871
|
export class McodaAgentInventoryClient {
|
|
383
|
-
command;
|
|
384
|
-
args;
|
|
385
|
-
timeoutMs;
|
|
386
|
-
runner;
|
|
387
872
|
constructor(input) {
|
|
388
873
|
this.command = input.command || DEFAULT_MCODA_BIN;
|
|
389
874
|
this.args = input.args?.length ? input.args : DEFAULT_MCODA_LIST_ARGS;
|
|
@@ -413,9 +898,6 @@ export class McodaAgentInventoryClient {
|
|
|
413
898
|
}
|
|
414
899
|
}
|
|
415
900
|
export class OllamaClient {
|
|
416
|
-
baseUrl;
|
|
417
|
-
fetchImpl;
|
|
418
|
-
timeoutMs;
|
|
419
901
|
constructor(input) {
|
|
420
902
|
this.baseUrl = trimTrailingSlash(input.baseUrl);
|
|
421
903
|
this.fetchImpl = input.fetchImpl || fetch;
|
|
@@ -502,9 +984,6 @@ function buildOpenAIChatCompletion(input) {
|
|
|
502
984
|
};
|
|
503
985
|
}
|
|
504
986
|
export class McodaLocalAgentExecutor {
|
|
505
|
-
command;
|
|
506
|
-
timeoutMs;
|
|
507
|
-
runner;
|
|
508
987
|
constructor(input) {
|
|
509
988
|
this.command = input.command || DEFAULT_MCODA_BIN;
|
|
510
989
|
this.timeoutMs = input.timeoutMs || DEFAULT_REQUEST_TIMEOUT_MS;
|
|
@@ -534,9 +1013,6 @@ export class McodaLocalAgentExecutor {
|
|
|
534
1013
|
}
|
|
535
1014
|
}
|
|
536
1015
|
export class MswarmSelfHostedNodeClient {
|
|
537
|
-
gatewayBaseUrl;
|
|
538
|
-
fetchImpl;
|
|
539
|
-
timeoutMs;
|
|
540
1016
|
constructor(input) {
|
|
541
1017
|
this.gatewayBaseUrl = trimTrailingSlash(input.gatewayBaseUrl);
|
|
542
1018
|
this.fetchImpl = input.fetchImpl || fetch;
|
|
@@ -549,6 +1025,19 @@ export class MswarmSelfHostedNodeClient {
|
|
|
549
1025
|
body: JSON.stringify({ node_id: nodeId, enrollment_token: enrollmentToken })
|
|
550
1026
|
}, this.timeoutMs);
|
|
551
1027
|
}
|
|
1028
|
+
async bootstrap(apiKey, payload) {
|
|
1029
|
+
return fetchJson(this.fetchImpl, `${this.gatewayBaseUrl}/v1/swarm/self-hosted/node/bootstrap`, {
|
|
1030
|
+
method: "POST",
|
|
1031
|
+
headers: {
|
|
1032
|
+
"content-type": "application/json",
|
|
1033
|
+
"x-api-key": apiKey
|
|
1034
|
+
},
|
|
1035
|
+
body: JSON.stringify(payload)
|
|
1036
|
+
}, this.timeoutMs);
|
|
1037
|
+
}
|
|
1038
|
+
async health() {
|
|
1039
|
+
return fetchJson(this.fetchImpl, `${this.gatewayBaseUrl}/healthz`, { method: "GET" }, this.timeoutMs);
|
|
1040
|
+
}
|
|
552
1041
|
async heartbeat(runtimeToken, payload) {
|
|
553
1042
|
return fetchJson(this.fetchImpl, `${this.gatewayBaseUrl}/v1/swarm/self-hosted/node/heartbeat`, {
|
|
554
1043
|
method: "POST",
|
|
@@ -569,13 +1058,28 @@ export class MswarmSelfHostedNodeClient {
|
|
|
569
1058
|
body: JSON.stringify(payload)
|
|
570
1059
|
}, this.timeoutMs);
|
|
571
1060
|
}
|
|
1061
|
+
async pollJob(runtimeToken, payload) {
|
|
1062
|
+
return fetchJson(this.fetchImpl, `${this.gatewayBaseUrl}/v1/swarm/self-hosted/node/jobs/poll`, {
|
|
1063
|
+
method: "POST",
|
|
1064
|
+
headers: {
|
|
1065
|
+
"content-type": "application/json",
|
|
1066
|
+
authorization: `Bearer ${runtimeToken}`
|
|
1067
|
+
},
|
|
1068
|
+
body: JSON.stringify(payload)
|
|
1069
|
+
}, Math.max(this.timeoutMs, (payload.wait_ms || 0) + 5000));
|
|
1070
|
+
}
|
|
1071
|
+
async postJobResult(runtimeToken, jobId, payload) {
|
|
1072
|
+
return fetchJson(this.fetchImpl, `${this.gatewayBaseUrl}/v1/swarm/self-hosted/node/jobs/${encodeURIComponent(jobId)}/result`, {
|
|
1073
|
+
method: "POST",
|
|
1074
|
+
headers: {
|
|
1075
|
+
"content-type": "application/json",
|
|
1076
|
+
authorization: `Bearer ${runtimeToken}`
|
|
1077
|
+
},
|
|
1078
|
+
body: JSON.stringify(payload)
|
|
1079
|
+
}, this.timeoutMs);
|
|
1080
|
+
}
|
|
572
1081
|
}
|
|
573
1082
|
export class SelfHostedNodeRuntime {
|
|
574
|
-
config;
|
|
575
|
-
gateway;
|
|
576
|
-
mcoda;
|
|
577
|
-
mcodaExecutor;
|
|
578
|
-
ollama;
|
|
579
1083
|
constructor(config, deps) {
|
|
580
1084
|
this.config = config;
|
|
581
1085
|
this.gateway =
|
|
@@ -606,6 +1110,96 @@ export class SelfHostedNodeRuntime {
|
|
|
606
1110
|
timeoutMs: config.requestTimeoutMs
|
|
607
1111
|
});
|
|
608
1112
|
}
|
|
1113
|
+
static async setup(setupConfig, deps) {
|
|
1114
|
+
const gateway = deps?.gateway ||
|
|
1115
|
+
new MswarmSelfHostedNodeClient({
|
|
1116
|
+
gatewayBaseUrl: setupConfig.gatewayBaseUrl,
|
|
1117
|
+
fetchImpl: deps?.fetchImpl,
|
|
1118
|
+
timeoutMs: setupConfig.requestTimeoutMs
|
|
1119
|
+
});
|
|
1120
|
+
const machineId = await readOrCreateSelfHostedMachineId(setupConfig.machineIdPath);
|
|
1121
|
+
const machineFingerprint = machineFingerprintFromId(machineId);
|
|
1122
|
+
const bootstrap = await gateway.bootstrap(setupConfig.apiKey, {
|
|
1123
|
+
machine_fingerprint: machineFingerprint,
|
|
1124
|
+
server_name: setupConfig.serverName,
|
|
1125
|
+
label: setupConfig.serverName,
|
|
1126
|
+
relay_mode: setupConfig.relayMode,
|
|
1127
|
+
direct_base_url: setupConfig.directBaseUrl || null,
|
|
1128
|
+
node_version: setupConfig.nodeVersion,
|
|
1129
|
+
discovery_mode: setupConfig.discoveryMode,
|
|
1130
|
+
expose_all_models: setupConfig.exposeAllModels,
|
|
1131
|
+
model_allowlist: setupConfig.modelAllowlist,
|
|
1132
|
+
model_blocklist: setupConfig.modelBlocklist,
|
|
1133
|
+
heartbeat_interval_seconds: setupConfig.heartbeatIntervalSeconds
|
|
1134
|
+
});
|
|
1135
|
+
const nodeId = optionalText(bootstrap.node?.node_id);
|
|
1136
|
+
const runtimeToken = optionalText(bootstrap.runtime_token);
|
|
1137
|
+
if (!nodeId || !runtimeToken) {
|
|
1138
|
+
throw new Error("Bootstrap response did not include node_id and runtime_token");
|
|
1139
|
+
}
|
|
1140
|
+
const heartbeatInterval = bootstrap.heartbeat_interval_seconds || setupConfig.heartbeatIntervalSeconds;
|
|
1141
|
+
const state = {
|
|
1142
|
+
node_id: nodeId,
|
|
1143
|
+
server_name: optionalText(bootstrap.node?.server_name) || setupConfig.serverName,
|
|
1144
|
+
relay_mode: bootstrap.node?.relay_mode || setupConfig.relayMode,
|
|
1145
|
+
machine_fingerprint: machineFingerprint,
|
|
1146
|
+
direct_base_url: setupConfig.directBaseUrl || null,
|
|
1147
|
+
runtime_token: undefined,
|
|
1148
|
+
config_version: bootstrap.config_version,
|
|
1149
|
+
heartbeat_interval_seconds: heartbeatInterval,
|
|
1150
|
+
heartbeat_timeout_seconds: bootstrap.heartbeat_timeout_seconds,
|
|
1151
|
+
enrolled_at: new Date().toISOString(),
|
|
1152
|
+
updated_at: new Date().toISOString(),
|
|
1153
|
+
gateway_base_url: setupConfig.gatewayBaseUrl,
|
|
1154
|
+
ollama_base_url: setupConfig.ollamaBaseUrl,
|
|
1155
|
+
discovery_mode: setupConfig.discoveryMode,
|
|
1156
|
+
mcoda_bin: setupConfig.mcodaBin,
|
|
1157
|
+
mcoda_list_args: setupConfig.mcodaListArgs,
|
|
1158
|
+
node_version: setupConfig.nodeVersion,
|
|
1159
|
+
request_timeout_ms: setupConfig.requestTimeoutMs,
|
|
1160
|
+
expose_all_models: setupConfig.exposeAllModels,
|
|
1161
|
+
model_allowlist: setupConfig.modelAllowlist,
|
|
1162
|
+
model_blocklist: setupConfig.modelBlocklist
|
|
1163
|
+
};
|
|
1164
|
+
await writeSelfHostedNodeState(setupConfig.statePath, state);
|
|
1165
|
+
await writeSelfHostedRuntimeToken(setupConfig.runtimeTokenPath, runtimeToken);
|
|
1166
|
+
const runtime = new SelfHostedNodeRuntime({
|
|
1167
|
+
gatewayBaseUrl: setupConfig.gatewayBaseUrl,
|
|
1168
|
+
nodeId,
|
|
1169
|
+
serverName: state.server_name,
|
|
1170
|
+
relayMode: state.relay_mode || setupConfig.relayMode,
|
|
1171
|
+
machineFingerprint,
|
|
1172
|
+
directBaseUrl: setupConfig.directBaseUrl || null,
|
|
1173
|
+
enrollmentToken: null,
|
|
1174
|
+
runtimeToken,
|
|
1175
|
+
discoveryMode: setupConfig.discoveryMode,
|
|
1176
|
+
mcodaBin: setupConfig.mcodaBin,
|
|
1177
|
+
mcodaListArgs: setupConfig.mcodaListArgs,
|
|
1178
|
+
ollamaBaseUrl: setupConfig.ollamaBaseUrl,
|
|
1179
|
+
statePath: setupConfig.statePath,
|
|
1180
|
+
runtimeTokenPath: setupConfig.runtimeTokenPath,
|
|
1181
|
+
invocationSigningSecret: null,
|
|
1182
|
+
listenHost: DEFAULT_LISTEN_HOST,
|
|
1183
|
+
listenPort: DEFAULT_LISTEN_PORT,
|
|
1184
|
+
nodeVersion: setupConfig.nodeVersion,
|
|
1185
|
+
heartbeatIntervalSeconds: heartbeatInterval,
|
|
1186
|
+
requestTimeoutMs: setupConfig.requestTimeoutMs,
|
|
1187
|
+
exposeAllModels: setupConfig.exposeAllModels,
|
|
1188
|
+
modelAllowlist: setupConfig.modelAllowlist,
|
|
1189
|
+
modelBlocklist: setupConfig.modelBlocklist
|
|
1190
|
+
}, { ...deps, gateway });
|
|
1191
|
+
const once = await runtime.runOnce();
|
|
1192
|
+
return {
|
|
1193
|
+
created: bootstrap.created === true,
|
|
1194
|
+
nodeId,
|
|
1195
|
+
serverName: state.server_name || setupConfig.serverName,
|
|
1196
|
+
modelCount: once.model_count,
|
|
1197
|
+
status: once.status,
|
|
1198
|
+
statePath: setupConfig.statePath,
|
|
1199
|
+
runtimeTokenPath: setupConfig.runtimeTokenPath,
|
|
1200
|
+
start: setupConfig.start
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
609
1203
|
async discoverModels() {
|
|
610
1204
|
if (this.config.discoveryMode === "ollama") {
|
|
611
1205
|
const [version, models] = await Promise.all([
|
|
@@ -642,7 +1236,15 @@ export class SelfHostedNodeRuntime {
|
|
|
642
1236
|
enrolled_at: currentState.enrolled_at || new Date().toISOString(),
|
|
643
1237
|
updated_at: new Date().toISOString(),
|
|
644
1238
|
gateway_base_url: this.config.gatewayBaseUrl,
|
|
645
|
-
ollama_base_url: this.config.ollamaBaseUrl
|
|
1239
|
+
ollama_base_url: this.config.ollamaBaseUrl,
|
|
1240
|
+
discovery_mode: this.config.discoveryMode,
|
|
1241
|
+
mcoda_bin: this.config.mcodaBin,
|
|
1242
|
+
mcoda_list_args: this.config.mcodaListArgs,
|
|
1243
|
+
node_version: this.config.nodeVersion,
|
|
1244
|
+
request_timeout_ms: this.config.requestTimeoutMs,
|
|
1245
|
+
expose_all_models: this.config.exposeAllModels,
|
|
1246
|
+
model_allowlist: this.config.modelAllowlist,
|
|
1247
|
+
model_blocklist: this.config.modelBlocklist
|
|
646
1248
|
};
|
|
647
1249
|
await writeSelfHostedNodeState(this.config.statePath, nextState);
|
|
648
1250
|
await writeSelfHostedRuntimeToken(this.config.runtimeTokenPath, runtimeToken);
|
|
@@ -814,9 +1416,75 @@ export class SelfHostedNodeRuntime {
|
|
|
814
1416
|
});
|
|
815
1417
|
return { count: models.length, response };
|
|
816
1418
|
}
|
|
1419
|
+
async pollAndExecuteJob(waitMs = DEFAULT_JOB_POLL_WAIT_MS) {
|
|
1420
|
+
const enrollment = await this.ensureEnrolled();
|
|
1421
|
+
const response = await this.gateway.pollJob(enrollment.runtimeToken, {
|
|
1422
|
+
node_id: this.config.nodeId,
|
|
1423
|
+
capacity: { active_jobs: 0, max_jobs: 1 },
|
|
1424
|
+
wait_ms: waitMs
|
|
1425
|
+
});
|
|
1426
|
+
const job = response.job || null;
|
|
1427
|
+
if (!job) {
|
|
1428
|
+
return { executed: false };
|
|
1429
|
+
}
|
|
1430
|
+
const result = await this.executeJob(job);
|
|
1431
|
+
await this.gateway.postJobResult(enrollment.runtimeToken, job.job_id, {
|
|
1432
|
+
...result,
|
|
1433
|
+
node_id: this.config.nodeId
|
|
1434
|
+
});
|
|
1435
|
+
return { executed: true, job_id: job.job_id, status: result.status };
|
|
1436
|
+
}
|
|
1437
|
+
async doctor() {
|
|
1438
|
+
const checks = [];
|
|
1439
|
+
checks.push({ name: "config", ok: Boolean(this.config.nodeId), message: this.config.nodeId || "missing node id" });
|
|
1440
|
+
const runtimeToken = this.config.runtimeToken || (await readSelfHostedRuntimeToken(this.config.runtimeTokenPath));
|
|
1441
|
+
checks.push({ name: "runtime_token", ok: Boolean(runtimeToken), message: runtimeToken ? "present" : "missing" });
|
|
1442
|
+
try {
|
|
1443
|
+
await this.gateway.health();
|
|
1444
|
+
checks.push({ name: "gateway_health", ok: true });
|
|
1445
|
+
}
|
|
1446
|
+
catch (error) {
|
|
1447
|
+
checks.push({
|
|
1448
|
+
name: "gateway_health",
|
|
1449
|
+
ok: false,
|
|
1450
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
try {
|
|
1454
|
+
const once = await this.runOnce();
|
|
1455
|
+
checks.push({ name: "heartbeat", ok: once.status === "online", message: once.status });
|
|
1456
|
+
checks.push({
|
|
1457
|
+
name: "local_agents",
|
|
1458
|
+
ok: once.model_count > 0,
|
|
1459
|
+
message: `${once.model_count} exposed local agent(s)`
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
catch (error) {
|
|
1463
|
+
checks.push({
|
|
1464
|
+
name: "heartbeat",
|
|
1465
|
+
ok: false,
|
|
1466
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
return { ok: checks.every((check) => check.ok), checks };
|
|
1470
|
+
}
|
|
817
1471
|
startDaemon() {
|
|
818
1472
|
let stopped = false;
|
|
819
1473
|
let timer = null;
|
|
1474
|
+
let polling = false;
|
|
1475
|
+
const poll = () => {
|
|
1476
|
+
if (stopped || polling || this.config.relayMode === "direct")
|
|
1477
|
+
return;
|
|
1478
|
+
polling = true;
|
|
1479
|
+
void this.pollAndExecuteJob()
|
|
1480
|
+
.catch(() => undefined)
|
|
1481
|
+
.finally(() => {
|
|
1482
|
+
polling = false;
|
|
1483
|
+
if (!stopped) {
|
|
1484
|
+
setTimeout(poll, 0);
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
};
|
|
820
1488
|
const schedule = () => {
|
|
821
1489
|
if (stopped)
|
|
822
1490
|
return;
|
|
@@ -828,7 +1496,10 @@ export class SelfHostedNodeRuntime {
|
|
|
828
1496
|
};
|
|
829
1497
|
void this.runOnce()
|
|
830
1498
|
.catch(() => undefined)
|
|
831
|
-
.finally(
|
|
1499
|
+
.finally(() => {
|
|
1500
|
+
schedule();
|
|
1501
|
+
poll();
|
|
1502
|
+
});
|
|
832
1503
|
return {
|
|
833
1504
|
stop: () => {
|
|
834
1505
|
stopped = true;
|