import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { pathToFileURL } from 'node:url'; import { MatrixRuntime } from '@open-matrix/core/runtime/MatrixRuntime.js'; import type { MatrixActor } from '@open-matrix/core/core/MatrixActor.js'; import { RequestReply } from '@open-matrix/core/engine/remoting/RequestReply.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 key?: string; readonly cloud?: string; readonly space?: string; readonly home?: string; readonly export?: string; readonly mount?: string; readonly json?: boolean; readonly props?: string; readonly ttlSeconds?: number; readonly checkOp?: string; readonly checkPayload?: string; } export interface IActorRunProviderResolution { readonly key: string; readonly cloud: string; readonly space: string; readonly checked: readonly string[]; } interface IActorCredentialExchange { readonly ok?: unknown; readonly error?: unknown; readonly space?: { readonly authorityRoot?: unknown; readonly spaceId?: unknown; readonly routeKey?: unknown; readonly publicNamespace?: unknown; }; readonly transport?: { readonly natsUrl?: unknown; readonly root?: unknown; }; readonly mount?: unknown; readonly effectiveMount?: unknown; readonly credentials?: { readonly jwt?: unknown; readonly seed?: unknown; readonly userPublicKey?: unknown; readonly expiresAt?: unknown; }; readonly source?: unknown; readonly credentialPipeline?: unknown; } 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 provider = resolveActorRunProvider(options); const exchange = await exchangeActorCredential({ provider, mount, actorId: ActorClass.name || mount, packageId: readPackageNameForEntry(entryPath) ?? 'standalone-actor', runtimeId: `standalone-actor:${process.pid}:${mount}`, accepts: ActorClass.accepts ?? {}, emits: ActorClass.emits ?? {}, subscribes: ActorClass.subscribes ?? {}, ttlSeconds: options.ttlSeconds, }); const authorityRoot = readRequiredString(exchange.space?.authorityRoot, 'actor credential response space.authorityRoot'); const natsUrl = readRequiredString(exchange.transport?.natsUrl, 'actor credential response transport.natsUrl'); const jwt = readRequiredString(exchange.credentials?.jwt, 'actor credential response credentials.jwt'); const seed = readRequiredString(exchange.credentials?.seed, 'actor credential response credentials.seed'); const connection = await connectActorNats({ natsUrl, authorityRoot, jwt, seed, }); const transport = new NatsTransport(connection, { root: authorityRoot }); 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, authorityRoot, effectiveMount: resolveEffectiveMount(exchange.effectiveMount, authorityRoot, mount), cloud: provider.cloud, natsUrl, source: readRequiredString(exchange.source, 'actor credential response source'), credentialPipeline: readRequiredString(exchange.credentialPipeline, 'actor credential response credentialPipeline'), 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(` authority: ${result.authorityRoot}`); console.log(` effective: ${result.effectiveMount}`); console.log(` cloud: ${result.cloud}`); 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 provider = resolveActorRunProvider(options); const exchange = await exchangeActorCredential({ provider, mount: spec.mount, actorId: spec.actorId, packageId: spec.packageId, runtimeId: spec.runtimeId, accepts: spec.accepts, emits: spec.emits, subscribes: spec.subscribes, ttlSeconds: options.ttlSeconds, }); const authorityRoot = readRequiredString(exchange.space?.authorityRoot, 'actor credential response space.authorityRoot'); const natsUrl = readRequiredString(exchange.transport?.natsUrl, 'actor credential response transport.natsUrl'); const jwt = readRequiredString(exchange.credentials?.jwt, 'actor credential response credentials.jwt'); const seed = readRequiredString(exchange.credentials?.seed, 'actor credential response credentials.seed'); const connection = await connectActorNats({ natsUrl, authorityRoot, jwt, seed, }); const transport = new NatsTransport(connection, { root: authorityRoot }); 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: authorityRoot, 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, authorityRoot, effectiveMount: resolveEffectiveMount(exchange.effectiveMount, authorityRoot, spec.mount), cloud: provider.cloud, natsUrl, source: readRequiredString(exchange.source, 'actor credential response source'), credentialPipeline: readRequiredString(exchange.credentialPipeline, 'actor credential response credentialPipeline'), 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(` authority: ${result.authorityRoot}`); console.log(` effective: ${result.effectiveMount}`); console.log(` cloud: ${result.cloud}`); 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 resolveActorRunProvider(options: IActorRunCommandOptions): IActorRunProviderResolution { const home = path.resolve(options.home?.trim() || path.join(os.homedir(), '.matrix')); const homeCredential = readApiKeyCredentialFile(path.join(home, 'credentials', 'matrix-api-key.json')); const homeConfig = readApiKeyConfigFile(path.join(home, 'config.json')); const defaultHome = path.resolve(os.homedir(), '.matrix'); const defaultCredential = home === defaultHome ? {} : readApiKeyCredentialFile(path.join(defaultHome, 'credentials', 'matrix-api-key.json')); const defaultConfig = home === defaultHome ? {} : readApiKeyConfigFile(path.join(defaultHome, 'config.json')); const hivecastNamedCredential = readApiKeyCredentialFile(path.join(defaultHome, 'credentials', 'hivecast-api-key.json')); const key = readOptionalString(options.key) ?? readOptionalString(process.env.MATRIX_API_KEY) ?? readOptionalString(process.env.HIVECAST_API_KEY) ?? homeCredential.key ?? homeConfig.key ?? defaultCredential.key ?? defaultConfig.key ?? hivecastNamedCredential.key; const cloud = readOptionalString(options.cloud) ?? readOptionalString(process.env.MATRIX_CLOUD) ?? readOptionalString(process.env.HIVECAST_CLOUD) ?? homeCredential.cloud ?? homeConfig.cloud ?? defaultCredential.cloud ?? defaultConfig.cloud ?? hivecastNamedCredential.cloud; const space = readOptionalString(options.space) ?? readOptionalString(process.env.MATRIX_SPACE) ?? readOptionalString(process.env.HIVECAST_SPACE) ?? homeCredential.space ?? homeConfig.space ?? defaultCredential.space ?? defaultConfig.space ?? hivecastNamedCredential.space; const checked = [ '--key', 'MATRIX_API_KEY', 'HIVECAST_API_KEY', '/credentials/matrix-api-key.json', '/config.json', '~/.matrix/credentials/matrix-api-key.json', '~/.matrix/config.json', '~/.matrix/credentials/hivecast-api-key.json', ]; if (!key) { throw new Error(`MATRIX_API_KEY_REQUIRED: matrix actor run requires --key or a configured API key. Checked: ${checked.join(', ')}`); } if (!cloud) { throw new Error('MATRIX_CLOUD_REQUIRED: matrix actor run requires --cloud or configured MATRIX_CLOUD/HIVECAST_CLOUD'); } if (!space) { throw new Error('MATRIX_SPACE_REQUIRED: matrix actor run requires --space or configured MATRIX_SPACE/HIVECAST_SPACE'); } return { key, cloud: normalizeCloudUrl(cloud), space, checked, }; } export async function exchangeActorCredential(input: { readonly provider: IActorRunProviderResolution; readonly mount: string; readonly actorId: string; readonly packageId: string; readonly runtimeId: string; readonly accepts: Record; readonly emits: Record; readonly subscribes: Record; readonly ttlSeconds?: number; readonly fetchImpl?: typeof fetch; }): Promise { const fetchImpl = input.fetchImpl ?? fetch; const response = await fetchImpl(new URL('/api/install/actor-credential', input.provider.cloud), { method: 'POST', headers: { authorization: `Bearer ${input.provider.key}`, 'content-type': 'application/json', }, body: JSON.stringify({ space: input.provider.space, mount: input.mount, actorId: input.actorId, packageId: input.packageId, runtimeId: input.runtimeId, accepts: input.accepts, emits: input.emits, subscribes: input.subscribes, ...(input.ttlSeconds ? { ttlSeconds: input.ttlSeconds } : {}), }), }); const body = await response.json().catch(() => null) as IActorCredentialExchange | null; if (!response.ok || body?.ok !== true) { const error = readOptionalString(body?.error) ?? `HTTP_${response.status}`; throw new Error(`ACTOR_CREDENTIAL_EXCHANGE_FAILED: ${error}`); } return body; } 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: { readonly natsUrl: string; readonly authorityRoot: string; readonly jwt: string; readonly seed: string; }): Promise { const encoder = new TextEncoder(); return await natsConnect({ servers: [input.natsUrl], name: `matrix-actor-run-${input.authorityRoot}-${process.pid}`, timeout: 5_000, 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 readPackageNameForEntry(entryPath: string): string | undefined { let dir = path.dirname(entryPath); while (dir !== path.dirname(dir)) { const packageJsonPath = path.join(dir, 'package.json'); if (fs.existsSync(packageJsonPath)) { const parsed = readJsonFile(packageJsonPath); return readOptionalString(parsed?.name); } dir = path.dirname(dir); } return undefined; } 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 readApiKeyCredentialFile(filePath: string): { readonly key?: string; readonly cloud?: string; readonly space?: string } { const value = readJsonFile(filePath); return { ...(readOptionalString(value?.apiKey) ?? readOptionalString(value?.key) ? { key: (readOptionalString(value?.apiKey) ?? readOptionalString(value?.key))! } : {}), ...(readOptionalString(value?.cloud) ? { cloud: readOptionalString(value?.cloud)! } : {}), ...(readOptionalString(value?.space) ?? readOptionalString(value?.authorityRoot) ? { space: (readOptionalString(value?.space) ?? readOptionalString(value?.authorityRoot))! } : {}), }; } function readApiKeyConfigFile(filePath: string): { readonly key?: string; readonly cloud?: string; readonly space?: string } { const value = readJsonFile(filePath); const apiKey = value?.apiKey && typeof value.apiKey === 'object' && !Array.isArray(value.apiKey) ? value.apiKey as Record : {}; return { ...(readOptionalString(apiKey.apiKey) ?? readOptionalString(apiKey.key) ? { key: (readOptionalString(apiKey.apiKey) ?? readOptionalString(apiKey.key))! } : {}), ...(readOptionalString(value?.cloud) ?? readOptionalString(value?.hivecastCloud) ? { cloud: (readOptionalString(value?.cloud) ?? readOptionalString(value?.hivecastCloud))! } : {}), ...(readOptionalString(value?.space) ?? readOptionalString(value?.authorityRoot) ? { space: (readOptionalString(value?.space) ?? readOptionalString(value?.authorityRoot))! } : {}), }; } 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 readRequiredString(value: unknown, label: string): string { const result = readOptionalString(value); if (!result) { throw new Error(`Missing ${label}`); } return result; } function readOptionalString(value: unknown): string | undefined { if (typeof value !== 'string') { return undefined; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } function resolveEffectiveMount(value: unknown, authorityRoot: string, mount: string): string { const fromExchange = readOptionalString(value); if (fromExchange) { return fromExchange; } return `${authorityRoot}.${mount}`; } function normalizeCloudUrl(value: string): string { const parsed = new URL(value); return parsed.toString().replace(/\/$/, ''); }