import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { pathToFileURL } from 'node:url'; import type { MatrixActor } from '@open-matrix/core/core/MatrixActor.js'; import { RequestReply } from '@open-matrix/core/engine/remoting/RequestReply.js'; import { MatrixRuntime } from '@open-matrix/core/runtime/MatrixRuntime.js'; import { NatsTransport } from '@open-matrix/core/transport/NatsTransport.js'; import { connect as natsConnect, jwtAuthenticator, type NatsConnection } from 'nats'; import { loadMatrixServiceManifest, type ILoadedMatrixServiceManifest } from '../utils/service-manifest.js'; import { createRunnerSecurityRealm } from '../utils/runner-security.js'; export interface IActorRunCommandOptions { readonly natsUrl?: string; readonly root?: string; readonly space?: string; readonly jwt?: string; readonly seed?: string; readonly home?: string; readonly export?: string; readonly mount?: string; readonly json?: boolean; readonly props?: string; readonly checkOp?: string; readonly checkPayload?: string; } export interface IBrokerRunConfig { readonly natsUrl: string; readonly root: string; readonly jwt?: string; readonly seed?: string; readonly checked: readonly string[]; } type ActorConstructor = { new (): MatrixActor; readonly accepts?: Record; readonly emits?: Record; readonly subscribes?: Record; readonly name: string; }; type PackageBootstrapFunction = (...args: readonly unknown[]) => Promise | unknown; export interface IPackageRunCommandOptions extends IActorRunCommandOptions { readonly entry?: string; readonly overrides?: string; } export interface IPackageRunSpec { readonly packageDir: string; readonly packageName: string; readonly manifestPath: string; readonly entryPath: string; readonly exportName: string; readonly mount: string; readonly actorId: string; readonly packageId: string; readonly runtimeId: string; readonly accepts: Record; readonly emits: Record; readonly subscribes: Record; readonly overrides: Record; readonly serviceInstance: ILoadedMatrixServiceManifest['serviceInstance']; } export async function actorRunCommand( entry: string, options: IActorRunCommandOptions, ): Promise { const entryPath = path.resolve(entry); if (!fs.existsSync(entryPath)) { throw new Error(`Actor entry not found: ${entryPath}`); } const exportName = options.export?.trim() || 'default'; const ActorClass = await loadActorConstructor(entryPath, exportName); const mount = options.mount?.trim() || inferMountFromEntry(entryPath); if (!mount) { throw new Error('matrix actor run requires --mount when it cannot infer a mount from the entry file'); } const broker = resolveActorRunBroker(options); const connection = await connectActorNats(broker); const transport = new NatsTransport(connection, { root: broker.root }); const runtime = new MatrixRuntime({ transport, logging: false, allowDynamicCompilation: false, securityRealm: createRunnerSecurityRealm(), }); const props = parseProps(options.props); await runtime.createSupervised(ActorClass, mount, props); await transport.flush(); const check = await runOptionalActorCheck(runtime, mount, options); const result = { ok: true, mode: 'standalone-actor-runner', entry: entryPath, export: exportName, actor: ActorClass.name, mount, root: broker.root, effectiveMount: `${broker.root}.${mount}`, natsUrl: broker.natsUrl, authMode: broker.jwt ? 'jwt' : 'none', hostStateWritten: false, ...(check ? { check } : {}), }; if (options.json) { console.log(JSON.stringify(result, null, 2)); } else { console.log('Actor mounted:'); console.log(` logical: ${result.mount}`); console.log(` root: ${result.root}`); console.log(` effective: ${result.effectiveMount}`); console.log(` broker: ${result.natsUrl}`); console.log('Press Ctrl+C to stop.'); } if (check) { await runtime.shutdown(); await transport.disconnect(); if (!connection.isClosed()) { await connection.close(); } return; } await waitForActorShutdown(async () => { await runtime.shutdown(); await transport.disconnect(); if (!connection.isClosed()) { await connection.close(); } }); } export async function packageRunCommand( packageDir: string, options: IPackageRunCommandOptions, ): Promise { const spec = resolvePackageRunSpec(packageDir, options); const broker = resolveActorRunBroker(options); const connection = await connectActorNats(broker); const transport = new NatsTransport(connection, { root: broker.root }); const runtime = new MatrixRuntime({ transport, logging: false, allowDynamicCompilation: false, securityRealm: createRunnerSecurityRealm(), }); const bootstrap = await loadPackageBootstrap(spec.entryPath, spec.exportName); const bootstrapResult = await bootstrap( spec.overrides, { runtime }, undefined, { packageName: spec.packageName, packageDir: spec.packageDir, manifestPath: spec.manifestPath, entryPath: spec.entryPath, exportName: spec.exportName, root: broker.root, mount: spec.mount, instance: spec.serviceInstance, serviceInstance: spec.serviceInstance, }, ); await transport.flush(); const check = await runOptionalActorCheck(runtime, spec.mount, options); const result = { ok: true, mode: 'standalone-package-runner', packageDir: spec.packageDir, packageName: spec.packageName, manifest: spec.manifestPath, entry: spec.entryPath, export: spec.exportName, mount: spec.mount, root: broker.root, effectiveMount: `${broker.root}.${spec.mount}`, natsUrl: broker.natsUrl, authMode: broker.jwt ? 'jwt' : 'none', hostStateWritten: false, ...(check ? { check } : {}), }; if (options.json) { console.log(JSON.stringify(result, null, 2)); } else { console.log('Package actor mounted:'); console.log(` package: ${result.packageName}`); console.log(` logical: ${result.mount}`); console.log(` root: ${result.root}`); console.log(` effective: ${result.effectiveMount}`); console.log(` broker: ${result.natsUrl}`); console.log('Press Ctrl+C to stop.'); } if (check) { await shutdownPackageBootstrapResult(bootstrapResult, runtime); await runtime.shutdown(); await transport.disconnect(); if (!connection.isClosed()) { await connection.close(); } return; } await waitForActorShutdown(async () => { await shutdownPackageBootstrapResult(bootstrapResult, runtime); await runtime.shutdown(); await transport.disconnect(); if (!connection.isClosed()) { await connection.close(); } }); } export function resolveActorRunBroker(options: IActorRunCommandOptions): IBrokerRunConfig { const candidateHomes = resolveCandidateHomes(options.home); const files = candidateHomes.flatMap((home) => [ { label: `${formatHomeLabel(home)}/credentials/matrix-broker.json`, value: readBrokerCredentialFile(path.join(home, 'credentials', 'matrix-broker.json')), }, { label: `${formatHomeLabel(home)}/credentials/matrix-nats.json`, value: readBrokerCredentialFile(path.join(home, 'credentials', 'matrix-nats.json')), }, { label: `${formatHomeLabel(home)}/config.json`, value: readBrokerConfigFile(path.join(home, 'config.json')), }, ]); const natsUrl = readOptionalString(options.natsUrl) ?? readOptionalString(process.env.MATRIX_NATS_URL) ?? readOptionalString(process.env.NATS_URL) ?? firstConfigured(files, 'natsUrl'); const root = readOptionalString(options.root) ?? readOptionalString(options.space) ?? readOptionalString(process.env.MATRIX_ROOT) ?? readOptionalString(process.env.MATRIX_SPACE) ?? firstConfigured(files, 'root') ?? firstConfigured(files, 'space'); const jwt = readOptionalString(options.jwt) ?? readOptionalString(process.env.MATRIX_NATS_JWT) ?? readOptionalString(process.env.NATS_JWT) ?? firstConfigured(files, 'jwt'); const seed = readOptionalString(options.seed) ?? readOptionalString(process.env.MATRIX_NATS_SEED) ?? readOptionalString(process.env.NATS_SEED) ?? firstConfigured(files, 'seed'); const checked = [ '--nats-url', '--root', '--space', '--jwt', '--seed', 'MATRIX_NATS_URL', 'MATRIX_ROOT', 'MATRIX_SPACE', 'MATRIX_NATS_JWT', 'MATRIX_NATS_SEED', 'NATS_URL', 'NATS_JWT', 'NATS_SEED', ...files.map((entry) => entry.label), ]; if (!natsUrl) { throw new Error(`MATRIX_NATS_URL_REQUIRED: matrix actor/package run requires --nats-url or configured broker URL. Checked: ${checked.join(', ')}`); } if (!root) { throw new Error('MATRIX_ROOT_REQUIRED: matrix actor/package run requires --root, --space, MATRIX_ROOT, MATRIX_SPACE, or configured root'); } if ((jwt && !seed) || (!jwt && seed)) { throw new Error('MATRIX_NATS_JWT_SEED_PAIR_REQUIRED: provide both JWT and seed, or neither for an unauthenticated local broker'); } return { natsUrl, root, ...(jwt ? { jwt } : {}), ...(seed ? { seed } : {}), checked, }; } export function resolvePackageRunSpec(packageDir: string, options: IPackageRunCommandOptions): IPackageRunSpec { const loaded = loadMatrixServiceManifest(packageDir); const entryPath = options.entry ? resolvePackageEntryPath(loaded.packageDir, options.entry) : loaded.entryPath; const exportName = options.export?.trim() || loaded.exportName; const mount = options.mount?.trim() || loaded.mount; if (!mount) { throw new Error('matrix package run requires --mount when the package descriptor does not declare one'); } const binding = resolvePackageBindingForMount(loaded, mount); const componentType = binding?.type ?? loaded.serviceInstance.componentType ?? loaded.serviceInstance.class; const actorId = componentType || mount; const runtimeId = `standalone-package:${process.pid}:${sanitizeRuntimeIdPart(loaded.packageName)}:${sanitizeRuntimeIdPart(mount)}`; return { packageDir: loaded.packageDir, packageName: loaded.packageName, manifestPath: loaded.manifestPath, entryPath, exportName, mount, actorId, packageId: loaded.packageName, runtimeId, accepts: acceptsArrayToRecord(binding?.accepts ?? []), emits: {}, subscribes: {}, overrides: parseOverrides(options.overrides), serviceInstance: loaded.serviceInstance, }; } async function connectActorNats(input: IBrokerRunConfig): Promise { const encoder = new TextEncoder(); return await natsConnect({ servers: [input.natsUrl], name: `matrix-actor-run-${input.root}-${process.pid}`, timeout: 5_000, ...(input.jwt && input.seed ? { authenticator: jwtAuthenticator( () => input.jwt!, () => encoder.encode(input.seed!), ), } : {}), }); } export async function loadActorConstructor(entryPath: string, exportName: string): Promise { const moduleUrl = `${pathToFileURL(entryPath).href}?matrix_actor_run=${Date.now()}-${Math.random().toString(16).slice(2)}`; const mod = await import(moduleUrl) as Record; const requested = actorConstructorFromExport(mod[exportName]); if (requested) { return requested; } const available = Object.entries(mod) .filter(([, value]) => actorConstructorFromExport(value)) .map(([name]) => name); throw new Error(`No actor class found as export "${exportName}". Available exports: ${available.join(', ') || '(none)'}`); } async function loadPackageBootstrap(entryPath: string, exportName: string): Promise { const moduleUrl = `${pathToFileURL(entryPath).href}?matrix_package_run=${Date.now()}-${Math.random().toString(16).slice(2)}`; const mod = await import(moduleUrl) as Record; const bootstrap = mod[exportName]; if (typeof bootstrap !== 'function') { throw new Error(`Package run export "${exportName}" was not found in ${entryPath}`); } return bootstrap as PackageBootstrapFunction; } function actorConstructorFromExport(value: unknown): ActorConstructor | null { if (typeof value !== 'function') { return null; } const candidate = value as ActorConstructor; return candidate.prototype ? candidate : null; } function inferMountFromEntry(entryPath: string): string { return path.basename(entryPath, path.extname(entryPath)) .replace(/Actor$/i, '') .replace(/([a-z0-9])([A-Z])/g, '$1-$2') .toLowerCase() .replace(/[^a-z0-9._-]+/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); } function parseProps(value: string | undefined): Record | undefined { const text = value?.trim(); if (!text) { return undefined; } const parsed = JSON.parse(text) as unknown; if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { throw new Error('matrix actor run --props must be a JSON object'); } return parsed as Record; } function parseOverrides(value: string | undefined): Record { const text = value?.trim(); if (!text) { return {}; } const parsed = JSON.parse(text) as unknown; if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { throw new Error('matrix package run --overrides must be a JSON object'); } return parsed as Record; } async function runOptionalActorCheck( runtime: MatrixRuntime, mount: string, options: IActorRunCommandOptions, ): Promise<{ readonly op: string; readonly result: unknown } | null> { const op = options.checkOp?.trim(); if (!op) { return null; } const payload = parseCheckPayload(options.checkPayload); const result = await RequestReply.execute, unknown>( runtime.getRootContext(), mount, op, payload, { timeoutMs: 5_000 }, ); return { op, result }; } function parseCheckPayload(value: string | undefined): Record { const text = value?.trim(); if (!text) { return {}; } const parsed = JSON.parse(text) as unknown; if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { throw new Error('matrix actor/package run --check-payload must be a JSON object'); } return parsed as Record; } function resolvePackageEntryPath(packageDir: string, entry: string): string { const trimmed = entry.trim(); if (!trimmed) { throw new Error('matrix package run --entry must be a non-empty path'); } const entryPath = path.resolve(packageDir, trimmed); if (!fs.existsSync(entryPath)) { throw new Error(`matrix package run entry does not exist: ${trimmed}`); } return entryPath; } function resolvePackageBindingForMount( loaded: ILoadedMatrixServiceManifest, mount: string, ): ILoadedMatrixServiceManifest['packageComponents'][number] | ILoadedMatrixServiceManifest['packageRoot'] | undefined { if (loaded.packageRoot?.mount === mount) { return loaded.packageRoot; } return loaded.packageComponents.find((entry) => entry.mount === mount); } function acceptsArrayToRecord(accepts: readonly string[]): Record { const result: Record = {}; for (const op of accepts) { result[op] = {}; } return result; } function sanitizeRuntimeIdPart(value: string): string { return value .replace(/^@/u, '') .replace(/[^a-zA-Z0-9._-]+/gu, '-') .replace(/-+/gu, '-') .replace(/^-|-$/gu, '') || 'package'; } async function shutdownPackageBootstrapResult(value: unknown, ownedRuntime: MatrixRuntime): Promise { if (!value || typeof value !== 'object') { return; } const runtime = (value as { readonly runtime?: unknown }).runtime; if (!runtime || typeof runtime !== 'object') { return; } if (runtime === ownedRuntime) { return; } const shutdown = (runtime as { readonly shutdown?: unknown }).shutdown; if (typeof shutdown === 'function') { await shutdown.call(runtime); } } function waitForActorShutdown(onShutdown: () => Promise): Promise { return new Promise((resolve, reject) => { let settled = false; const finish = (error?: unknown): void => { if (settled) return; settled = true; process.off('SIGINT', shutdown); process.off('SIGTERM', shutdown); if (error) { reject(error); return; } resolve(); }; const shutdown = (): void => { void onShutdown().then(() => finish()).catch((error) => finish(error)); }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); }); } function resolveCandidateHomes(homeOption: string | undefined): readonly string[] { const homes = [ homeOption ? path.resolve(homeOption) : undefined, path.resolve(process.cwd(), '.matrix'), path.resolve(os.homedir(), '.matrix'), ].filter((value): value is string => Boolean(value)); return [...new Set(homes)]; } function formatHomeLabel(home: string): string { const defaultHome = path.resolve(os.homedir(), '.matrix'); const cwdHome = path.resolve(process.cwd(), '.matrix'); if (home === defaultHome) return '~/.matrix'; if (home === cwdHome) return './.matrix'; return home; } function readBrokerCredentialFile(filePath: string): Record { const value = readJsonFile(filePath); return normalizeBrokerConfig(value); } function readBrokerConfigFile(filePath: string): Record { const value = readJsonFile(filePath); const nested = [ value, value?.broker, value?.nats, value?.transport, value?.credentials, ].filter((entry): entry is Record => Boolean(entry && typeof entry === 'object' && !Array.isArray(entry))); return nested.reduce>((result, entry) => ({ ...result, ...normalizeBrokerConfig(entry), }), {}); } function normalizeBrokerConfig(value: Record | null): Record { if (!value) { return {}; } return { ...(readOptionalString(value.natsUrl) ?? readOptionalString(value.url) ?? readOptionalString(value.brokerUrl) ? { natsUrl: (readOptionalString(value.natsUrl) ?? readOptionalString(value.url) ?? readOptionalString(value.brokerUrl))! } : {}), ...(readOptionalString(value.root) ?? readOptionalString(value.space) ?? readOptionalString(value.authorityRoot) ? { root: (readOptionalString(value.root) ?? readOptionalString(value.space) ?? readOptionalString(value.authorityRoot))! } : {}), ...(readOptionalString(value.space) ? { space: readOptionalString(value.space)! } : {}), ...(readOptionalString(value.jwt) ? { jwt: readOptionalString(value.jwt)! } : {}), ...(readOptionalString(value.seed) ? { seed: readOptionalString(value.seed)! } : {}), }; } function firstConfigured( files: readonly { readonly value: Record }[], key: 'natsUrl' | 'root' | 'space' | 'jwt' | 'seed', ): string | undefined { for (const file of files) { const value = file.value[key]; if (value) { return value; } } return undefined; } function readJsonFile(filePath: string): Record | null { if (!fs.existsSync(filePath)) { return null; } try { const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown; return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record : null; } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error(`Invalid JSON in ${filePath}: ${message}`); } } function readOptionalString(value: unknown): string | undefined { if (typeof value !== 'string') { return undefined; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; }