UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

136 lines (124 loc) • 6.01 kB
import { createMeshHub } from "./hub.ts"; import { createWriteStream, mkdirSync, readFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { resolveConfiguredHubProjectTokens, selectHubProjectTokenMap } from "./hub-identity.ts"; import { DEFAULT_MESSAGE_RETENTION_MS, DEFAULT_MESSAGE_TTL_MS, DEFAULT_PORT, DEFAULT_RATE_LIMIT_MAX, DEFAULT_RATE_LIMIT_WINDOW_MS, } from "./protocol.ts"; import { parseWorkflowDefinitions } from "./workflow.ts"; const host = process.env.KXM_HOST ?? "127.0.0.1"; const port = Number.parseInt(process.env.KXM_PORT ?? String(DEFAULT_PORT), 10); const authToken = process.env.KXM_AUTH_TOKEN; const workspaceDir = resolve(process.env.KXM_WORKSPACE_DIR?.trim() || ".kxm"); const configDir = resolve(process.env.KXM_CONFIG_DIR?.trim() || join(workspaceDir, "config")); const logsDir = resolve(process.env.KXM_LOGS_DIR?.trim() || join(workspaceDir, "logs")); const assetsDir = resolve(process.env.KXM_ASSETS_DIR?.trim() || join(workspaceDir, "assets")); const stateDir = resolve(process.env.KXM_STATE_DIR?.trim() || join(workspaceDir, "state")); const dataPathValue = process.env.KXM_DATA_PATH?.trim(); const dataPath = dataPathValue === ":memory:" ? dataPathValue : resolve(dataPathValue || join(stateDir, "kxm.db")); const logPath = resolve(process.env.KXM_LOG_PATH?.trim() || join(logsDir, "kxm-hub.jsonl")); const messageTtlMs = Number.parseInt(process.env.KXM_MESSAGE_TTL_MS ?? String(DEFAULT_MESSAGE_TTL_MS), 10); const messageRetentionMs = Number.parseInt( process.env.KXM_MESSAGE_RETENTION_MS ?? String(DEFAULT_MESSAGE_RETENTION_MS), 10, ); const rateLimitMax = Number.parseInt(process.env.KXM_RATE_LIMIT_MAX ?? String(DEFAULT_RATE_LIMIT_MAX), 10); import { createLogger } from "./logger.ts"; const rateLimitWindowMs = Number.parseInt( process.env.KXM_RATE_LIMIT_WINDOW_MS ?? String(DEFAULT_RATE_LIMIT_WINDOW_MS), 10, ); for (const directory of [configDir, logsDir, assetsDir, stateDir, dirname(logPath)]) { mkdirSync(directory, { recursive: true }); } const structuredLog = createLogger({ component: "hub", path: logPath, }); function parseProjectTokenEnv(raw: string | undefined): Record<string, string> | undefined { const trimmed = raw?.trim(); if (!trimmed) return undefined; const value = JSON.parse(trimmed) as unknown; if (!value || Array.isArray(value) || typeof value !== "object") { throw new Error("KXM_PROJECT_TOKENS must be a JSON object of project names to tokens"); } const entries = Object.entries(value as Record<string, unknown>); if (entries.some(([project, token]) => !project.trim() || typeof token !== "string" || !token.trim())) { throw new Error("KXM_PROJECT_TOKENS must contain non-empty project names and token strings"); } return Object.fromEntries(entries) as Record<string, string>; } function projectTokens(): Record<string, string> | undefined { const explicit = process.env.KXM_PROJECT_TOKENS_EXPLICIT === "1"; const fromEnv = parseProjectTokenEnv(process.env.KXM_PROJECT_TOKENS); const workdir = resolve(process.env.KXM_WORKDIR?.trim() || process.cwd()); const configured = explicit ? undefined : resolveConfiguredHubProjectTokens(workdir, process.env); const selected = selectHubProjectTokenMap({ explicit: explicit ? fromEnv : undefined, configured, file: explicit ? undefined : fromEnv, }); if (selected.source === "config") { const count = Object.keys(selected.tokens ?? {}).length; process.stderr.write(`kxm hub: project tokens from .kxm/config.yaml (${count} projects, key references resolved, values not saved)\n`); } return selected.tokens; } if (!Number.isInteger(port) || port < 0 || port > 65_535) { throw new Error("KXM_PORT must be an integer between 0 and 65535"); } if (![messageTtlMs, messageRetentionMs, rateLimitMax, rateLimitWindowMs].every(Number.isInteger)) { throw new Error("message TTL and rate limit settings must be integers"); } const configuredProjectTokens = projectTokens(); const inlineWorkflows = process.env.KXM_WEBHOOK_WORKFLOWS?.trim(); const workflowFile = process.env.KXM_WEBHOOK_WORKFLOWS_FILE?.trim(); if (inlineWorkflows && workflowFile) { throw new Error("configure only one of KXM_WEBHOOK_WORKFLOWS or KXM_WEBHOOK_WORKFLOWS_FILE"); } const webhookWorkflows = parseWorkflowDefinitions( workflowFile ? readFileSync(resolve(workflowFile), "utf8") : inlineWorkflows, ); const hub = createMeshHub({ host, port, dataPath, assetsDir, messageTtlMs, messageRetentionMs, rateLimit: { maxRequests: rateLimitMax, windowMs: rateLimitWindowMs }, ...(authToken ? { authToken } : {}), ...(configuredProjectTokens ? { projectTokens: configuredProjectTokens } : {}), ...(webhookWorkflows.length ? { webhookWorkflows } : {}), logger: structuredLog, }); const address = await hub.start(); const auth = authToken ? "token" : "none"; if (auth === "none") { process.stderr.write("kxm hub: auth=none; every loopback caller is trusted. Set KXM_AUTH_TOKEN before relying on this hub.\n"); } structuredLog({ event: "hub_started", url: address.url, workspaceDir, configDir, logsDir, assetsDir, stateDir, dataPath, logPath, auth }); process.stdout.write(`kxm hub listening at ${address.url}; storage=${dataPath}; auth=${auth}\n`); let shutdownPromise: Promise<void> | undefined; function shutdown(signal: string): Promise<void> { if (shutdownPromise) return shutdownPromise; shutdownPromise = (async () => { structuredLog({ event: "hub_stopping", signal }); await hub.close(); structuredLog.close(); process.exit(0); })(); return shutdownPromise; } process.once("SIGINT", () => void shutdown("SIGINT")); process.once("SIGTERM", () => void shutdown("SIGTERM")); process.once("message", (message: unknown) => { if (message && typeof message === "object" && (message as { type?: string }).type === "shutdown") { const signal = (message as { signal?: unknown }).signal; void shutdown(typeof signal === "string" ? signal : "parent"); } });