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