@kontextmind/kxm
Version:
KXM local-first multi-agent orchestration and operator dashboard
136 lines (124 loc) • 6.01 kB
text/typescript
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");
}
});