@mcoda/mswarm 0.1.81 → 0.1.86

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/dist/runtime.js CHANGED
@@ -3,8 +3,10 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { cpus, freemem, hostname, homedir, loadavg, platform, totalmem, userInfo } from "node:os";
4
4
  import { spawn } from "node:child_process";
5
5
  import { createHash, createHmac, randomUUID } from "node:crypto";
6
+ import { isIP } from "node:net";
7
+ import { GlobalRepository } from "@mcoda/db";
6
8
  import { MswarmCodaliExecutor } from "./codali-executor.js";
7
- import { MSWARM_CAPABILITY_SCHEMA_VERSION, assertMswarmSafeRelativePath, validateMswarmArchiveEntry, buildMswarmCapabilityNames, buildMswarmPrivateCapabilityCatalogEntry, buildMswarmLocalArtifactUri, buildMswarmSandboxProfile, defaultMswarmArtifactAccessPolicy, defaultMswarmArtifactRetentionPolicy, projectMswarmPublicCapabilities, validateMswarmGenericJobRequest } from "@mcoda/shared";
9
+ import { MSWARM_CAPABILITY_SCHEMA_VERSION, CryptoHelper, assertMswarmSafeRelativePath, validateMswarmArchiveEntry, buildMswarmCapabilityNames, buildMswarmPrivateCapabilityCatalogEntry, buildMswarmLocalArtifactUri, buildMswarmSandboxProfile, defaultMswarmArtifactAccessPolicy, defaultMswarmArtifactRetentionPolicy, projectMswarmPublicCapabilities, validateMswarmGenericJobRequest } from "@mcoda/shared";
8
10
  const DEFAULT_GATEWAY_BASE_URL = "http://127.0.0.1:8080";
9
11
  const DEFAULT_SETUP_GATEWAY_BASE_URL = "https://api.mswarm.org";
10
12
  const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434";
@@ -25,7 +27,7 @@ const DEFAULT_MCODA_BIN = "mcoda";
25
27
  const DEFAULT_MCODA_LIST_ARGS = ["agent", "list", "--json", "--refresh-health"];
26
28
  const DEFAULT_COMMAND_MAX_BUFFER = 16 * 1024 * 1024;
27
29
  const DEFAULT_LOCAL_ARTIFACT_MAX_BYTES = 512 * 1024 * 1024;
28
- const DEFAULT_JOB_POLL_WAIT_MS = 25000;
30
+ const DEFAULT_JOB_POLL_WAIT_MS = 2000;
29
31
  const DEFAULT_SELF_HOSTED_JOBS_POLL_PATH = "/v1/swarm/self-hosted/node/jobs/poll";
30
32
  const DEFAULT_SELF_HOSTED_JOBS_START_PATH_TEMPLATE = "/v1/swarm/self-hosted/node/jobs/:jobId/start";
31
33
  const DEFAULT_SELF_HOSTED_JOBS_EVENTS_PATH_TEMPLATE = "/v1/swarm/self-hosted/node/jobs/:jobId/events";
@@ -354,6 +356,90 @@ function parseList(value) {
354
356
  .map((entry) => entry.trim())
355
357
  .filter(Boolean);
356
358
  }
359
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
360
+ const DOMAIN_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
361
+ function normalizeSelfHostedDomainClient(value) {
362
+ const normalized = value.trim().toLowerCase().replace(/\.+$/g, "");
363
+ if (!normalized || normalized.length > 253 || normalized.includes("..")) {
364
+ return null;
365
+ }
366
+ const labels = normalized.split(".");
367
+ if (!labels.every((label) => DOMAIN_LABEL_PATTERN.test(label))) {
368
+ return null;
369
+ }
370
+ return normalized;
371
+ }
372
+ export function normalizeSelfHostedNodeClientIdentity(value) {
373
+ if (isRecord(value)) {
374
+ const kind = optionalText(value.kind)?.toLowerCase();
375
+ const rawValue = optionalText(value.value);
376
+ const normalized = normalizeSelfHostedNodeClientIdentity(rawValue);
377
+ if (!normalized) {
378
+ return null;
379
+ }
380
+ if (kind && kind !== normalized.kind) {
381
+ return null;
382
+ }
383
+ const addedAt = optionalText(value.added_at);
384
+ return addedAt ? { ...normalized, added_at: addedAt } : normalized;
385
+ }
386
+ const raw = optionalText(value);
387
+ if (!raw) {
388
+ return null;
389
+ }
390
+ const trimmed = raw.trim();
391
+ if (isIP(trimmed) !== 0) {
392
+ return { kind: "ip", value: trimmed.toLowerCase() };
393
+ }
394
+ if (UUID_PATTERN.test(trimmed)) {
395
+ return { kind: "uuid", value: trimmed.toLowerCase() };
396
+ }
397
+ const domain = normalizeSelfHostedDomainClient(trimmed);
398
+ if (domain) {
399
+ return { kind: "domain", value: domain };
400
+ }
401
+ return null;
402
+ }
403
+ export function normalizeSelfHostedNodeClientAllowlist(value) {
404
+ const entries = Array.isArray(value)
405
+ ? value
406
+ : typeof value === "string"
407
+ ? parseList(value)
408
+ : value == null || value === false
409
+ ? []
410
+ : [value];
411
+ const byKey = new Map();
412
+ for (const entry of entries) {
413
+ const identity = normalizeSelfHostedNodeClientIdentity(entry);
414
+ if (!identity) {
415
+ throw new Error(typeof entry === "string"
416
+ ? `Invalid mswarm node client identifier: ${entry}`
417
+ : "Invalid mswarm node client identifier");
418
+ }
419
+ byKey.set(`${identity.kind}:${identity.value}`, identity);
420
+ }
421
+ return [...byKey.values()];
422
+ }
423
+ export function addSelfHostedNodeClients(current, additions, now = new Date().toISOString()) {
424
+ const byKey = new Map();
425
+ for (const entry of current) {
426
+ const normalized = normalizeSelfHostedNodeClientIdentity(entry);
427
+ if (normalized) {
428
+ byKey.set(`${normalized.kind}:${normalized.value}`, normalized);
429
+ }
430
+ }
431
+ for (const entry of additions) {
432
+ byKey.set(`${entry.kind}:${entry.value}`, { ...entry, added_at: entry.added_at || now });
433
+ }
434
+ return [...byKey.values()];
435
+ }
436
+ export function removeSelfHostedNodeClients(current, removals) {
437
+ const removeKeys = new Set(removals.map((entry) => `${entry.kind}:${entry.value}`));
438
+ return current
439
+ .map((entry) => normalizeSelfHostedNodeClientIdentity(entry))
440
+ .filter((entry) => Boolean(entry))
441
+ .filter((entry) => !removeKeys.has(`${entry.kind}:${entry.value}`));
442
+ }
357
443
  function parseArgs(value, fallback) {
358
444
  const parsed = parseList(value);
359
445
  return parsed.length > 0 ? parsed : fallback;
@@ -679,6 +765,13 @@ class HttpStatusError extends Error {
679
765
  function isHttpStatusError(error, status) {
680
766
  return error instanceof HttpStatusError && (status === undefined || error.status === status);
681
767
  }
768
+ const RELAY_RESULT_POST_RETRY_DELAYS_MS = [250, 1000, 2500];
769
+ function runtimeErrorMessage(error) {
770
+ return error instanceof Error ? error.message : String(error);
771
+ }
772
+ async function sleep(ms) {
773
+ await new Promise((resolve) => setTimeout(resolve, ms));
774
+ }
682
775
  async function fetchJson(fetchImpl, url, init, timeoutMs) {
683
776
  const controller = new AbortController();
684
777
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -1266,7 +1359,10 @@ export async function readSelfHostedNodeConfig(env = process.env) {
1266
1359
  hardwareTelemetryEnabled: parseBoolean(env.MSWARM_SELF_HOSTED_HARDWARE_TELEMETRY_ENABLED ?? env.MSWARM_SELF_HOSTED_HARDWARE_TELEMETRY, state.hardware_telemetry_enabled === true),
1267
1360
  exposeAllModels: resolveDaemonExposeAllModels(env, state),
1268
1361
  modelAllowlist: parseList(env.MSWARM_SELF_HOSTED_MODEL_ALLOWLIST || state.model_allowlist),
1269
- modelBlocklist: parseList(env.MSWARM_SELF_HOSTED_MODEL_BLOCKLIST || state.model_blocklist)
1362
+ modelBlocklist: parseList(env.MSWARM_SELF_HOSTED_MODEL_BLOCKLIST || state.model_blocklist),
1363
+ clientAllowlist: normalizeSelfHostedNodeClientAllowlist(env.MSWARM_SELF_HOSTED_CLIENT_ALLOWLIST ||
1364
+ env.MSWARM_SELF_HOSTED_CLIENTS ||
1365
+ state.client_allowlist)
1270
1366
  };
1271
1367
  }
1272
1368
  export async function readOwnerSetupConfig(argv = process.argv.slice(3), env = process.env) {
@@ -1293,6 +1389,10 @@ export async function readOwnerSetupConfig(argv = process.argv.slice(3), env = p
1293
1389
  DEFAULT_OLLAMA_BASE_URL;
1294
1390
  const allowlist = parseList(options.allow || env.MSWARM_SELF_HOSTED_MODEL_ALLOWLIST);
1295
1391
  const blocklist = parseList(options.block || env.MSWARM_SELF_HOSTED_MODEL_BLOCKLIST);
1392
+ const clientAllowlist = normalizeSelfHostedNodeClientAllowlist(options.clients ||
1393
+ options.client ||
1394
+ env.MSWARM_SELF_HOSTED_CLIENT_ALLOWLIST ||
1395
+ env.MSWARM_SELF_HOSTED_CLIENTS);
1296
1396
  const packageNodeVersion = await readPackageNodeVersion();
1297
1397
  const maxConcurrentJobs = parsePositiveInteger(options["max-concurrent-jobs"] || env.MSWARM_SELF_HOSTED_MAX_CONCURRENT_JOBS, 1);
1298
1398
  const maxConcurrentLlmJobs = parsePositiveInteger(options["max-concurrent-llm-jobs"] || env.MSWARM_SELF_HOSTED_MAX_CONCURRENT_LLM_JOBS, maxConcurrentJobs);
@@ -1332,6 +1432,7 @@ export async function readOwnerSetupConfig(argv = process.argv.slice(3), env = p
1332
1432
  exposeAllModels: resolveOwnerSetupExposeAllModels(options, env),
1333
1433
  modelAllowlist: allowlist,
1334
1434
  modelBlocklist: blocklist,
1435
+ clientAllowlist,
1335
1436
  start: options.start === true
1336
1437
  };
1337
1438
  }
@@ -1668,6 +1769,20 @@ function configText(config, ...keys) {
1668
1769
  }
1669
1770
  return undefined;
1670
1771
  }
1772
+ async function defaultMcodaAgentAuthResolver(agent) {
1773
+ const agentId = optionalText(agent.id);
1774
+ if (!agentId) {
1775
+ return undefined;
1776
+ }
1777
+ const repo = await GlobalRepository.create();
1778
+ try {
1779
+ const secret = await repo.getAgentAuthSecret(agentId);
1780
+ return secret?.encryptedSecret ? CryptoHelper.decryptSecret(secret.encryptedSecret) : undefined;
1781
+ }
1782
+ finally {
1783
+ await repo.close();
1784
+ }
1785
+ }
1671
1786
  function mcodaAgentDefaultModel(agent) {
1672
1787
  return (optionalText(agent.defaultModel) ||
1673
1788
  optionalText(agent.default_model) ||
@@ -1683,18 +1798,35 @@ function resolveCodaliProviderForAgent(agent) {
1683
1798
  if (["ollama-remote", "ollama-cli", "ollama", "local-model"].includes(adapter || "")) {
1684
1799
  return "ollama-remote";
1685
1800
  }
1686
- if (adapter === "openai" || adapter === "openai-compatible" || adapter === "openai-cli") {
1801
+ if (adapter === "openai" ||
1802
+ adapter === "openai-api" ||
1803
+ adapter === "openai-compatible" ||
1804
+ adapter === "openai-cli") {
1687
1805
  return "openai-compatible";
1688
1806
  }
1689
1807
  if (adapter === "codex-cli")
1690
1808
  return "codex-cli";
1691
1809
  return adapter || undefined;
1692
1810
  }
1811
+ function mcodaAgentRequiresApiKey(agent, mapped) {
1812
+ if (mapped.provider !== "openai-compatible") {
1813
+ return false;
1814
+ }
1815
+ if (mapped.localRunner) {
1816
+ return mapped.authMode === "bearer";
1817
+ }
1818
+ const adapter = optionalText(agent.adapter) || "";
1819
+ if (["openai", "openai-api", "openai-compatible"].includes(adapter)) {
1820
+ return mapped.authMode !== "none";
1821
+ }
1822
+ return mapped.authMode === "bearer";
1823
+ }
1693
1824
  function mapMcodaAgentToCodaliAgent(agent, fallbackSlug) {
1694
1825
  const adapter = optionalText(agent.adapter) || "unknown";
1695
1826
  const model = mcodaAgentDefaultModel(agent) || fallbackSlug;
1696
1827
  const config = agent.config ?? null;
1697
1828
  const localRunner = normalizeMcodaLocalRunnerConfig(adapter, config);
1829
+ const configAuthMode = normalizeLocalRunnerAuthMode(config?.authMode);
1698
1830
  const supportsTools = optionalBoolean(agent.supportsTools, agent.supports_tools, localRunner?.supportsTools) === true;
1699
1831
  return {
1700
1832
  slug: optionalText(agent.slug) || fallbackSlug,
@@ -1709,7 +1841,7 @@ function mapMcodaAgentToCodaliAgent(agent, fallbackSlug) {
1709
1841
  : configText(config, "apiKey", "api_key"),
1710
1842
  localRunner,
1711
1843
  runnerKind: localRunner?.runnerKind,
1712
- authMode: localRunner?.authMode,
1844
+ authMode: localRunner?.authMode ?? configAuthMode,
1713
1845
  dummyBearerToken: localRunner?.dummyBearerToken,
1714
1846
  headers: localRunner?.headers,
1715
1847
  extraBody: localRunner?.extraBody,
@@ -1747,6 +1879,7 @@ const DOCDEX_JOB_ERROR_CODES = new Set([
1747
1879
  ]);
1748
1880
  const PRE_START_JOB_ERROR_CODES = new Set([
1749
1881
  "selected_agent_unavailable",
1882
+ "selected_agent_auth_unavailable",
1750
1883
  "selected_agent_unhealthy",
1751
1884
  "validation_failed",
1752
1885
  "docdex_context_missing",
@@ -3436,6 +3569,7 @@ export class SelfHostedNodeRuntime {
3436
3569
  args: config.mcodaListArgs,
3437
3570
  timeoutMs: config.requestTimeoutMs
3438
3571
  });
3572
+ this.mcodaAgentAuthResolver = deps?.mcodaAgentAuthResolver || defaultMcodaAgentAuthResolver;
3439
3573
  this.mcodaExecutor =
3440
3574
  deps?.mcodaExecutor ||
3441
3575
  new McodaLocalAgentExecutor({
@@ -3626,6 +3760,7 @@ export class SelfHostedNodeRuntime {
3626
3760
  expose_all_models: setupConfig.exposeAllModels,
3627
3761
  model_allowlist: setupConfig.modelAllowlist,
3628
3762
  model_blocklist: setupConfig.modelBlocklist,
3763
+ client_allowlist: setupConfig.clientAllowlist,
3629
3764
  heartbeat_interval_seconds: setupConfig.heartbeatIntervalSeconds,
3630
3765
  max_concurrent_jobs: setupConfig.maxConcurrentJobs,
3631
3766
  max_concurrent_llm_jobs: setupConfig.maxConcurrentLlmJobs,
@@ -3687,7 +3822,8 @@ export class SelfHostedNodeRuntime {
3687
3822
  expose_all_models: setupConfig.exposeAllModels,
3688
3823
  exposure_policy: setupConfig.exposeAllModels ? "all" : "none",
3689
3824
  model_allowlist: setupConfig.modelAllowlist,
3690
- model_blocklist: setupConfig.modelBlocklist
3825
+ model_blocklist: setupConfig.modelBlocklist,
3826
+ client_allowlist: setupConfig.clientAllowlist
3691
3827
  };
3692
3828
  await writeSelfHostedNodeState(setupConfig.statePath, state);
3693
3829
  await writeSelfHostedRuntimeToken(setupConfig.runtimeTokenPath, runtimeToken);
@@ -3740,7 +3876,8 @@ export class SelfHostedNodeRuntime {
3740
3876
  hardwareTelemetryEnabled: setupConfig.hardwareTelemetryEnabled,
3741
3877
  exposeAllModels: setupConfig.exposeAllModels,
3742
3878
  modelAllowlist: setupConfig.modelAllowlist,
3743
- modelBlocklist: setupConfig.modelBlocklist
3879
+ modelBlocklist: setupConfig.modelBlocklist,
3880
+ clientAllowlist: setupConfig.clientAllowlist
3744
3881
  }, { ...deps, gateway: runtimeGateway });
3745
3882
  const once = await runtime.runOnce();
3746
3883
  return {
@@ -3869,7 +4006,8 @@ export class SelfHostedNodeRuntime {
3869
4006
  expose_all_models: this.config.exposeAllModels,
3870
4007
  exposure_policy: this.config.exposeAllModels ? "all" : "none",
3871
4008
  model_allowlist: this.config.modelAllowlist,
3872
- model_blocklist: this.config.modelBlocklist
4009
+ model_blocklist: this.config.modelBlocklist,
4010
+ client_allowlist: this.config.clientAllowlist
3873
4011
  };
3874
4012
  await writeSelfHostedNodeState(this.config.statePath, nextState);
3875
4013
  await writeSelfHostedRuntimeToken(this.config.runtimeTokenPath, runtimeToken);
@@ -3903,7 +4041,21 @@ export class SelfHostedNodeRuntime {
3903
4041
  if (mapped.health_status && mapped.health_status !== "healthy" && mapped.health_status !== "unknown") {
3904
4042
  throw new SelfHostedPreStartJobError("selected_agent_unhealthy", `selected local mcoda agent ${selected} is ${mapped.health_status}`);
3905
4043
  }
3906
- return mapMcodaAgentToCodaliAgent(agent, selected);
4044
+ const codaliAgent = mapMcodaAgentToCodaliAgent(agent, selected);
4045
+ if (!codaliAgent.apiKey && mcodaAgentRequiresApiKey(agent, codaliAgent)) {
4046
+ let apiKey;
4047
+ try {
4048
+ apiKey = optionalText(await this.mcodaAgentAuthResolver(agent)) || undefined;
4049
+ }
4050
+ catch {
4051
+ throw new SelfHostedPreStartJobError("selected_agent_auth_unavailable", `selected local mcoda agent ${selected} auth could not be loaded`);
4052
+ }
4053
+ if (!apiKey) {
4054
+ throw new SelfHostedPreStartJobError("selected_agent_auth_unavailable", `selected local mcoda agent ${selected} requires an API key; run "mcoda agent auth set ${selected}" on this node`);
4055
+ }
4056
+ codaliAgent.apiKey = apiKey;
4057
+ }
4058
+ return codaliAgent;
3907
4059
  }
3908
4060
  async executeGenericJob(envelope, options = {}) {
3909
4061
  const startedAt = Date.now();
@@ -4416,6 +4568,7 @@ export class SelfHostedNodeRuntime {
4416
4568
  model_count: models.length,
4417
4569
  exposed_model_count: exposedModelCount
4418
4570
  },
4571
+ client_allowlist: this.config.clientAllowlist,
4419
4572
  models,
4420
4573
  capabilities: capabilityPayload,
4421
4574
  ...(loadTelemetry.hardware_pressure ? { hardware_pressure: loadTelemetry.hardware_pressure } : {})
@@ -4576,10 +4729,28 @@ export class SelfHostedNodeRuntime {
4576
4729
  }
4577
4730
  : (({ stream_events: _streamEvents, ...rest }) => rest)(result)
4578
4731
  : result;
4579
- try {
4580
- await this.gateway.postJobResult(enrollment.runtimeToken, job.job_id, this.jobResultPayload(postedResult));
4732
+ let resultPostError = null;
4733
+ for (let attempt = 0; attempt <= RELAY_RESULT_POST_RETRY_DELAYS_MS.length; attempt += 1) {
4734
+ try {
4735
+ await this.gateway.postJobResult(enrollment.runtimeToken, job.job_id, this.jobResultPayload(postedResult));
4736
+ resultPostError = null;
4737
+ break;
4738
+ }
4739
+ catch (error) {
4740
+ resultPostError = error;
4741
+ const attemptNumber = attempt + 1;
4742
+ const maxAttempts = RELAY_RESULT_POST_RETRY_DELAYS_MS.length + 1;
4743
+ const retryDelayMs = RELAY_RESULT_POST_RETRY_DELAYS_MS[attempt];
4744
+ if (retryDelayMs === undefined) {
4745
+ break;
4746
+ }
4747
+ console.warn(`[mswarm] self-hosted result post failed for ${job.job_id} ` +
4748
+ `(attempt ${attemptNumber}/${maxAttempts}): ${runtimeErrorMessage(error)}`);
4749
+ await sleep(retryDelayMs);
4750
+ }
4581
4751
  }
4582
- catch (error) {
4752
+ if (resultPostError) {
4753
+ const error = resultPostError;
4583
4754
  const mismatch = this.lifecycleProtocolMismatch("result", error);
4584
4755
  if (mismatch) {
4585
4756
  await this.markLifecycleProtocolDegraded(mismatch);
@@ -4632,7 +4803,9 @@ export class SelfHostedNodeRuntime {
4632
4803
  return;
4633
4804
  polling = true;
4634
4805
  void this.pollAndExecuteJob()
4635
- .catch(() => undefined)
4806
+ .catch((error) => {
4807
+ console.error(`[mswarm] self-hosted relay poll failed: ${runtimeErrorMessage(error)}`);
4808
+ })
4636
4809
  .finally(() => {
4637
4810
  polling = false;
4638
4811
  if (!stopped) {