feat: initial Matrix SDK

Full SDK workspace: core, contracts, sdk, cli, browser-host, browser-kit,
federation, omega-core, oracle, self-healing, strategies, tools, emacs.
Clean extraction from the development monorepo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hypnotranz 2026-06-07 15:54:15 -06:00
commit 1a0419dc0c
1358 changed files with 167803 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
node_modules/
**/node_modules/
dist/
**/dist/
.turbo/
**/.turbo/
.tsbuildinfo
**/.tsbuildinfo
*.tgz

22
README.md Normal file
View File

@ -0,0 +1,22 @@
# Matrix SDK Workspace
This workspace is the isolated SDK incubation area for
`WORKSTREAMS/matrix-sdk-isolation`.
Current proven checkpoints:
- copied SDK-bearing packages build/test/type-check/pack independently
- copied package identities were renamed to final `@open-matrix/*` names
- `@open-matrix/sdk` facade package builds/tests/packs
- `@open-matrix/browser-kit` builds package-manager-ready `dist` exports
- a fresh external temp consumer installs packed tarballs, compiles, starts a
`MatrixActor`, and calls it through the Matrix runtime/transport path
Proof commands:
```bash
pnpm --dir projects/matrix-sdk prove
pnpm --dir projects/matrix-sdk prove:external-consumer
```
Matrix-3 consumers have not been repointed yet.

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "matrix-sdk-workspace",
"version": "0.0.0",
"private": true,
"type": "module",
"packageManager": "pnpm@10.29.3",
"scripts": {
"clean": "node scripts/clean.mjs",
"build": "pnpm -r --sort --if-present run build",
"test": "pnpm -r --sort --if-present run test",
"type-check": "pnpm -r --sort --if-present run type-check",
"pack:all": "pnpm -r exec pnpm pack",
"prove:external-consumer": "node scripts/prove-external-consumer.mjs",
"prove:cli-uat": "node scripts/prove-sdk-cli-uat.mjs",
"prove": "pnpm run clean && pnpm run build && pnpm run test && pnpm run type-check && pnpm run pack:all"
},
"dependencies": {
"@aws-crypto/sha256-js": "5.2.0",
"@nats-io/nats-core": "^3.3.1",
"morphdom": "^2.7.8",
"nats.ws": "^1.30.3",
"vite": "^5.4.21"
},
"devDependencies": {
"@types/node": "^25.0.10",
"esbuild": "^0.27.3",
"tsx": "^4.15.6",
"typescript": "^5.7.0"
}
}

View File

@ -0,0 +1,60 @@
# @open-matrix/browser-host Contract
Layer: browser runtime host.
## Owns
```text
browser runtime bootstrap consumption
MatrixAuthorityContext installation into runtime context
browser runtimeInstanceRoot propagation
hosted runtime target resolution for browser apps
browser app readiness probes
```
## Does Not Own
```text
auth decisions
namespace ownership
Registry storage
Device inventory storage
NATS/JWT policy
```
## Consumes
```text
/api/bootstrap authorityContext
MatrixAuthorityContext
browser bus token material from gateway bootstrap
```
## Provided Surfaces
```text
matrix-dsl-host browser element
hosted runtime target resolution helpers
app-ready probe helpers
bundle freshness monitor
```
## Forbidden
```text
selectedAuthorityRoot from DOM authority-root
selectedAuthorityRoot from runtime.root
selectedAuthorityRoot from assetHostRoot
selectedAuthorityRoot from URL host
```
## Configuration
Browser authority comes from `/api/bootstrap`. Package configuration may control app mounting and readiness behavior, but it must not decide data authority.
## Proof Gates
```text
pnpm test:focused @open-matrix/browser-host
pnpm --filter @open-matrix/browser-host type-check
```

View File

@ -0,0 +1,32 @@
#!/usr/bin/env node
/**
* @open-matrix/browser-host build
*
* Shell-phase build. Compiles src/ via tsc to dist/. Phase 1 of
* core-decontamination uses this to verify the package boundary works
* end-to-end before files are moved out of core.
*/
import { execFileSync } from 'node:child_process';
import { createRequire } from 'node:module';
import { rmSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const distDir = resolve(__dirname, 'dist');
const require = createRequire(import.meta.url);
const typescriptPackageJson = require.resolve('typescript/package.json', { paths: [__dirname] });
const tscBin = resolve(dirname(typescriptPackageJson), 'bin/tsc');
rmSync(distDir, { recursive: true, force: true });
execFileSync(process.execPath, [
tscBin,
'-p',
resolve(__dirname, 'tsconfig.build.json'),
], {
cwd: __dirname,
stdio: 'inherit',
});
console.log('[browser-host-build] dist/index.js');

View File

@ -0,0 +1,65 @@
{
"schemaVersion": 1,
"package": "@open-matrix/browser-host",
"layer": "browser-host",
"owns": [
"browser runtime bootstrap consumption",
"MatrixAuthorityContext installation into browser runtime context",
"runtimeInstanceRoot propagation"
],
"doesNotOwn": [
"auth decisions",
"namespace ownership",
"Registry storage",
"provider binding resolution"
],
"providedActorSurfaces": [
{
"subjectPattern": "<runtimeInstanceRoot>.<browser-app-mount>.$inbox",
"placement": "browser-runtime",
"ops": [
"$introspect",
"$prompt"
]
}
],
"requiredActorSurfaces": [
{
"subjectPattern": "SPACE-HIVECAST.system.gateway.http",
"rootSource": "asset host HTTP bootstrap",
"ops": [
"/api/bootstrap"
]
}
],
"configuration": {
"sources": [
"/api/bootstrap authorityContext"
],
"forbiddenSources": [
"DOM authority-root",
"runtime.root",
"assetHostRoot"
]
},
"persistence": {
"owns": [
"none"
],
"reads": [
"bootstrap response only"
]
},
"security": {
"mustAllow": [
"browser session bootstrap credentials for authorized selectable roots"
],
"mustDeny": [
"browser-side recomputation of authority roots"
]
},
"proofGates": [
"guard:control-surface-contracts",
"pnpm test:focused @open-matrix/browser-host"
]
}

View File

@ -0,0 +1,67 @@
{
"name": "@open-matrix/browser-host",
"version": "0.1.0",
"description": "Browser host shell for Matrix custom elements (extracted from @open-matrix/core during core-decontamination).",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./host": {
"types": "./dist/host.d.ts",
"import": "./dist/host.js",
"default": "./dist/host.js"
},
"./recovery": {
"types": "./dist/recovery.d.ts",
"import": "./dist/recovery.js",
"default": "./dist/recovery.js"
},
"./app-ready": {
"types": "./dist/app-ready.d.ts",
"import": "./dist/app-ready.js",
"default": "./dist/app-ready.js"
},
"./bundle-freshness": {
"types": "./dist/BundleFreshness.d.ts",
"import": "./dist/BundleFreshness.js",
"default": "./dist/BundleFreshness.js"
},
"./profile-trace": {
"types": "./dist/profile-trace.d.ts",
"import": "./dist/profile-trace.js",
"default": "./dist/profile-trace.js"
},
"./package.json": "./package.json"
},
"scripts": {
"build": "node build.mjs",
"test": "node --import tsx --test src/__tests__/*.spec.ts",
"test:ci": "pnpm run test",
"type-check": "tsc --noEmit"
},
"files": [
"src",
"dist",
"package.json",
"CONTRACT.md",
"matrix.control-surface.contract.json"
],
"dependencies": {
"@open-matrix/contracts": "workspace:*",
"@open-matrix/federation": "workspace:*",
"@open-matrix/core": "workspace:*"
},
"devDependencies": {
"tsx": "^4.0.0",
"typescript": "^5.7.0"
},
"license": "MIT",
"engines": {
"node": ">=20"
}
}

View File

@ -0,0 +1,277 @@
import type { IMatrixContext } from '@open-matrix/core';
import { RequestReply } from '@open-matrix/core';
export type MatrixAppReadinessLevel = 'shell' | 'operational' | 'cognitive';
export interface MatrixAppReadyActorProbe {
readonly mount: string;
readonly operation: string;
readonly ok: boolean;
readonly error?: string;
}
export interface MatrixAppReadyActorProbeRequest {
readonly mount: string;
readonly operation: string;
readonly payload?: Record<string, unknown>;
readonly targetRoot?: string;
readonly timeoutMs?: number;
readonly accept?: (result: unknown) => boolean;
readonly invalidResultMessage?: string;
}
export interface MatrixAppReadyDetail {
readonly appName: string;
readonly root: string | null;
readonly authorityRoot: string | null;
readonly runtimeTargetRoot: string | null;
readonly requestedTarget: string | null;
readonly actualTarget: string | null;
readonly transportConnected: boolean;
readonly readinessLevel: MatrixAppReadinessLevel;
readonly requiredActorsReachable: boolean;
readonly actorProbes: readonly MatrixAppReadyActorProbe[];
readonly errors: readonly string[];
}
export interface MatrixAppReadySignalOptions {
readonly appName: string;
readonly appElement: HTMLElement;
readonly root: string | null;
readonly authorityRoot: string | null;
readonly runtimeTargetRoot: string | null;
readonly requestedTarget: string | null;
readonly actualTarget: string | null;
readonly transportConnected: boolean;
readonly readinessLevel: MatrixAppReadinessLevel;
readonly requiredActorsReachable: boolean;
readonly actorProbes?: readonly MatrixAppReadyActorProbe[];
readonly errors: readonly string[];
}
export interface MatrixAppReadyMonitorOptions {
readonly appName: string;
readonly appSelector: string;
readonly hostSelector?: string;
readonly timeoutMs?: number;
readonly pollIntervalMs?: number;
readonly readiness: (
appElement: HTMLElement,
hostElement: HTMLElement | null
) => MatrixAppReadyDetail | Promise<MatrixAppReadyDetail | null> | null;
}
const DEFAULT_TIMEOUT_MS = 30_000;
const DEFAULT_POLL_INTERVAL_MS = 100;
export function markMatrixAppReady(options: MatrixAppReadySignalOptions): MatrixAppReadyDetail {
const detail: MatrixAppReadyDetail = {
appName: options.appName,
root: options.root,
authorityRoot: options.authorityRoot,
runtimeTargetRoot: options.runtimeTargetRoot,
requestedTarget: options.requestedTarget,
actualTarget: options.actualTarget,
transportConnected: options.transportConnected,
readinessLevel: options.readinessLevel,
requiredActorsReachable: options.requiredActorsReachable,
actorProbes: options.actorProbes ?? [],
errors: options.errors,
};
options.appElement.setAttribute('data-app-ready', options.appName);
options.appElement.setAttribute('data-app-ready-level', options.readinessLevel);
options.appElement.setAttribute('data-app-required-actors-reachable', String(options.requiredActorsReachable));
const readyRoot = options.runtimeTargetRoot === null ? '' : options.runtimeTargetRoot;
const requestedTarget = options.requestedTarget === null ? '' : options.requestedTarget;
const actualTarget = options.actualTarget === null ? '' : options.actualTarget;
options.appElement.setAttribute('data-app-ready-root', readyRoot);
options.appElement.setAttribute('data-app-ready-requested-target', requestedTarget);
options.appElement.setAttribute('data-app-ready-actual-target', actualTarget);
document.documentElement.setAttribute('data-app-ready', options.appName);
document.documentElement.setAttribute('data-app-ready-level', options.readinessLevel);
document.documentElement.setAttribute('data-app-required-actors-reachable', String(options.requiredActorsReachable));
document.documentElement.setAttribute('data-app-ready-root', readyRoot);
document.documentElement.setAttribute('data-app-ready-requested-target', requestedTarget);
document.documentElement.setAttribute('data-app-ready-actual-target', actualTarget);
window.dispatchEvent(new CustomEvent<MatrixAppReadyDetail>('matrix-app-ready', { detail }));
return detail;
}
export function monitorMatrixAppReady(options: MatrixAppReadyMonitorOptions): void {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return;
}
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
const startedAt = performance.now();
let completed = false;
let pending = false;
let timer: number | null = null;
const clearTimer = () => {
if (timer !== null) {
window.clearTimeout(timer);
timer = null;
}
};
const attempt = () => {
if (completed || pending) {
return;
}
const appElement = document.querySelector(options.appSelector);
let hostElement: HTMLElement | null = null;
if (options.hostSelector) {
const selectedHost = document.querySelector(options.hostSelector);
if (!(selectedHost instanceof HTMLElement)) {
scheduleNextAttempt(startedAt, timeoutMs, pollIntervalMs, attempt, (nextTimer) => {
timer = nextTimer;
}, clearTimer);
return;
}
hostElement = selectedHost;
}
if (appElement instanceof HTMLElement) {
const readiness = options.readiness(appElement, hostElement);
if (isPromiseLike(readiness)) {
pending = true;
readiness
.then((resolved) => {
pending = false;
if (resolved) {
completeReadiness(options, appElement, resolved, clearTimer);
completed = true;
return;
}
scheduleNextAttempt(startedAt, timeoutMs, pollIntervalMs, attempt, (nextTimer) => {
timer = nextTimer;
}, clearTimer);
})
.catch(() => {
pending = false;
scheduleNextAttempt(startedAt, timeoutMs, pollIntervalMs, attempt, (nextTimer) => {
timer = nextTimer;
}, clearTimer);
});
return;
}
if (readiness) {
completed = true;
completeReadiness(options, appElement, readiness, clearTimer);
return;
}
}
scheduleNextAttempt(startedAt, timeoutMs, pollIntervalMs, attempt, (nextTimer) => {
timer = nextTimer;
}, clearTimer);
};
window.addEventListener('beforeunload', clearTimer, { once: true });
attempt();
}
export async function probeMatrixAppReadyActors(
context: IMatrixContext,
probes: readonly MatrixAppReadyActorProbeRequest[],
): Promise<readonly MatrixAppReadyActorProbe[]> {
const results: MatrixAppReadyActorProbe[] = [];
for (const probe of probes) {
try {
const result = await RequestReply.execute<Record<string, unknown>, unknown>(
context,
probe.mount,
probe.operation,
probe.payload ?? {},
{
timeoutMs: probe.timeoutMs ?? 5000,
...(probe.targetRoot ? { targetRoot: probe.targetRoot } : {}),
},
);
if (probe.accept && !probe.accept(result)) {
results.push({
mount: probe.mount,
operation: probe.operation,
ok: false,
error: probe.invalidResultMessage ?? 'Actor returned an invalid readiness response',
});
continue;
}
results.push({
mount: probe.mount,
operation: probe.operation,
ok: true,
});
} catch (error) {
results.push({
mount: probe.mount,
operation: probe.operation,
ok: false,
error: error instanceof Error ? error.message : String(error),
});
}
}
return results;
}
export function matrixAppReadyProbeErrors(
probes: readonly MatrixAppReadyActorProbe[],
): readonly string[] {
return probes
.filter((probe) => !probe.ok)
.map((probe) => `${probe.mount} ${probe.operation}: ${probe.error ?? 'not reachable'}`);
}
export function matrixAppReadyProbesOk(
probes: readonly MatrixAppReadyActorProbe[],
): boolean {
return probes.length > 0 && probes.every((probe) => probe.ok);
}
function completeReadiness(
options: MatrixAppReadyMonitorOptions,
appElement: HTMLElement,
readiness: MatrixAppReadyDetail,
clearTimer: () => void,
): void {
clearTimer();
markMatrixAppReady({
appName: options.appName,
appElement,
root: readiness.root,
authorityRoot: readiness.authorityRoot,
runtimeTargetRoot: readiness.runtimeTargetRoot,
requestedTarget: readiness.requestedTarget,
actualTarget: readiness.actualTarget,
transportConnected: readiness.transportConnected,
readinessLevel: readiness.readinessLevel,
requiredActorsReachable: readiness.requiredActorsReachable,
actorProbes: readiness.actorProbes,
errors: readiness.errors,
});
}
function isPromiseLike<T>(value: T | Promise<T>): value is Promise<T> {
return Boolean(
value
&& typeof value === 'object'
&& 'then' in value
&& typeof (value as { then?: unknown }).then === 'function'
);
}
function scheduleNextAttempt(
startedAt: number,
timeoutMs: number,
pollIntervalMs: number,
attempt: () => void,
setTimer: (timer: number) => void,
clearTimer: () => void,
): void {
if (performance.now() - startedAt >= timeoutMs) {
clearTimer();
return;
}
setTimer(window.setTimeout(attempt, pollIntervalMs));
}

View File

@ -0,0 +1,174 @@
type BrowserProfileValue = string | number | boolean | null | undefined;
export type IBrowserProfileDetails = Record<string, BrowserProfileValue>;
export interface IBrowserProfileEvent {
readonly scope: string;
readonly label: string;
readonly durationMs?: number;
readonly timestamp: string;
readonly details?: IBrowserProfileDetails;
}
type BrowserProfileWindow = Window & {
__mxProfileEvents?: IBrowserProfileEvent[];
__mxProfileSeq?: number;
};
function getWindow(): BrowserProfileWindow | null {
if (typeof window === 'undefined') {
return null;
}
return window as BrowserProfileWindow;
}
function readProfileSetting(): string | null {
const currentWindow = getWindow();
if (!currentWindow) {
return null;
}
const search = typeof currentWindow.location?.search === 'string'
? currentWindow.location.search
: '';
const value = new URLSearchParams(search).get('mxProfile');
return typeof value === 'string' && value.trim().length > 0 ? value.trim().toLowerCase() : null;
}
function formatDetails(details?: IBrowserProfileDetails): string {
if (!details) {
return '';
}
const entries = Object.entries(details)
.filter(([, value]) => value !== undefined)
.map(([key, value]) => `${key}=${String(value)}`);
return entries.length > 0 ? ` ${entries.join(' ')}` : '';
}
function appendBrowserProfileEvent(event: IBrowserProfileEvent): void {
const currentWindow = getWindow();
if (!currentWindow) {
return;
}
if (!Array.isArray(currentWindow.__mxProfileEvents)) {
currentWindow.__mxProfileEvents = [];
}
currentWindow.__mxProfileEvents.push(event);
}
function nextProfileSequence(): number {
const currentWindow = getWindow();
if (!currentWindow) {
return 0;
}
currentWindow.__mxProfileSeq = (currentWindow.__mxProfileSeq ?? 0) + 1;
return currentWindow.__mxProfileSeq;
}
function nowMs(): number {
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
return performance.now();
}
return Date.now();
}
export function isBrowserProfileEnabled(scope: string): boolean {
const setting = readProfileSetting();
if (!setting) {
return false;
}
if (setting === '1' || setting === 'true' || setting === 'all' || setting === '*') {
return true;
}
return setting
.split(',')
.map((part) => part.trim())
.filter((part) => part.length > 0)
.includes(scope.trim().toLowerCase());
}
export function logBrowserProfile(
scope: string,
label: string,
details?: IBrowserProfileDetails,
): void {
if (!isBrowserProfileEnabled(scope)) {
return;
}
console.info(`[MxProfile:${scope}] ${label}${formatDetails(details)}`);
appendBrowserProfileEvent({
scope,
label,
details,
timestamp: new Date().toISOString(),
});
}
export function createBrowserProfileSpan(
scope: string,
label: string,
details?: IBrowserProfileDetails,
): (finalDetails?: IBrowserProfileDetails) => void {
if (!isBrowserProfileEnabled(scope)) {
return () => {};
}
const startTime = nowMs();
const sequence = nextProfileSequence();
const startMark = `mx-profile:${scope}:${label}:start:${sequence}`;
const endMark = `mx-profile:${scope}:${label}:end:${sequence}`;
if (typeof performance !== 'undefined' && typeof performance.mark === 'function') {
performance.mark(startMark);
}
if (details) {
logBrowserProfile(scope, `${label}:start`, details);
} else {
logBrowserProfile(scope, `${label}:start`);
}
return (finalDetails?: IBrowserProfileDetails) => {
const durationMs = nowMs() - startTime;
if (typeof performance !== 'undefined' && typeof performance.mark === 'function') {
performance.mark(endMark);
}
if (
typeof performance !== 'undefined'
&& typeof performance.measure === 'function'
) {
performance.measure(`mx-profile:${scope}:${label}`, startMark, endMark);
}
const mergedDetails = finalDetails
? { ...(details ?? {}), ...finalDetails }
: details;
console.info(
`[MxProfile:${scope}] ${label} ${durationMs.toFixed(1)}ms${formatDetails(mergedDetails)}`,
);
appendBrowserProfileEvent({
scope,
label,
durationMs,
details: mergedDetails,
timestamp: new Date().toISOString(),
});
};
}
export async function profileBrowserSpan<T>(
scope: string,
label: string,
fn: () => Promise<T>,
details?: IBrowserProfileDetails,
): Promise<T> {
const finish = createBrowserProfileSpan(scope, label, details);
try {
const result = await fn();
finish();
return result;
} catch (error) {
finish({
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}

View File

@ -0,0 +1,197 @@
import { MatrixActor } from '@open-matrix/core';
import { MatrixRuntime } from '@open-matrix/core';
type MatrixIntrospect = ReturnType<MatrixActor['on$introspect']>;
type MatrixIntrospectChild = NonNullable<MatrixIntrospect['children']>[number];
export type BrowserRuntimeLifecycleState = 'live' | 'closing';
export interface IBrowserRuntimeControlService {
readonly runtimeId: string;
readonly runtimeMount: string;
readonly startedAt: string;
getState(): BrowserRuntimeLifecycleState;
getRuntime(): MatrixRuntime;
getInventory(): readonly IBrowserRuntimeControlInventoryEntry[];
}
export interface IMountBrowserRuntimeControlOptions {
readonly runtime: MatrixRuntime;
readonly runtimeId: string;
readonly runtimeMount: string;
readonly controlMount: string;
readonly startedAt: string;
getState(): BrowserRuntimeLifecycleState;
getInventory?: () => readonly IBrowserRuntimeControlInventoryEntry[];
}
export interface IBrowserRuntimeControlInventoryEntry {
readonly mount: string;
readonly type?: string;
}
const BROWSER_RUNTIME_CONTROL_SERVICE = 'browser-runtime-control';
export class BrowserRuntimeControlActor extends MatrixActor {
static readonly hasDynamicChildren = true;
static description = 'Runtime control actor for a browser tab runtime.';
static override accepts: Record<string, unknown> = {
'runtime.status': { description: 'Report browser runtime lifecycle and direct actor inventory', options: 'object?' },
'runtime.ready': { description: 'Report whether the browser runtime is ready for traffic', options: 'object?' },
'runtime.health': { description: 'Report browser runtime health', options: 'object?' },
};
protected override onIntrospect(payload?: { depth?: 'basic' | 'full' }): ReturnType<MatrixActor['onIntrospect']> {
const base = super.onIntrospect(payload);
const service = this._service();
// The browser runtime actor extends the base introspect shape with
// browser-specific runtime metadata. The supertype declares a strict
// shape; cast through unknown so the extension stays type-safe at
// the call site of `base` while satisfying the override signature.
const extended: ReturnType<MatrixActor['onIntrospect']> = {
...base,
children: [
...(base.children ?? []),
...this._directRuntimeChildren(service),
],
};
return Object.assign(extended, {
type: 'runtime',
runtimeId: service.runtimeId,
runtimeType: 'browser',
state: service.getState(),
startedAt: service.startedAt,
uptimeMs: Date.now() - Date.parse(service.startedAt),
});
}
onRuntimeStatus(): Record<string, unknown> {
const service = this._service();
const runtime = service.getRuntime();
const mounts = runtime.getAllComponents().map((component) => component.mount);
return {
ok: true,
runtimeId: service.runtimeId,
runtimeMount: service.runtimeMount,
type: 'browser',
state: service.getState(),
startedAt: service.startedAt,
uptimeMs: Date.now() - Date.parse(service.startedAt),
actorCount: mounts.length,
mounts,
rootActors: this._directRuntimeChildren(service).map((child) => ({
mount: child.mount,
componentClass: child.componentClass,
})),
};
}
onRuntimeReady(): Record<string, unknown> {
const state = this._service().getState();
return {
ok: true,
ready: state === 'live',
state,
};
}
onRuntimeHealth(): Record<string, unknown> {
const service = this._service();
const state = service.getState();
return {
ok: true,
healthy: state === 'live',
runtimeId: service.runtimeId,
state,
};
}
private _directRuntimeChildren(service: IBrowserRuntimeControlService): MatrixIntrospectChild[] {
const runtimeMount = service.runtimeMount.trim();
const currentMount = this.mount.trim();
const seen = new Set<string>();
const children: MatrixIntrospectChild[] = [];
for (const entry of service.getInventory()) {
const actualMount = typeof entry.mount === 'string' ? entry.mount.trim() : '';
const childMount = this._directChildMount(runtimeMount, currentMount, actualMount);
if (!childMount || seen.has(childMount)) {
continue;
}
seen.add(childMount);
children.push({
id: childMount,
slot: 'runtime',
mount: childMount,
componentClass: entry.type || 'RuntimeInventoryActor',
accepts: [],
emits: [],
tracks: [],
});
}
for (const component of service.getRuntime().getAllComponents()) {
const actualMount = component.mount.trim();
const childMount = this._directChildMount(runtimeMount, currentMount, actualMount);
if (!childMount || seen.has(childMount)) {
continue;
}
seen.add(childMount);
children.push({
id: childMount,
slot: 'runtime',
mount: childMount,
componentClass: component.constructor.name || 'MatrixActor',
accepts: [],
emits: [],
tracks: [],
});
}
return children.sort((left, right) => left.mount.localeCompare(right.mount));
}
private _directChildMount(runtimeMount: string, currentMount: string, actualMount: string): string | null {
if (!actualMount || actualMount === currentMount) {
return null;
}
if (runtimeMount && actualMount.startsWith(`${runtimeMount}.`)) {
const localMount = actualMount.slice(runtimeMount.length + 1);
return localMount && !localMount.includes('.') ? localMount : null;
}
if (!actualMount.includes('.')) {
return actualMount;
}
return null;
}
private _service(): IBrowserRuntimeControlService {
const service = this.context?.getService<IBrowserRuntimeControlService>(BROWSER_RUNTIME_CONTROL_SERVICE);
if (!service) {
throw new Error('Browser runtime control service is not registered');
}
return service;
}
}
export async function mountBrowserRuntimeControl(
options: IMountBrowserRuntimeControlOptions,
): Promise<string> {
const service: IBrowserRuntimeControlService = {
runtimeId: options.runtimeId,
runtimeMount: options.runtimeMount,
startedAt: options.startedAt,
getRuntime: () => options.runtime,
getState: options.getState,
getInventory: () => options.getInventory?.() ?? [],
};
options.runtime.registerService(BROWSER_RUNTIME_CONTROL_SERVICE, service);
if (!options.runtime.hasComponent(options.runtimeMount)) {
await options.runtime.createSupervised(BrowserRuntimeControlActor, options.runtimeMount);
}
if (options.controlMount !== options.runtimeMount && !options.runtime.hasComponent(options.controlMount)) {
await options.runtime.createSupervised(BrowserRuntimeControlActor, options.controlMount);
}
return options.controlMount;
}

View File

@ -0,0 +1,112 @@
export interface BundleFreshnessMonitorOptions {
readonly appName: string;
readonly currentModuleUrl: string;
readonly enabled?: boolean;
readonly intervalMs?: number;
readonly documentUrl?: string;
readonly fetchHtml?: (url: string) => Promise<string>;
readonly reload?: () => void;
}
const DEFAULT_BUNDLE_FRESHNESS_INTERVAL_MS = 30_000;
const MIN_BUNDLE_FRESHNESS_INTERVAL_MS = 5_000;
export function htmlReferencesBundle(
html: string,
currentModuleUrl: string,
documentUrl: string,
): boolean {
const currentPath = normalizeUrlPath(currentModuleUrl, documentUrl);
const scriptPattern = /<script\b[^>]*\bsrc=(["'])(.*?)\1/gi;
for (const match of html.matchAll(scriptPattern)) {
const rawSrc = match[2]?.trim();
if (!rawSrc) {
continue;
}
if (normalizeUrlPath(rawSrc, documentUrl) === currentPath) {
return true;
}
}
const inlineImportPattern = /\bimport\s+(?:[^'"]+\s+from\s+)?(["'])(.*?)\1/gi;
for (const match of html.matchAll(inlineImportPattern)) {
const rawImport = match[2]?.trim();
if (!rawImport) {
continue;
}
if (normalizeUrlPath(rawImport, documentUrl) === currentPath) {
return true;
}
}
return false;
}
export function installBundleFreshnessMonitor(
options: BundleFreshnessMonitorOptions,
): () => void {
if (options.enabled === false || typeof window === 'undefined' || typeof document === 'undefined') {
return () => {};
}
const documentUrl = options.documentUrl ?? window.location.href;
const fetchHtml = options.fetchHtml ?? fetchDocumentHtml;
const reload = options.reload ?? (() => window.location.reload());
const intervalMs = Math.max(
options.intervalMs ?? DEFAULT_BUNDLE_FRESHNESS_INTERVAL_MS,
MIN_BUNDLE_FRESHNESS_INTERVAL_MS,
);
let checking = false;
let disposed = false;
const check = async () => {
if (checking || disposed) {
return;
}
checking = true;
try {
const html = await fetchHtml(documentUrl);
if (!htmlReferencesBundle(html, options.currentModuleUrl, documentUrl)) {
console.warn(
`[${options.appName}] Loaded bundle is no longer the served entrypoint; reloading.`,
);
reload();
}
} catch {
// Keep the running app active if the entrypoint probe itself fails.
} finally {
checking = false;
}
};
const onVisible = () => {
if (document.visibilityState === 'visible') {
void check();
}
};
const timer = window.setInterval(() => {
void check();
}, intervalMs);
window.addEventListener('focus', check);
document.addEventListener('visibilitychange', onVisible);
return () => {
disposed = true;
window.clearInterval(timer);
window.removeEventListener('focus', check);
document.removeEventListener('visibilitychange', onVisible);
};
}
function normalizeUrlPath(value: string, baseUrl: string): string {
return new URL(value, baseUrl).pathname;
}
async function fetchDocumentHtml(url: string): Promise<string> {
const response = await fetch(url, {
cache: 'no-store',
credentials: 'same-origin',
headers: {
accept: 'text/html',
},
});
return response.text();
}

View File

@ -0,0 +1,145 @@
type HostedRuntimeUnavailableReason = 'missing' | 'not-live' | 'unauthorized';
export interface IHostedRuntimeRecoveryInput {
readonly runtimeRoot?: string | null;
readonly authorityRoot?: string | null;
readonly requestedRoot?: string | null;
readonly unavailableReason?: string | null;
readonly currentHref?: string | null;
}
export interface IHostedRuntimeRecoveryAction {
readonly key: 'retry' | 'auto' | 'shell';
readonly label: string;
readonly href: string;
readonly title: string;
}
export interface IHostedRuntimeRecoveryPlan {
readonly requestedRoot: string;
readonly runtimeRoot: string;
readonly authorityRoot: string;
readonly unavailableReason: HostedRuntimeUnavailableReason;
readonly summary: string;
readonly actions: IHostedRuntimeRecoveryAction[];
}
function normalize(value: string | null | undefined): string {
return typeof value === 'string' ? value.trim() : '';
}
function serializeRelativeUrl(url: URL): string {
return `${url.pathname}${url.search}${url.hash}`;
}
function resolveCurrentHref(currentHref?: string | null): string | null {
const direct = normalize(currentHref);
if (direct) {
return direct;
}
if (typeof window !== 'undefined' && typeof window.location?.href === 'string') {
return window.location.href;
}
return null;
}
function buildSummary(
requestedRoot: string,
runtimeRoot: string,
authorityRoot: string,
unavailableReason: HostedRuntimeUnavailableReason,
): string {
if (unavailableReason === 'unauthorized') {
const authorityLabel = authorityRoot || 'the signed-in authority';
return `Requested target ${requestedRoot} is not authorized for ${authorityLabel}.`;
}
const reasonText = unavailableReason === 'not-live'
? 'is registered but currently offline'
: 'is no longer registered';
if (requestedRoot === runtimeRoot) {
return `Requested runtime ${requestedRoot} ${reasonText}.`;
}
if (!runtimeRoot) {
const authorityLabel = authorityRoot || 'the selected authority';
return `Requested runtime ${requestedRoot} ${reasonText}; no live runtime is currently attached for ${authorityLabel}.`;
}
return `Requested runtime ${requestedRoot} ${reasonText}; clear the selected target before using ${runtimeRoot}.`;
}
export function resolveHostedRuntimeRecoveryPlan(
input: IHostedRuntimeRecoveryInput,
): IHostedRuntimeRecoveryPlan | null {
const requestedRoot = normalize(input.requestedRoot);
const runtimeRoot = normalize(input.runtimeRoot);
const authorityRoot = normalize(input.authorityRoot);
const unavailableReasonRaw = normalize(input.unavailableReason);
const unavailableReason = unavailableReasonRaw === 'missing'
|| unavailableReasonRaw === 'not-live'
|| unavailableReasonRaw === 'unauthorized'
? unavailableReasonRaw
: null;
if (!requestedRoot || !unavailableReason) {
return null;
}
const summary = buildSummary(requestedRoot, runtimeRoot, authorityRoot, unavailableReason);
const actions: IHostedRuntimeRecoveryAction[] = [];
const href = resolveCurrentHref(input.currentHref);
if (href) {
try {
const currentUrl = new URL(href, 'https://hivecast.ai');
const hasExplicitTarget = currentUrl.searchParams.has('daemonRoot') || currentUrl.searchParams.has('daemonRealm');
const relativeCurrentHref = serializeRelativeUrl(currentUrl);
actions.push({
key: 'retry',
label: 'Retry',
href: relativeCurrentHref,
title: 'Retry the current hosted app target.',
});
if (hasExplicitTarget) {
const autoUrl = new URL(currentUrl.toString());
autoUrl.searchParams.delete('daemonRoot');
autoUrl.searchParams.delete('daemonRealm');
actions.push({
key: 'auto',
label: 'Use Auto',
href: serializeRelativeUrl(autoUrl),
title: 'Clear the explicit runtime target and let HiveCast pick the current live target.',
});
}
actions.push({
key: 'shell',
label: 'Open Shell',
href: `/apps/web/#dashboard?redirect=${encodeURIComponent(relativeCurrentHref)}`,
title: 'Open the HiveCast shell and return to this app after choosing a target.',
});
return {
requestedRoot,
runtimeRoot,
authorityRoot,
unavailableReason,
summary,
actions,
};
} catch {
// Ignore malformed URLs and show shell-only recovery.
}
}
actions.push({
key: 'shell',
label: 'Open Shell',
href: '/apps/web/#dashboard',
title: 'Open the HiveCast shell to inspect or switch attached leaves.',
});
return {
requestedRoot,
runtimeRoot,
authorityRoot,
unavailableReason,
summary,
actions,
};
}

View File

@ -0,0 +1,139 @@
import type { MatrixAuthorityContext } from '@open-matrix/core';
type HostLike = {
getAttribute?: (name: string) => string | null;
dataset?: Record<string, string | undefined>;
} | null | undefined;
type HostLocatorLike = {
closest?: (selector: string) => unknown;
getRootNode?: () => unknown;
} | null | undefined;
export const HOSTED_RUNTIME_TARGET_CHANGED_EVENT = 'matrix:hosted-runtime-target-changed';
export interface IHostedRuntimeTargetInfo {
runtimeRoot: string;
runtimeInstanceRoot: string;
authorityRoot: string;
assetHostRoot: string;
platformServiceRoot: string;
sessionAuthorityRoot: string;
selectedAuthorityRoot: string;
platformRoot: string;
targetSource: string;
targetKind: string;
requestedRoot: string;
unavailableReason: string;
isAuthenticated: boolean;
isPlatformAdmin: boolean;
selectableAuthorityRoots: readonly string[];
}
function trimValue(value: string | null | undefined): string {
return typeof value === 'string' ? value.trim() : '';
}
function readBoolean(value: string | null | undefined): boolean {
return value === 'true';
}
function readCsv(value: string | null | undefined): readonly string[] {
if (typeof value !== 'string') {
return [];
}
return value.split(',').map((item) => item.trim()).filter((item) => item.length > 0);
}
export function resolveHostedRuntimeTargetInfo(host: HostLike): IHostedRuntimeTargetInfo {
const runtimeInstanceRoot = trimValue(
host?.dataset?.runtimeInstanceRoot
?? host?.getAttribute?.('runtime-root')
?? host?.dataset?.runtimeRoot
?? host?.dataset?.runtimeTargetRoot,
);
const selectedAuthorityRoot = trimValue(host?.dataset?.selectedAuthorityRoot);
const sessionAuthorityRoot = trimValue(host?.dataset?.sessionAuthorityRoot);
return {
runtimeRoot: runtimeInstanceRoot,
runtimeInstanceRoot,
authorityRoot: selectedAuthorityRoot || sessionAuthorityRoot,
assetHostRoot: trimValue(host?.dataset?.assetHostRoot),
platformServiceRoot: trimValue(host?.dataset?.platformServiceRoot),
sessionAuthorityRoot,
selectedAuthorityRoot,
platformRoot: trimValue(
host?.dataset?.platformServiceRoot
?? host?.getAttribute?.('bus-root')
?? host?.dataset?.platformRoot,
),
targetSource: trimValue(host?.dataset?.runtimeTargetSource),
targetKind: trimValue(host?.dataset?.runtimeTargetKind),
requestedRoot: trimValue(host?.dataset?.runtimeTargetRequestedRoot),
unavailableReason: trimValue(host?.dataset?.runtimeTargetUnavailableReason),
isAuthenticated: readBoolean(host?.dataset?.authenticated),
isPlatformAdmin: readBoolean(host?.dataset?.isPlatformAdmin),
selectableAuthorityRoots: readCsv(host?.dataset?.selectableAuthorityRoots),
};
}
export function hostedTargetAuthorityContext(info: IHostedRuntimeTargetInfo): MatrixAuthorityContext {
return {
assetHostRoot: info.assetHostRoot,
platformServiceRoot: info.platformServiceRoot || info.platformRoot,
sessionAuthorityRoot: info.sessionAuthorityRoot || null,
selectedAuthorityRoot: info.selectedAuthorityRoot || null,
runtimeInstanceRoot: info.runtimeInstanceRoot || info.runtimeRoot || null,
isAuthenticated: info.isAuthenticated,
isPlatformAdmin: info.isPlatformAdmin,
selectableAuthorityRoots: info.selectableAuthorityRoots,
};
}
export function resolveHostedRuntimeTargetHost(node: HostLocatorLike): HostLike {
let current = node;
const visited = new Set<unknown>();
while (current && !visited.has(current)) {
visited.add(current);
const direct = typeof current.closest === 'function'
? current.closest('matrix-dsl-host')
: null;
if (direct && typeof direct === 'object') {
return direct as HostLike;
}
const root = typeof current.getRootNode === 'function'
? current.getRootNode()
: null;
if (!root || typeof root !== 'object' || !('host' in root)) {
break;
}
current = (root as { host?: HostLocatorLike }).host ?? null;
}
return null;
}
export function hostedRuntimeTargetInfoEquals(
left: IHostedRuntimeTargetInfo,
right: IHostedRuntimeTargetInfo,
): boolean {
return (
left.runtimeRoot === right.runtimeRoot
&& left.runtimeInstanceRoot === right.runtimeInstanceRoot
&& left.authorityRoot === right.authorityRoot
&& left.assetHostRoot === right.assetHostRoot
&& left.platformServiceRoot === right.platformServiceRoot
&& left.sessionAuthorityRoot === right.sessionAuthorityRoot
&& left.selectedAuthorityRoot === right.selectedAuthorityRoot
&& left.platformRoot === right.platformRoot
&& left.targetSource === right.targetSource
&& left.targetKind === right.targetKind
&& left.requestedRoot === right.requestedRoot
&& left.unavailableReason === right.unavailableReason
&& left.isAuthenticated === right.isAuthenticated
&& left.isPlatformAdmin === right.isPlatformAdmin
&& left.selectableAuthorityRoots.length === right.selectableAuthorityRoots.length
&& left.selectableAuthorityRoots.every((value, index) => value === right.selectableAuthorityRoots[index])
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import { htmlReferencesBundle } from '../BundleFreshness';
describe('htmlReferencesBundle', () => {
it('accepts the currently served Vite entry module', () => {
const html = '<script type="module" crossorigin src="/apps/director/assets/index-new.js"></script>';
assert.equal(
htmlReferencesBundle(
html,
'https://dev.hivecast.ai/apps/director/assets/index-new.js',
'https://dev.hivecast.ai/apps/director/',
),
true,
);
});
it('accepts Vite dev HTML that imports the entrypoint from an inline module script', () => {
const html = '<script type="module">import "./index.ts";</script>';
assert.equal(
htmlReferencesBundle(
html,
'https://local.hivecast.ai/apps/web/index.ts',
'https://local.hivecast.ai/apps/web/',
),
true,
);
});
it('rejects an older content-hashed bundle', () => {
const html = '<script type="module" crossorigin src="/apps/director/assets/index-new.js"></script>';
assert.equal(
htmlReferencesBundle(
html,
'https://dev.hivecast.ai/apps/director/assets/index-old.js',
'https://dev.hivecast.ai/apps/director/',
),
false,
);
});
});

View File

@ -0,0 +1,92 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolveHostedRuntimeTargetInfo } from '../HostedRuntimeTarget';
const thisDir = path.dirname(fileURLToPath(import.meta.url));
describe('resolveHostedRuntimeTargetInfo', () => {
it('keeps asset/platform/session/selected roots separate', () => {
const host = {
getAttribute(name: string): string | null {
if (name === 'runtime-root') return 'test-user-1.DIRECTOR.SESSION-ABC';
if (name === 'authority-root') return 'SPACE-HIVECAST';
return null;
},
dataset: {
assetHostRoot: 'SPACE-HIVECAST',
platformServiceRoot: 'SPACE-HIVECAST',
sessionAuthorityRoot: 'test-user-1',
selectedAuthorityRoot: 'test-user-1',
selectableAuthorityRoots: 'test-user-1',
platformRoot: 'SPACE-HIVECAST',
authenticated: 'true',
isPlatformAdmin: 'false',
},
};
assert.deepEqual(resolveHostedRuntimeTargetInfo(host), {
runtimeRoot: 'test-user-1.DIRECTOR.SESSION-ABC',
runtimeInstanceRoot: 'test-user-1.DIRECTOR.SESSION-ABC',
authorityRoot: 'test-user-1',
assetHostRoot: 'SPACE-HIVECAST',
platformServiceRoot: 'SPACE-HIVECAST',
sessionAuthorityRoot: 'test-user-1',
selectedAuthorityRoot: 'test-user-1',
platformRoot: 'SPACE-HIVECAST',
targetSource: '',
targetKind: '',
requestedRoot: '',
unavailableReason: '',
isAuthenticated: true,
isPlatformAdmin: false,
selectableAuthorityRoots: ['test-user-1'],
});
});
it('requests browser NATS credentials for selectedAuthorityRoot, not sessionAuthorityRoot', () => {
const source = fs.readFileSync(path.join(thisDir, '..', 'MatrixDSLHost.ts'), 'utf8');
assert.match(source, /const selectedAuthorityRoot = \(this\.dataset\.selectedAuthorityRoot \?\? ''\)\.trim\(\);/);
assert.match(source, /: \(selectedAuthorityRoot \|\| requestedDaemonRoot\);/);
assert.doesNotMatch(source, /principalAuthorityRoot[\s\S]{0,220}requestedDaemonRoot === platformRoot/);
});
it('does not let addressRoot decide session or authority context roots', () => {
const source = fs.readFileSync(path.join(thisDir, '..', 'MatrixDSLHost.ts'), 'utf8');
assert.doesNotMatch(source, /bootstrap\.authenticated \? bootstrap\.addressRoot/);
assert.doesNotMatch(source, /const authorityRootService =[\s\S]{0,120}\|\| this\.dataset\.addressRoot/);
});
it('does not promote anonymous platform bootstrap into selectedAuthorityRoot', () => {
const source = fs.readFileSync(path.join(thisDir, '..', 'MatrixDSLHost.ts'), 'utf8');
assert.match(source, /parseBootstrapAuthorityContext\(bootstrap\.authorityContext\)/);
assert.match(source, /const selectedAuthorityRoot = authorityContext\.selectedAuthorityRoot \?\? '';/);
assert.doesNotMatch(source, /bootstrapSelectedAuthorityRoot/);
assert.doesNotMatch(source, /bootstrapAuthorityContextRoot/);
});
it('does not let runtime summary addressRoot re-home selectedAuthorityRoot', () => {
const source = fs.readFileSync(path.join(thisDir, '..', 'MatrixDSLHost.ts'), 'utf8');
assert.match(source, /_hostedAuthorityRootFromBootstrap/);
assert.doesNotMatch(source, /_selectHostedAuthorityRoot/);
assert.doesNotMatch(source, /summaryAuthorityRoot/);
assert.doesNotMatch(source, /summary\.authority\?\.addressRoot/);
});
it('skips Phase B platform probes for normal user authority sessions', () => {
const source = fs.readFileSync(path.join(thisDir, '..', 'MatrixDSLHost.ts'), 'utf8');
assert.match(source, /const isNormalUserAuthoritySession = bootstrapAuthenticated/);
assert.match(source, /userAuthorityRoot !== serviceRoot/);
assert.match(source, /Phase B platform probe skipped for user authority session/);
});
it('does not refresh bus tokens through a platform auth target for normal users', () => {
const source = fs.readFileSync(path.join(thisDir, '..', 'MatrixDSLHost.ts'), 'utf8');
assert.match(source, /_shouldSkipBusTokenRefreshForTarget\(authTarget\)/);
assert.match(source, /targetRoot !== userAuthorityRoot/);
assert.match(source, /Bus token refresh skipped for platform auth target in user authority session/);
});
});

View File

@ -0,0 +1,56 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
describe('MatrixDSLHost login-required bootstrap', () => {
it('does not auto-redirect matrix-web away from its own login/signup shell', () => {
const source = readFileSync(resolve(import.meta.dirname, '../MatrixDSLHost.ts'), 'utf8');
assert.match(source, /hostedAppName !== 'web'/);
assert.match(source, /this\._redirectToPlatformLogin\(\)/);
});
it('reserves device handoff for the edge Device shell', () => {
const source = readFileSync(resolve(import.meta.dirname, '../MatrixDSLHost.ts'), 'utf8');
assert.match(source, /_shouldRedirectToDeviceLogin\(hostedAppName/);
assert.match(source, /normalizedAppName === 'edge'/);
assert.match(source, /this\._redirectToDeviceLogin\(\)/);
});
it('keeps the NATS backbone available for authenticated Matrix Web namespace setup', () => {
const source = readFileSync(resolve(import.meta.dirname, '../MatrixDSLHost.ts'), 'utf8');
assert.match(source, /const natsWsUrl = natsWsAttr \?\? \(!discoveredNats \? backboneUrl : null\);/);
assert.doesNotMatch(source, /renderNamespaceSetupLocally\s*\?\s*null\s*:/);
});
it('does not query platform registry metadata for the Matrix Web shell itself', () => {
const source = readFileSync(resolve(import.meta.dirname, '../MatrixDSLHost.ts'), 'utf8');
assert.match(source, /const appName = this\._deriveHostedAppName\(\);[\s\S]*if \(!appName\) return;[\s\S]*if \(appName === 'web'\) return;/);
});
it('does not query platform registry metadata for standalone apps before a user Space is selected', () => {
const source = readFileSync(resolve(import.meta.dirname, '../MatrixDSLHost.ts'), 'utf8');
assert.match(source, /const platformServiceRoot = \(this\.dataset\.platformServiceRoot \?\? ''\)\.trim\(\);/);
assert.match(source, /if \(platformServiceRoot && selectedAuthorityRoot === platformServiceRoot && !isPlatformAdmin\) \{\s*return;\s*\}/);
});
it('does not claim platform service registry entries for browser apps before a user Space is selected', () => {
const source = readFileSync(resolve(import.meta.dirname, '../MatrixDSLHost.ts'), 'utf8');
assert.match(source, /this\._shouldSkipBrowserRuntimeRegistryClaim\(controlPlaneRoot\)/);
assert.match(source, /result: 'skipped-no-registry-authority'/);
assert.match(source, /return !candidateAuthorityRoots\.some\(\(root\) => root\.length > 0 && root !== platformRoot\);/);
});
it('does not claim browser runtime registry entries when this Host is not the authority coordinator', () => {
const source = readFileSync(resolve(import.meta.dirname, '../MatrixDSLHost.ts'), 'utf8');
assert.match(source, /bootstrapNodeRoleActive\(bootstrap\.nodeRoles, 'authorityCoordinator'\)/);
assert.match(source, /this\.dataset\.authorityCoordinatorActive === 'false'/);
});
});

View File

@ -0,0 +1,6 @@
/**
* App-ready signal emit and observe Matrix app readiness across
* shell, operational, and cognitive levels.
*/
export * from './AppReadySignal.js';

View File

@ -0,0 +1,112 @@
# Boundary Identifiers — In-Package Reference
If you're editing files in this package, you must know which identifier
you're touching. Full spec:
`WORKSTREAMS/platform-control-plane-freeze/FOUR-ROOTS-DISENTANGLE.md`
(working title preserved for grep continuity).
## The Four Boundary Identifiers
| Boundary name | What it identifies | Example | Where it crosses boundaries |
|---|---|---|---|
| **subjectScope** | NATS subject prefix for this Host's actors | `SPACE-HIVECAST` | `data-subject-scope` attr, `bootstrap.subjectScope` JSON field, `IBrowserBootstrapResponse.subjectScope` |
| **authority** | Who owns this surface (user/principal) | `test-user-1` | `data-authority` attr, `bootstrap.authority` JSON field, `IBrowserBootstrapResponse.authority` |
| **appName** | Which webapp this is | `director` | `data-app-name` attr, `bootstrap.appName` JSON field, `matrix.json:appName` |
| **tabRuntimeId** | This browser tab's runtime identity | `test-user-1.DIRECTOR.SESSION-A1B2C3D4` | `data-tab-runtime-id` attr, computed client-side from `authority` + `appName` + session id |
## The discipline: clear at the boundary, flexible inside
The names above are required at **interface boundaries** — anywhere code in
this package talks to code outside it. That means:
- HTTP bootstrap response fields
- HTML attributes on `<matrix-dsl-host>`
- Exported TypeScript interfaces, types, and function signatures
- matrix.json fields
Inside private functions and private fields of this package, local variable
names can use whatever feels natural for the surrounding code. The
discipline is at the seams.
## Rules
1. **Boundary-crossing values use the canonical boundary name.** When you
read a value from a config file, HTML attribute, JSON field, or exported
interface, you read the canonical name (`subjectScope`, `authority`,
`appName`, `tabRuntimeId`). You can rename it as a local variable inside
your function if you want; just make sure the boundary read happened
under the right name.
2. **One concept per boundary name.** Never write `setAttribute('root', ...)`
in new code. The `root` attribute is ambiguous — it has meant all four
identifiers at different times in git history. Pick the specific one.
3. **No fallbacks.** Per CLAUDE.md "no fallbacks or legacy crutches,"
if `appName` is missing from a bootstrap that needs it, **throw**.
Do not silently degrade to using `subjectScope` in the `appName` slot —
that's the wire-crossing bug this workstream exists to fix.
4. **Forbidden boundary names** (do not introduce in new code at boundaries):
- `root` — meant all four at different times
- `realm` — alias of `root`, same problem
- `baseRoot` — got passed in as appName but is named after subjectScope
- `rootAttr` — generic, ambiguous
- `daemonRoot` / `daemonRealm` — pre-Host-Service legacy
- `busRoot` / `authorityRoot` / `runtimeRoot` — these were in-flight rename
drafts; superseded by `subjectScope` / `authority` / `tabRuntimeId`
- `transportRoot` — classify each site; if the value has `.SESSION-XXXX`,
the boundary name is `tabRuntimeId`; otherwise it's `subjectScope`
- `spaceRoot` — use `authority`
- `appRoot` — use `appName` or `tabRuntimeId` depending on which
5. **Allowed legacy aliases (compat only at boundaries, one release):**
- `bootstrap.root` — still emitted for one release; readers must migrate
- `bootstrap.platformRoot` — synonym of `subjectScope` during transition
- `bootstrap.addressRoot` — synonym of `authority` during transition
- `bootstrap.busRoot` / `bootstrap.authorityRoot` / `bootstrap.runtimeRoot`
in-flight rename names from earlier drafts; emit both old and new for
one release so any consumer already coded against them keeps working
## HTML attribute writes are intent-explicit
When you write attributes during browser bootstrap, write the specific
canonical attribute name:
```ts
this.setAttribute('data-subject-scope', subjectScope); // not 'root', not 'realm'
this.setAttribute('data-authority', authority);
this.setAttribute('data-app-name', appName);
this.setAttribute('data-tab-runtime-id', tabRuntimeId);
```
The legacy `root` attribute is still written during the transition release
(transport readers in older code path still consume it as `subjectScope` in
hosted mode). New code reads `data-subject-scope` instead.
## Why this exists
Through git history, "root" has meant all four of these identifiers at
different times. `commit d73fc73ee` (May 14, 2026) introduced `busRoot`
as a separate bootstrap field when Space-claimed users couldn't load the
platform shell. That fix worked for transport, but the HTML attribute
`root` is read by THREE consumers expecting THREE different meanings, so
writing `busRoot` into it broke tab identity downstream
(`tab_realm.ts:generateIdentityTabRoot`, whose `appName` parameter was being
fed `busRoot` until this workstream landed).
The lesson: **at boundaries, names that say what they mean prevent the
wire-crossing class of bug.** Internal variable names can stay flexible;
the boundary names cannot.
## Where each identifier comes from
- **subjectScope** — server-side, derived from `status.transport.root` of
whichever Host's gateway answered the HTTP request
- **authority** — server-side, the signed-in principal's
`primary_authority_root` (or, on Device hosts, the Device's linked
authority from `host-auth.json`)
- **appName** — server-side, derived from the route's app context
(`/apps/director/``director`); declared in the package's `matrix.json`
- **tabRuntimeId** — client-side, generated as
`${authority}.${appName.toUpperCase()}.SESSION-${sessionId}`,
persisted in sessionStorage

View File

@ -0,0 +1,10 @@
/**
* Browser host element + control actor.
*
* The MatrixDSLHost custom element bootstraps a browser tab's Matrix
* runtime, transport, and root context. Importing this module registers
* the `<matrix-dsl-host>` custom element as a side effect.
*/
export * from './MatrixDSLHost.js';
export * from './BrowserRuntimeControlActor.js';

View File

@ -0,0 +1,23 @@
/**
* @open-matrix/browser-host
*
* Browser-host shell for Matrix custom elements. Extracted from
* @open-matrix/core during the core-decontamination workstream to stop
* MatrixDSLHost changes from rebuilding the actor kernel.
*
* Phase 1 (this commit): package shell only. The actual files still live
* in `@open-matrix/core/browser/*` and are re-exported here so consumer
* migrations can start in parallel with the move. Phase 1 commit 3 moves
* the files and removes the core copies.
*
* Use subpath imports when possible:
* from '@open-matrix/browser-host/host' host element + control
* from '@open-matrix/browser-host/recovery' recovery + target helpers
* from '@open-matrix/browser-host/app-ready' readiness signal
*/
export * from './BundleFreshness.js';
export * from './host.js';
export * from './recovery.js';
export * from './app-ready.js';
export * from './profile-trace.js';

View File

@ -0,0 +1,5 @@
/**
* Browser profile tracing helpers.
*/
export * from './BrowserProfileTrace.js';

View File

@ -0,0 +1,6 @@
/**
* Hosted-runtime recovery and target-info helpers.
*/
export * from './HostedRuntimeRecovery.js';
export * from './HostedRuntimeTarget.js';

View File

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"outDir": "dist",
"declaration": true,
"declarationMap": false,
"sourceMap": false
},
"exclude": ["src/__tests__/**/*.ts"]
}

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022", "DOM"],
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"useDefineForClassFields": true
},
"include": ["src"]
}

View File

@ -0,0 +1,23 @@
#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import { createRequire } from 'node:module';
import { cpSync, rmSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const packageDir = dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
const typescriptPackageJson = require.resolve('typescript/package.json', { paths: [packageDir] });
const tscBin = resolve(dirname(typescriptPackageJson), 'bin/tsc');
rmSync(resolve(packageDir, 'dist'), { recursive: true, force: true });
execFileSync(process.execPath, [tscBin, '-p', resolve(packageDir, 'tsconfig.json')], {
cwd: packageDir,
stdio: 'inherit',
});
execFileSync(process.execPath, [resolve(packageDir, 'fix-esm-imports.mjs')], {
cwd: packageDir,
stdio: 'inherit',
});
cpSync(resolve(packageDir, 'src/theme/base.css'), resolve(packageDir, 'dist/theme/base.css'));
cpSync(resolve(packageDir, 'src/theme/tokens.css'), resolve(packageDir, 'dist/theme/tokens.css'));

View File

@ -0,0 +1,59 @@
#!/usr/bin/env node
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
import { dirname, extname, join, resolve } from 'node:path';
const distDir = new URL('./dist/', import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1');
let fixed = 0;
let files = 0;
function processFile(filePath) {
if (!filePath.endsWith('.js') && !filePath.endsWith('.d.ts')) return;
files += 1;
const fileDir = dirname(filePath);
const content = readFileSync(filePath, 'utf8');
let changed = false;
const addExt = (match, prefix, specifier, suffix) => {
const ext = extname(specifier);
if (['.js', '.mjs', '.cjs', '.json', '.css'].includes(ext)) {
return match;
}
const resolved = resolve(fileDir, specifier);
const fileCandidates = [`${resolved}.js`, `${resolved}.mjs`, `${resolved}.cjs`, `${resolved}.d.ts`];
if (fileCandidates.some((candidate) => existsSync(candidate))) {
changed = true;
return `${prefix}${specifier}.js${suffix}`;
}
if (existsSync(resolved) && statSync(resolved).isDirectory()) {
changed = true;
return `${prefix}${specifier}/index.js${suffix}`;
}
changed = true;
return `${prefix}${specifier}.js${suffix}`;
};
const updated = content
.replace(/(from\s+['"])(\.\.?\/[^'"]+)(['"])/g, addExt)
.replace(/(import\(['"])(\.\.?\/[^'"]+)(['"]\))/g, addExt)
.replace(/(import\s+['"])(\.\.?\/[^'"]+)(['"])/g, addExt);
if (changed) {
writeFileSync(filePath, updated);
fixed += 1;
}
}
function walk(dir) {
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) {
walk(full);
} else {
processFile(full);
}
}
}
walk(distDir);
console.log(`Fixed ${fixed}/${files} files with .js extensions`);

View File

@ -0,0 +1,64 @@
{
"name": "@open-matrix/browser-kit",
"version": "0.1.0",
"description": "Shared browser infrastructure for Matrix webapps — shims, theme tokens, vite config factory, app shell",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./shims/*": {
"types": "./dist/shims/*.d.ts",
"import": "./dist/shims/*.js",
"default": "./dist/shims/*.js"
},
"./theme": {
"types": "./dist/theme/index.d.ts",
"import": "./dist/theme/index.js",
"default": "./dist/theme/index.js"
},
"./theme/base.css": "./dist/theme/base.css",
"./theme/tokens.css": "./dist/theme/tokens.css",
"./vite": {
"types": "./dist/vite/index.d.ts",
"import": "./dist/vite/index.js",
"default": "./dist/vite/index.js"
},
"./components/MxAppShell": {
"types": "./dist/components/MxAppShell.d.ts",
"import": "./dist/components/MxAppShell.js",
"default": "./dist/components/MxAppShell.js"
},
"./package.json": "./package.json"
},
"files": [
"dist",
"package.json"
],
"scripts": {
"build": "node build.mjs",
"test": "node --input-type=module -e \"const kit = await import('./dist/vite/index.js'); if (typeof kit.createMatrixViteConfig !== 'function') throw new Error('createMatrixViteConfig export missing');\"",
"type-check": "tsc -p tsconfig.json --noEmit"
},
"peerDependencies": {
"@open-matrix/core": "*",
"vite": "^5.0.0"
},
"peerDependenciesMeta": {
"@open-matrix/core": {
"optional": true
},
"vite": {
"optional": true
}
},
"devDependencies": {
"@open-matrix/core": "workspace:*",
"typescript": "^5.7.0",
"vite": "^5.4.21"
}
}

View File

@ -0,0 +1,153 @@
/**
* MxAppShell shared page layout with header and status bar.
*
* Provides consistent chrome for all Matrix webapps:
* - Top header with app title, icon, and NATS connection indicator
* - Status bar at bottom with daemon root, user identity, connection state
* - Content slot for the app's root component
*
* Usage in index.html:
* <mx-app-shell title="Harness" icon="science">
* <mx-harness-app slot="content" name="harness"></mx-harness-app>
* </mx-app-shell>
*
* Or wrap programmatically the shell injects theme tokens automatically.
*/
export class MxAppShell extends HTMLElement {
private _connected = false;
private _daemonRoot = '';
private _principalId = '';
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback(): void {
this._connected = true;
this._render();
this._watchConnection();
}
disconnectedCallback(): void {
this._connected = false;
}
private _render(): void {
if (!this.shadowRoot) return;
const title = this.getAttribute('title') || 'Matrix';
const icon = this.getAttribute('icon') || '';
this.shadowRoot.innerHTML = `
<style>
:host {
display: flex;
flex-direction: column;
height: 100%;
font-family: var(--mx-font-family, system-ui, -apple-system, sans-serif);
color: var(--mx-color-text, #f1f5f9);
background: var(--mx-color-surface, #0f172a);
}
.shell-content {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.shell-status-bar {
display: flex;
align-items: center;
padding: 4px 12px;
background: var(--mx-color-surface-inset, #020617);
border-top: 1px solid var(--mx-color-border-light, #1e293b);
font-size: var(--mx-font-size-xs, 10px);
color: var(--mx-color-text-muted, #475569);
gap: 12px;
flex-shrink: 0;
height: 24px;
}
.status-item {
display: flex;
align-items: center;
gap: 4px;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--mx-color-success, #22c55e);
}
.status-dot.disconnected {
background: var(--mx-color-danger, #ef4444);
}
.status-spacer { flex: 1; }
.status-label { color: var(--mx-color-text-secondary, #94a3b8); }
</style>
<div class="shell-content">
<slot name="content"></slot>
<slot></slot>
</div>
<div class="shell-status-bar">
<div class="status-item">
<span class="status-dot" id="conn-dot"></span>
<span id="conn-label">Connecting...</span>
</div>
<div class="status-item">
<span class="status-label">Root:</span>
<span id="daemon-root"></span>
</div>
<span class="status-spacer"></span>
<div class="status-item">
<span class="status-label">User:</span>
<span id="principal-id"></span>
</div>
<div class="status-item">
<span class="status-label">${title}</span>
</div>
</div>
`;
}
private _watchConnection(): void {
// Poll for DSL host connection state
const interval = setInterval(() => {
if (!this._connected) { clearInterval(interval); return; }
const dslHost = this.closest('matrix-dsl-host') ?? document.querySelector('matrix-dsl-host');
const daemonRoot = dslHost?.getAttribute('daemon-root') ?? '';
const dot = this.shadowRoot?.querySelector('#conn-dot') as HTMLElement;
const label = this.shadowRoot?.querySelector('#conn-label') as HTMLElement;
const rootEl = this.shadowRoot?.querySelector('#daemon-root') as HTMLElement;
if (daemonRoot && daemonRoot.trim().length > 0) {
this._daemonRoot = daemonRoot;
if (dot) dot.classList.remove('disconnected');
if (label) label.textContent = 'Connected';
if (rootEl) rootEl.textContent = daemonRoot;
} else {
if (dot) dot.classList.add('disconnected');
if (label) label.textContent = 'Connecting...';
}
}, 2000);
}
updatePrincipal(principalId: string): void {
this._principalId = principalId;
const el = this.shadowRoot?.querySelector('#principal-id') as HTMLElement;
if (el) el.textContent = principalId;
}
}
if (typeof customElements !== 'undefined' && !customElements.get('mx-app-shell')) {
customElements.define('mx-app-shell', MxAppShell);
}

1
packages/browser-kit/src/css.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module '*.css';

View File

@ -0,0 +1,13 @@
/**
* @open-matrix/browser-kit shared browser infrastructure for Matrix webapps.
*
* Provides:
* - registerMatrixElements() centralized custom element registration
* - MxAppShell shared page layout with header + status bar
* - Theme tokens import '@open-matrix/browser-kit/theme'
* - Vite config factory import { createMatrixViteConfig } from '@open-matrix/browser-kit/vite'
* - Browser shims import '@open-matrix/browser-kit/shims/crypto' etc.
*/
export { registerMatrixElements } from './register';
export { MxAppShell } from './components/MxAppShell';

View File

@ -0,0 +1,24 @@
/**
* Centralized custom element registration for Matrix webapps.
*
* Usage:
* import { registerMatrixElements } from '@open-matrix/browser-kit';
* import { MxSplit } from '@open-matrix/core';
* import { MyApp } from './MyApp';
*
* registerMatrixElements([
* ['my-app', MyApp],
* ['mx-split', MxSplit],
* ]);
*/
export function registerMatrixElements(
elements: Array<[string, CustomElementConstructor]>,
): void {
if (typeof customElements === 'undefined') return;
for (const [tag, cls] of elements) {
if (!customElements.get(tag)) {
customElements.define(tag, cls);
}
}
}

View File

@ -0,0 +1,31 @@
function unsupported(name: string): never {
throw new Error(`[director/browser-shim] ${name} is not available in the browser`);
}
export function spawn(): never {
return unsupported('child_process.spawn');
}
export function spawnSync(): never {
return unsupported('child_process.spawnSync');
}
export function execSync(): never {
return unsupported('child_process.execSync');
}
export function execFileSync(): never {
return unsupported('child_process.execFileSync');
}
export function execFile(): never {
return unsupported('child_process.execFile');
}
export default {
spawn,
spawnSync,
execSync,
execFileSync,
execFile,
};

View File

@ -0,0 +1,92 @@
type HashLike = {
update(input: string | Uint8Array, encoding?: string): HashLike;
digest(encoding?: 'hex'): string | Uint8Array;
};
function toBytes(input: string | Uint8Array, encoding?: string): Uint8Array {
if (input instanceof Uint8Array) return input;
if (encoding && encoding !== 'utf8' && encoding !== 'utf-8') {
throw new Error(`[director/browser-shim] Unsupported hash encoding: ${encoding}`);
}
return new TextEncoder().encode(input);
}
function simpleHash(bytes: Uint8Array): string {
let h1 = 0x811c9dc5;
let h2 = 0x01000193;
for (const byte of bytes) {
h1 ^= byte;
h1 = Math.imul(h1, 0x01000193);
h2 ^= byte;
h2 = Math.imul(h2, 0x27d4eb2d);
}
const a = (h1 >>> 0).toString(16).padStart(8, '0');
const b = (h2 >>> 0).toString(16).padStart(8, '0');
return `${a}${b}${a}${b}${a}${b}${a}${b}`;
}
export function createHash(_algorithm = 'sha256'): HashLike {
const chunks: Uint8Array[] = [];
return {
update(input: string | Uint8Array, encoding?: string): HashLike {
chunks.push(toBytes(input, encoding));
return this;
},
digest(encoding?: 'hex'): string | Uint8Array {
const size = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const merged = new Uint8Array(size);
let offset = 0;
for (const chunk of chunks) {
merged.set(chunk, offset);
offset += chunk.length;
}
const hex = simpleHash(merged);
if (!encoding || encoding === 'hex') return hex;
return new TextEncoder().encode(hex);
},
};
}
export function randomBytes(size: number): Uint8Array {
const out = new Uint8Array(size);
const nativeCrypto = globalThis.crypto;
if (nativeCrypto?.getRandomValues) {
nativeCrypto.getRandomValues(out);
return out;
}
for (let i = 0; i < out.length; i += 1) {
out[i] = Math.floor(Math.random() * 256);
}
return out;
}
export function randomUUID(): string {
const nativeCrypto = globalThis.crypto;
if (nativeCrypto?.randomUUID) return nativeCrypto.randomUUID();
const bytes = randomBytes(16);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));
return [
hex.slice(0, 4).join(''),
hex.slice(4, 6).join(''),
hex.slice(6, 8).join(''),
hex.slice(8, 10).join(''),
hex.slice(10, 16).join(''),
].join('-');
}
const shimCrypto = {
createHash,
randomBytes,
randomUUID,
getRandomValues<T extends ArrayBufferView | null>(array: T): T {
if (array && 'byteLength' in array) {
const view = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
view.set(randomBytes(view.length));
}
return array;
},
};
export default shimCrypto;

View File

@ -0,0 +1 @@
export { readFile, writeFile, mkdir, default } from './fs';

View File

@ -0,0 +1,21 @@
function unsupported(name: string): never {
throw new Error(`[director/browser-shim] ${name} is not available in the browser`);
}
export async function readFile(): Promise<string> {
return unsupported('fs.readFile');
}
export async function writeFile(): Promise<void> {
return unsupported('fs.writeFile');
}
export async function mkdir(): Promise<void> {
return unsupported('fs.mkdir');
}
export default {
readFile,
writeFile,
mkdir,
};

View File

@ -0,0 +1,3 @@
export { MatrixActorHtmlElement } from '@open-matrix/core/framework/MatrixActorHtmlElement';
export { RequestReply } from '@open-matrix/core/engine/remoting/RequestReply';
export { MxSplit } from '@open-matrix/core/framework/components/MxSplit';

View File

@ -0,0 +1,26 @@
export function join(...parts: string[]): string {
return parts.filter(Boolean).join('/').replace(/\/+/g, '/');
}
export function resolve(...parts: string[]): string {
return join(...parts);
}
export function dirname(path: string): string {
const normalized = path.replace(/\/+/g, '/');
const index = normalized.lastIndexOf('/');
return index <= 0 ? '.' : normalized.slice(0, index);
}
export function basename(path: string): string {
const normalized = path.replace(/\/+/g, '/');
const index = normalized.lastIndexOf('/');
return index >= 0 ? normalized.slice(index + 1) : normalized;
}
export default {
join,
resolve,
dirname,
basename,
};

View File

@ -0,0 +1,45 @@
/**
* Matrix Base Styles applied globally by <mx-app-shell> or imported directly.
* Provides reset, scrollbar styling, and utility classes.
*/
/* ── Reset ────────────────────────────────────────────────────────── */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
height: 100%;
font-family: var(--mx-font-family, system-ui, -apple-system, sans-serif);
font-size: var(--mx-font-size-base, 13px);
color: var(--mx-color-text, #f1f5f9);
background: var(--mx-color-surface, #0f172a);
-webkit-font-smoothing: antialiased;
}
/* ── Scrollbar ────────────────────────────────────────────────────── */
::-webkit-scrollbar {
width: var(--mx-scrollbar-width, 6px);
height: var(--mx-scrollbar-width, 6px);
}
::-webkit-scrollbar-track {
background: var(--mx-scrollbar-track, #020617);
}
::-webkit-scrollbar-thumb {
background: var(--mx-scrollbar-thumb, #334155);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--mx-scrollbar-thumb-hover, #1e293b);
}
/* ── Links ────────────────────────────────────────────────────────── */
a {
color: var(--mx-color-link, #82aaff);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}

View File

@ -0,0 +1,8 @@
/**
* Matrix Theme import to inject CSS tokens and base styles.
*
* Usage in your app's index.ts:
* import '@open-matrix/browser-kit/theme';
*/
import './tokens.css';
import './base.css';

View File

@ -0,0 +1,68 @@
/**
* Matrix Design Tokens unified CSS custom properties for all Matrix webapps.
*
* Based on Chat's --mx-color-* system, extended with layout and typography tokens.
* Import this in your app root or let <mx-app-shell> inject it automatically.
*
* Usage in components:
* background: var(--mx-color-surface);
* color: var(--mx-color-text);
* border: 1px solid var(--mx-color-border);
*/
:root {
/* ── Surface colors ────────────────────────────────────────────────── */
--mx-color-surface: #0f172a;
--mx-color-surface-alt: #1e293b;
--mx-color-surface-inset: #020617;
--mx-color-surface-hover: #334155;
--mx-color-surface-card: #16213e;
/* ── Text colors ───────────────────────────────────────────────────── */
--mx-color-text: #f1f5f9;
--mx-color-text-secondary: #94a3b8;
--mx-color-text-muted: #475569;
--mx-color-link: #82aaff;
/* ── Semantic colors ───────────────────────────────────────────────── */
--mx-color-primary: #3b82f6;
--mx-color-primary-hover: #60a5fa;
--mx-color-accent: #e94560;
--mx-color-success: #22c55e;
--mx-color-warning: #f59e0b;
--mx-color-danger: #ef4444;
--mx-color-error: #ef4444;
/* ── Border colors ─────────────────────────────────────────────────── */
--mx-color-border: #334155;
--mx-color-border-light: #1e293b;
--mx-color-focus-ring: #3b82f6;
/* ── Typography ────────────────────────────────────────────────────── */
--mx-font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
--mx-font-mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace;
--mx-font-size-xs: 10px;
--mx-font-size-sm: 11px;
--mx-font-size-base: 13px;
--mx-font-size-md: 14px;
--mx-font-size-lg: 18px;
/* ── Radius ────────────────────────────────────────────────────────── */
--mx-radius-sm: 3px;
--mx-radius-md: 6px;
--mx-radius-lg: 8px;
--mx-radius-full: 9999px;
/* ── Spacing ───────────────────────────────────────────────────────── */
--mx-space-xs: 4px;
--mx-space-sm: 8px;
--mx-space-md: 12px;
--mx-space-lg: 16px;
--mx-space-xl: 20px;
/* ── Scrollbar ─────────────────────────────────────────────────────── */
--mx-scrollbar-width: 6px;
--mx-scrollbar-track: var(--mx-color-surface-inset);
--mx-scrollbar-thumb: var(--mx-color-border);
--mx-scrollbar-thumb-hover: var(--mx-color-border-light);
}

View File

@ -0,0 +1,81 @@
/**
* Matrix Vite Config Factory generates a correct vite.config.ts for any Matrix webapp.
*
* Usage in your app's vite.config.ts:
* import { createMatrixViteConfig } from '@open-matrix/browser-kit/vite';
* export default createMatrixViteConfig({ appName: 'harness' });
*/
import { defineConfig, loadEnv } from 'vite';
import { resolve, dirname } from 'path';
export interface MatrixViteConfigOptions {
appName: string;
root?: string;
outDir?: string;
daemonPort?: number;
extraAliases?: Record<string, string>;
extraExternals?: string[];
}
export function createMatrixViteConfig(options: MatrixViteConfigOptions, callerDir: string) {
const {
appName,
root = 'src',
outDir = 'dist',
daemonPort = 3100,
} = options;
const browserKitShims = resolve(dirname(new URL(import.meta.url).pathname), '../shims');
return defineConfig(({ command, mode }) => {
const env = loadEnv(mode, callerDir, '');
const daemonOrigin = (env.MATRIX_DAEMON_ORIGIN || `http://127.0.0.1:${daemonPort}`).replace(/\/$/, '');
return {
root: resolve(callerDir, root),
publicDir: false,
base: `/apps/${appName}/`,
build: {
outDir: resolve(callerDir, outDir),
emptyOutDir: true,
rollupOptions: {
input: resolve(callerDir, root, 'index.html'),
},
target: 'es2022',
chunkSizeWarningLimit: 800,
},
resolve: {
alias: {
'@open-matrix/core': resolve(browserKitShims, 'open-matrix-core.ts'),
crypto: resolve(browserKitShims, 'crypto.ts'),
'node:crypto': resolve(browserKitShims, 'crypto.ts'),
child_process: resolve(browserKitShims, 'child_process.ts'),
'node:child_process': resolve(browserKitShims, 'child_process.ts'),
'fs/promises': resolve(browserKitShims, 'fs-promises.ts'),
'node:fs/promises': resolve(browserKitShims, 'fs-promises.ts'),
fs: resolve(browserKitShims, 'fs.ts'),
'node:fs': resolve(browserKitShims, 'fs.ts'),
path: resolve(browserKitShims, 'path.ts'),
'node:path': resolve(browserKitShims, 'path.ts'),
...options.extraAliases,
},
},
...(command === 'serve'
? {
server: {
proxy: {
'/.matrix': { target: daemonOrigin, changeOrigin: true },
'/api': { target: daemonOrigin, changeOrigin: true },
'/auth': { target: daemonOrigin, changeOrigin: true },
'/login': { target: daemonOrigin, changeOrigin: true },
'/logout': { target: daemonOrigin, changeOrigin: true },
'/healthz': { target: daemonOrigin, changeOrigin: true },
'/nats-ws': { target: daemonOrigin, changeOrigin: true, ws: true },
},
},
}
: {}),
};
});
}

View File

@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"strict": true,
"lib": ["ES2022", "DOM"],
"noEmit": false,
"tsBuildInfoFile": "dist/.tsbuildinfo"
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts"
],
"exclude": [
"dist",
"node_modules"
]
}

22
packages/cli/CONTRACT.md Normal file
View File

@ -0,0 +1,22 @@
# @open-matrix/cli Contract
`@open-matrix/cli` is scoped to package-author development.
It may depend on:
- `@open-matrix/core`
- other packages in the SDK package set, when needed
- Node standard library modules
It must not depend on:
- `@matrix/mx-cli`
- `@open-matrix/host-service`
- `@open-matrix/host-control`
- `@open-matrix/system-*`
- `@open-matrix/system-auth`
- `@open-matrix/system-gateway-http`
- `@open-matrix/nats-auth`
- `hivecast`
User packages must depend on SDK libraries, not on this CLI package.

23
packages/cli/README.md Normal file
View File

@ -0,0 +1,23 @@
# @open-matrix/cli
Package-author CLI for the Matrix SDK layer.
This package owns the SDK/package-author `matrix` binary. It does not depend on
Matrix-3 product packages, Host Service, Host Control, system actors, HiveCast,
Device Link, or NATS authority packages.
Initial command surface:
```bash
matrix init [dir]
matrix add actor <name>
matrix actor <name>
matrix configure --key <key> --cloud <url> --space <space>
matrix run <entry> --mount <mount>
matrix invoke <mount> <op> [json]
```
`matrix run` starts a standalone actor runner in the current folder. `matrix
invoke` sends a Matrix request envelope to that runner through its local control
port. Managed Runtime Host promotion belongs to the later Matrix-3/Runtime Host
repoint phase.

40
packages/cli/package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "@open-matrix/cli",
"version": "0.1.0",
"description": "Package-author Matrix CLI for SDK projects.",
"type": "module",
"private": false,
"bin": {
"matrix": "./dist/bin/matrix.js"
},
"exports": {
".": {
"types": "./dist/cli.d.ts",
"import": "./dist/cli.js",
"default": "./dist/cli.js"
},
"./package.json": "./package.json"
},
"files": [
"dist",
"package.json",
"README.md",
"CONTRACT.md"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "node --test dist/__tests__/*.test.js",
"type-check": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@open-matrix/core": "workspace:*"
},
"devDependencies": {
"@types/node": "^25.0.10",
"typescript": "^5.7.0"
},
"license": "MIT",
"engines": {
"node": ">=20"
}
}

View File

@ -0,0 +1,105 @@
import assert from 'node:assert/strict';
import { mkdir, mkdtemp, readFile, stat, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { main } from '../cli.js';
test('init, add actor, and configure write package-local SDK state', async () => {
const root = await mkdtemp(join(tmpdir(), 'matrix-sdk-cli-test.'));
await main(['init', root, '--name', 'demo-package']);
await main(['add', 'actor', 'greeter', '--dir', root, '--mount', 'demo.greeter']);
await main([
'configure',
'--dir', root,
'--key', 'test-key',
'--cloud', 'https://example.invalid',
'--space', 'demo-space',
]);
const manifest = JSON.parse(await readFile(join(root, 'matrix.json'), 'utf8'));
const packageJson = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
assert.equal(packageJson.private, undefined);
assert.equal(packageJson.dependencies['@open-matrix/core'], '^0.1.0');
assert.equal(manifest.name, 'demo-package');
assert.equal(manifest.runtime.entry, './dist/index.js');
assert.deepEqual(manifest.runtime.factory, {
kind: 'factory',
export: 'createStandaloneGreeterActor',
instance: {
id: 'RUNTIME-HOST-DEMO-GREETER',
class: 'service',
rootKind: 'component',
componentType: 'GreeterActor',
autoStart: true,
},
});
assert.deepEqual(manifest.components, [{
type: 'GreeterActor',
export: 'GreeterActor',
mount: 'demo.greeter',
surface: 'headless',
accepts: ['echo', 'getStatus'],
}]);
const actorSource = await readFile(join(root, 'src', 'GreeterActor.ts'), 'utf8');
assert.match(actorSource, /from '@open-matrix\/core'/);
const factorySource = await readFile(join(root, 'src', 'create-standalone-GreeterActor.ts'), 'utf8');
assert.match(factorySource, /createStandaloneGreeterActor/);
assert.match(factorySource, /runtime\.createSupervised\(GreeterActor, mount\)/);
const credential = JSON.parse(await readFile(join(root, '.matrix', 'credentials', 'matrix-api-key.json'), 'utf8'));
assert.equal(credential.key, 'test-key');
assert.equal(credential.cloud, 'https://example.invalid');
assert.equal(credential.space, 'demo-space');
const credentialMode = (await stat(join(root, '.matrix', 'credentials', 'matrix-api-key.json'))).mode & 0o777;
assert.equal(credentialMode, 0o600);
});
test('up delegates a Host-runnable package to an explicit Runtime Host command', async () => {
const root = await mkdtemp(join(tmpdir(), 'matrix-sdk-cli-up-test.'));
const fakeRuntimeHost = join(root, 'fake-runtime-host.mjs');
const calledPath = join(root, 'runtime-host-called.json');
await main(['init', root, '--name', 'demo-package']);
await main(['add', 'actor', 'greeter', '--dir', root, '--mount', 'demo.greeter']);
await mkdir(join(root, 'dist'), { recursive: true });
await writeFile(join(root, 'dist', 'index.js'), 'export function createStandaloneGreeterActor() {}\n', 'utf8');
await writeFile(fakeRuntimeHost, `
import { writeFileSync } from 'node:fs';
writeFileSync(${JSON.stringify(calledPath)}, JSON.stringify({
argv: process.argv.slice(2),
cwd: process.cwd(),
}, null, 2));
`, 'utf8');
await main([
'configure',
'--dir', root,
'--key', 'test-key',
'--cloud', 'https://example.invalid',
'--space', 'demo-space',
'--runtime-host-bin', fakeRuntimeHost,
'--runtime-host-routing', 'workspace-source',
]);
await main(['up', '--dir', root, '--json']);
const call = JSON.parse(await readFile(calledPath, 'utf8'));
assert.equal(call.cwd, root);
assert.deepEqual(call.argv.slice(0, 3), ['--workspace-source', 'up', root]);
assert.ok(call.argv.includes('--json'));
});
test('add actor preserves explicit Actor class suffix', async () => {
const root = await mkdtemp(join(tmpdir(), 'matrix-sdk-cli-actor-suffix-test.'));
await main(['init', root, '--name', 'demo-package']);
await main(['add', 'actor', 'GreeterActor', '--dir', root, '--mount', 'demo.greeter']);
const manifest = JSON.parse(await readFile(join(root, 'matrix.json'), 'utf8'));
assert.equal(manifest.runtime.factory.export, 'createStandaloneGreeterActor');
assert.equal(manifest.runtime.factory.instance.componentType, 'GreeterActor');
assert.equal(manifest.components[0].type, 'GreeterActor');
await stat(join(root, 'src', 'GreeterActor.ts'));
await stat(join(root, 'src', 'create-standalone-GreeterActor.ts'));
});

View File

@ -0,0 +1,8 @@
#!/usr/bin/env node
import { main } from '../cli.js';
main(process.argv.slice(2)).catch((error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
console.error(message);
process.exitCode = 1;
});

796
packages/cli/src/cli.ts Normal file
View File

@ -0,0 +1,796 @@
import { spawnSync } from 'node:child_process';
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
import { mkdirSync, readFileSync, writeFileSync, chmodSync, existsSync, statSync } from 'node:fs';
import { basename, dirname, resolve, join } from 'node:path';
import { pathToFileURL } from 'node:url';
import {
InMemoryBroker,
InMemoryTransport,
MatrixActor,
MatrixRuntime,
TopicRouter,
type Unsub,
} from '@open-matrix/core';
type CommandOptions = Record<string, string | boolean>;
type ActorConstructor = new () => MatrixActor;
type PackageBootstrapFunction = (...args: readonly unknown[]) => Promise<unknown> | unknown;
interface ParsedArgs {
readonly positional: string[];
readonly options: CommandOptions;
}
interface RunStatus {
readonly pid: number;
readonly host: string;
readonly port: number;
readonly mount: string;
readonly entry: string;
readonly exportName: string;
readonly startedAt: string;
}
interface PackageRunSpec {
readonly packageDir: string;
readonly entryPath: string;
readonly exportName: string;
readonly mount: string;
}
function parseArgs(args: string[]): ParsedArgs {
const positional: string[] = [];
const options: CommandOptions = {};
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (!arg.startsWith('--')) {
positional.push(arg);
continue;
}
const withoutPrefix = arg.slice(2);
const [rawKey, inlineValue] = withoutPrefix.split(/=(.*)/s, 2);
const key = rawKey.trim();
if (!key) throw new Error(`Invalid option: ${arg}`);
if (inlineValue !== undefined) {
options[key] = inlineValue;
continue;
}
const next = args[i + 1];
if (next && !next.startsWith('--')) {
options[key] = next;
i += 1;
} else {
options[key] = true;
}
}
return { positional, options };
}
function stringOption(options: CommandOptions, key: string): string | undefined {
const value = options[key];
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
}
function numberOption(options: CommandOptions, key: string, defaultValue: number): number {
const raw = stringOption(options, key);
if (!raw) return defaultValue;
const parsed = Number.parseInt(raw, 10);
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`--${key} must be a positive integer`);
return parsed;
}
function workspaceDir(options: CommandOptions): string {
return resolve(stringOption(options, 'dir') ?? process.cwd());
}
function matrixDir(root: string): string {
return join(root, '.matrix');
}
function ensureDir(path: string): void {
mkdirSync(path, { recursive: true });
}
function readJsonFile(path: string): unknown {
return JSON.parse(readFileSync(path, 'utf8'));
}
function writeJsonFile(path: string, value: unknown): void {
ensureDir(dirname(path));
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
}
function normalizePackageName(name: string): string {
return name
.trim()
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, '-')
.replace(/^-+|-+$/g, '') || 'matrix-package';
}
function toPascalCase(input: string): string {
const parts = input.trim().split(/[^a-zA-Z0-9]+/).filter(Boolean);
const name = parts.map((part) => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`).join('');
return name || 'Demo';
}
function toActorClassName(input: string): string {
const className = toPascalCase(input);
return className.endsWith('Actor') ? className : `${className}Actor`;
}
function writeIfMissing(path: string, contents: string): void {
if (!existsSync(path)) writeFileSync(path, contents, 'utf8');
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function optionalString(value: unknown): string | undefined {
if (typeof value !== 'string') return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
async function initCommand(options: CommandOptions, positional: string[]): Promise<void> {
const root = resolve(positional[0] ?? stringOption(options, 'dir') ?? process.cwd());
const packageName = normalizePackageName(stringOption(options, 'name') ?? basename(root));
ensureDir(root);
ensureDir(join(root, 'src'));
ensureDir(matrixDir(root));
writeIfMissing(join(root, 'package.json'), `${JSON.stringify({
name: packageName,
version: '0.0.0',
type: 'module',
scripts: {
build: 'tsc -p tsconfig.json',
},
dependencies: {
'@open-matrix/core': '^0.1.0',
},
devDependencies: {
typescript: '^5.7.0',
},
}, null, 2)}\n`);
writeIfMissing(join(root, 'tsconfig.json'), `${JSON.stringify({
compilerOptions: {
target: 'ES2022',
module: 'NodeNext',
moduleResolution: 'NodeNext',
strict: true,
skipLibCheck: true,
outDir: 'dist',
},
include: ['src/**/*.ts'],
}, null, 2)}\n`);
writeIfMissing(join(root, 'src', 'index.ts'), '// Matrix package exports are added here.\n');
writeIfMissing(join(root, 'matrix.json'), `${JSON.stringify({
name: packageName,
version: '0.0.0',
runtime: {
language: 'typescript',
entry: 'dist/index.js',
},
components: [],
}, null, 2)}\n`);
console.log(JSON.stringify({ ok: true, command: 'init', dir: root, name: packageName }, null, 2));
}
function readMatrixManifest(root: string): Record<string, unknown> {
const manifestPath = join(root, 'matrix.json');
if (!existsSync(manifestPath)) throw new Error(`matrix.json not found in ${root}; run matrix init first`);
const value = readJsonFile(manifestPath);
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`matrix.json must be a JSON object: ${manifestPath}`);
}
return value as Record<string, unknown>;
}
function appendExport(indexPath: string, className: string): void {
const line = `export { ${className} } from './${className}.js';`;
const current = existsSync(indexPath) ? readFileSync(indexPath, 'utf8') : '';
if (!current.includes(line)) {
writeFileSync(indexPath, `${current.trimEnd()}\n${line}\n`, 'utf8');
}
}
function appendFactoryExport(indexPath: string, className: string): void {
const line = `export { createStandalone${className} } from './create-standalone-${className}.js';`;
const current = existsSync(indexPath) ? readFileSync(indexPath, 'utf8') : '';
if (!current.includes(line)) {
writeFileSync(indexPath, `${current.trimEnd()}\n${line}\n`, 'utf8');
}
}
function runtimeIdToken(value: string): string {
const token = value
.toUpperCase()
.replace(/[^A-Z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.replace(/-+/g, '-');
return token || 'ACTOR';
}
function actorFactoryTemplate(className: string, mount: string): string {
const factoryName = `createStandalone${className}`;
return `import { InMemoryBroker, InMemoryTransport, MatrixRuntime } from '@open-matrix/core';
import { ${className} } from './${className}.js';
interface StandaloneBootContext {
readonly root?: string;
readonly mount?: string;
}
interface StandaloneRunnerServices {
readonly runtime?: MatrixRuntime;
}
function readTrimmedString(value: unknown): string | undefined {
if (typeof value !== 'string') return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function requiredStandaloneRoot(requestedRoot: string | undefined, runtime: MatrixRuntime): string {
const runtimeRoot = readTrimmedString(runtime.transport.root);
const root = requestedRoot ?? runtimeRoot;
if (!root) throw new Error('${factoryName} requires bootContext.root or a runtime transport root');
return root;
}
export async function ${factoryName}(
_overrides: Record<string, unknown> = {},
services?: StandaloneRunnerServices,
_config?: unknown,
bootContext: StandaloneBootContext = {},
): Promise<{
runtime: MatrixRuntime;
actor: ${className};
root: string;
mount: string;
}> {
const requestedRoot = readTrimmedString(bootContext.root);
const runtime = services?.runtime ?? new MatrixRuntime({
transport: new InMemoryTransport(new InMemoryBroker(), {
name: '${mount}-standalone',
...(requestedRoot ? { root: requestedRoot } : {}),
}),
logging: false,
});
const root = requiredStandaloneRoot(requestedRoot, runtime);
const mount = readTrimmedString(bootContext.mount) ?? '${mount}';
const actor = await runtime.createSupervised(${className}, mount);
return { runtime, actor, root, mount };
}
`;
}
async function addActorCommand(options: CommandOptions, name: string): Promise<void> {
const root = workspaceDir(options);
const normalized = normalizePackageName(name);
const className = toActorClassName(name);
const mount = stringOption(options, 'mount') ?? normalized;
const sourcePath = join(root, 'src', `${className}.ts`);
const factoryPath = join(root, 'src', `create-standalone-${className}.ts`);
readMatrixManifest(root);
ensureDir(join(root, 'src'));
writeIfMissing(sourcePath, `import { MatrixActor } from '@open-matrix/core';
export class ${className} extends MatrixActor {
static accepts = {
echo: {},
getStatus: {},
};
onEcho(payload: { message?: string }) {
return {
ok: true,
message: payload.message ?? null,
actor: '${className}',
};
}
onGetStatus() {
return {
ok: true,
actor: '${className}',
mount: '${mount}',
};
}
}
`);
appendExport(join(root, 'src', 'index.ts'), className);
writeIfMissing(factoryPath, actorFactoryTemplate(className, mount));
appendFactoryExport(join(root, 'src', 'index.ts'), className);
const manifest = readMatrixManifest(root);
const components = Array.isArray(manifest.components)
? manifest.components.filter(isRecord)
: [];
const nextComponents = components.filter((entry) => entry.type !== className && entry.mount !== mount);
nextComponents.push({
type: className,
export: className,
mount,
surface: 'headless',
accepts: ['echo', 'getStatus'],
});
writeJsonFile(join(root, 'matrix.json'), {
...manifest,
runtime: {
...(isRecord(manifest.runtime) ? manifest.runtime : {}),
language: 'typescript',
entry: './dist/index.js',
factory: {
kind: 'factory',
export: `createStandalone${className}`,
instance: {
id: `RUNTIME-HOST-${runtimeIdToken(mount)}`,
class: 'service',
rootKind: 'component',
componentType: className,
autoStart: true,
},
},
},
components: nextComponents,
});
console.log(JSON.stringify({ ok: true, command: 'add actor', dir: root, actor: className, mount }, null, 2));
}
async function configureCommand(options: CommandOptions): Promise<void> {
const root = workspaceDir(options);
const key = stringOption(options, 'key') ?? process.env.MATRIX_API_KEY ?? process.env.HIVECAST_API_KEY;
const cloud = stringOption(options, 'cloud') ?? process.env.MATRIX_CLOUD ?? process.env.HIVECAST_CLOUD;
const space = stringOption(options, 'space') ?? process.env.MATRIX_SPACE ?? process.env.HIVECAST_SPACE;
const runtimeHostBin = stringOption(options, 'runtime-host-bin') ?? process.env.MATRIX_RUNTIME_HOST_BIN;
const runtimeHostRouting = stringOption(options, 'runtime-host-routing') ?? process.env.MATRIX_RUNTIME_HOST_ROUTING;
if (!key) throw new Error('MATRIX_API_KEY_REQUIRED: matrix configure requires --key or MATRIX_API_KEY');
if (!cloud) throw new Error('MATRIX_CLOUD_REQUIRED: matrix configure requires --cloud or MATRIX_CLOUD');
if (!space) throw new Error('MATRIX_SPACE_REQUIRED: matrix configure requires --space or MATRIX_SPACE');
if (runtimeHostRouting && !['release', 'workspace-dist', 'workspace-source'].includes(runtimeHostRouting)) {
throw new Error(`--runtime-host-routing must be release, workspace-dist, or workspace-source, got ${runtimeHostRouting}`);
}
const configPath = join(matrixDir(root), 'config.json');
const credentialPath = join(matrixDir(root), 'credentials', 'matrix-api-key.json');
writeJsonFile(configPath, {
cloud,
space,
...(runtimeHostBin ? { runtimeHostBin } : {}),
...(runtimeHostRouting ? { runtimeHostRouting } : {}),
});
writeJsonFile(credentialPath, { key, cloud, space });
chmodSync(credentialPath, 0o600);
console.log(JSON.stringify({ ok: true, command: 'configure', dir: root, cloud, space }, null, 2));
}
function actorConstructorFromExport(value: unknown): ActorConstructor | null {
if (typeof value !== 'function') return null;
const candidate = value as { prototype?: { initialize?: unknown } };
if (typeof candidate.prototype?.initialize !== 'function') return null;
return value as ActorConstructor;
}
async function importActor(entry: string, exportName: string): Promise<ActorConstructor> {
const moduleUrl = `${pathToFileURL(resolve(entry)).href}?matrix_cli=${Date.now()}`;
const mod = await import(moduleUrl) as Record<string, unknown>;
const requested = actorConstructorFromExport(mod[exportName]);
if (requested) return requested;
const available = Object.entries(mod).filter(([, value]) => actorConstructorFromExport(value)).map(([key]) => key);
if (available.length === 1) return actorConstructorFromExport(mod[available[0]])!;
throw new Error(`No actor class found as export "${exportName}". Available actor exports: ${available.join(', ') || '(none)'}`);
}
async function importPackageBootstrap(entry: string, exportName: string): Promise<PackageBootstrapFunction> {
const moduleUrl = `${pathToFileURL(resolve(entry)).href}?matrix_cli=${Date.now()}`;
const mod = await import(moduleUrl) as Record<string, unknown>;
const value = mod[exportName];
if (typeof value !== 'function') {
throw new Error(`Package runtime factory export "${exportName}" was not found in ${entry}`);
}
return value as PackageBootstrapFunction;
}
function readRequestBody(req: IncomingMessage): Promise<unknown> {
return new Promise((resolveBody, reject) => {
const chunks: Buffer[] = [];
req.on('data', (chunk: Buffer) => chunks.push(chunk));
req.on('error', reject);
req.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf8');
try {
resolveBody(raw.trim() ? JSON.parse(raw) : {});
} catch (error) {
reject(error);
}
});
});
}
function sendJson(res: ServerResponse, status: number, body: unknown): void {
const text = JSON.stringify(body);
res.writeHead(status, {
'content-type': 'application/json',
'content-length': Buffer.byteLength(text),
});
res.end(text);
}
async function requestActor(
transport: InMemoryTransport,
mount: string,
op: string,
payload: unknown,
): Promise<unknown> {
const correlationId = `matrix-cli-${Date.now()}-${Math.random().toString(16).slice(2)}`;
const replyTo = `$replies.${correlationId}`;
let unsubscribe: Unsub = () => {};
const response = new Promise<unknown>((resolveResponse, reject) => {
const timeout = setTimeout(() => {
unsubscribe();
reject(new Error(`REQUEST_TIMEOUT: ${mount}.${op}`));
}, 5000);
unsubscribe = transport.subscribe(replyTo, (value) => {
clearTimeout(timeout);
unsubscribe();
resolveResponse(value);
});
});
transport.publish(TopicRouter.inbox(mount), {
op,
payload,
replyTo,
correlationId,
});
return response;
}
async function serveRuntime(input: {
readonly runtime: MatrixRuntime;
readonly transport: InMemoryTransport;
readonly root: string;
readonly mount: string;
readonly entry: string;
readonly exportName: string;
readonly host: string;
readonly port: number;
readonly workspaceRoot: string;
}): Promise<void> {
const { runtime, transport, root, mount, entry, exportName, host, port, workspaceRoot } = input;
const server = createServer(async (req, res) => {
try {
if (req.method === 'GET' && req.url === '/healthz') {
sendJson(res, 200, { ok: true, mount, root, pid: process.pid });
return;
}
if (req.method !== 'POST' || req.url !== '/invoke') {
sendJson(res, 404, { ok: false, error: 'NOT_FOUND' });
return;
}
const body = await readRequestBody(req) as { mount?: unknown; op?: unknown; payload?: unknown };
const targetMount = typeof body.mount === 'string' && body.mount.trim() ? body.mount.trim() : mount;
const op = typeof body.op === 'string' && body.op.trim() ? body.op.trim() : '';
if (!op) throw new Error('matrix invoke requires an op');
const result = await requestActor(transport, targetMount, op, body.payload);
sendJson(res, 200, result);
} catch (error) {
sendJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) });
}
});
await new Promise<void>((resolveListen) => server.listen(port, host, resolveListen));
const status: RunStatus = {
pid: process.pid,
host,
port,
mount,
entry: resolve(entry),
exportName,
startedAt: new Date().toISOString(),
};
writeJsonFile(join(matrixDir(workspaceRoot), 'run.json'), status);
console.log(JSON.stringify({ ok: true, command: 'run', root, ...status }, null, 2));
const stop = async () => {
await runtime.shutdown();
server.close();
};
process.once('SIGINT', () => { void stop().finally(() => process.exit(0)); });
process.once('SIGTERM', () => { void stop().finally(() => process.exit(0)); });
}
async function runActorEntryCommand(options: CommandOptions, entry: string): Promise<void> {
const mount = stringOption(options, 'mount');
if (!mount) throw new Error('matrix run <entry> requires --mount <mount>');
const exportName = stringOption(options, 'export') ?? 'default';
const host = stringOption(options, 'host') ?? '127.0.0.1';
const port = numberOption(options, 'port', 3920);
const root = workspaceDir(options);
const ActorClass = await importActor(entry, exportName);
const broker = new InMemoryBroker();
const transport = new InMemoryTransport(broker, { name: `matrix-cli:${mount}`, root: 'sdk-local' });
const runtime = new MatrixRuntime({ transport, logging: false });
await runtime.create(ActorClass, mount);
await serveRuntime({ runtime, transport, root: 'sdk-local', mount, entry, exportName, host, port, workspaceRoot: root });
}
function resolvePackageRunSpec(packageDir: string, options: CommandOptions): PackageRunSpec {
const manifestPath = join(packageDir, 'matrix.json');
if (!existsSync(manifestPath)) throw new Error(`matrix.json not found in ${packageDir}; run matrix init first`);
const manifest = readJsonFile(manifestPath);
if (!isRecord(manifest)) throw new Error(`matrix.json must be a JSON object: ${manifestPath}`);
const runtime = isRecord(manifest.runtime) ? manifest.runtime : undefined;
const factory = isRecord(runtime?.factory) ? runtime.factory : undefined;
if (!factory) {
throw new Error(`${manifestPath} must declare runtime.factory before matrix run/up can use package mode`);
}
const entry = stringOption(options, 'entry') ?? optionalString(factory.entry) ?? optionalString(runtime?.entry);
if (!entry) throw new Error(`${manifestPath} runtime.factory requires entry or runtime.entry`);
const exportName = stringOption(options, 'export') ?? optionalString(factory.export);
if (!exportName) throw new Error(`${manifestPath} runtime.factory requires export`);
const instance = isRecord(factory.instance) ? factory.instance : undefined;
const mount = stringOption(options, 'mount')
?? optionalString(instance?.mount)
?? resolveComponentMount(manifest, instance);
if (!mount) {
throw new Error(`${manifestPath} runtime.factory must resolve a mount through --mount, instance.mount, or components[]`);
}
const entryPath = resolve(packageDir, entry);
if (!existsSync(entryPath)) {
throw new Error(`Package runtime entry does not exist: ${entryPath}. Run the package build first.`);
}
return { packageDir, entryPath, exportName, mount };
}
function resolveComponentMount(manifest: Record<string, unknown>, instance: Record<string, unknown> | undefined): string | undefined {
const components = Array.isArray(manifest.components) ? manifest.components.filter(isRecord) : [];
const componentType = optionalString(instance?.componentType);
if (componentType) {
const matching = components.find((component) => optionalString(component.type) === componentType);
return optionalString(matching?.mount);
}
if (components.length === 1) return optionalString(components[0].mount);
return undefined;
}
async function runPackageCommand(options: CommandOptions, packageDir: string): Promise<void> {
const spec = resolvePackageRunSpec(packageDir, options);
const host = stringOption(options, 'host') ?? '127.0.0.1';
const port = numberOption(options, 'port', 3920);
const root = workspaceDir(options);
const rootName = stringOption(options, 'root') ?? 'sdk-local';
const broker = new InMemoryBroker();
const transport = new InMemoryTransport(broker, { name: `matrix-cli:${spec.mount}`, root: rootName });
const runtime = new MatrixRuntime({ transport, logging: false });
const bootstrap = await importPackageBootstrap(spec.entryPath, spec.exportName);
await bootstrap({}, { runtime }, undefined, {
root: rootName,
mount: spec.mount,
packageDir: spec.packageDir,
entryPath: spec.entryPath,
exportName: spec.exportName,
});
await serveRuntime({
runtime,
transport,
root: rootName,
mount: spec.mount,
entry: spec.entryPath,
exportName: spec.exportName,
host,
port,
workspaceRoot: root,
});
}
async function runCommand(options: CommandOptions, positional: string[]): Promise<void> {
const root = workspaceDir(options);
const target = positional[0] ? resolve(positional[0]) : root;
if (existsSync(target) && statSync(target).isDirectory()) {
await runPackageCommand(options, target);
return;
}
if (!positional[0]) throw new Error('matrix run requires an entry file or package directory');
await runActorEntryCommand(options, positional[0]);
}
async function invokeCommand(options: CommandOptions, positional: string[]): Promise<void> {
const root = workspaceDir(options);
const mount = positional[0];
const op = positional[1];
const payloadRaw = positional[2] ?? '{}';
if (!mount) throw new Error('matrix invoke requires <mount>');
if (!op) throw new Error('matrix invoke requires <op>');
const statusPath = join(matrixDir(root), 'run.json');
const status = existsSync(statusPath) ? readJsonFile(statusPath) as Partial<RunStatus> : {};
const host = stringOption(options, 'host') ?? (typeof status.host === 'string' ? status.host : '127.0.0.1');
const port = numberOption(options, 'port', typeof status.port === 'number' ? status.port : 3920);
const payload = JSON.parse(payloadRaw);
const response = await fetch(`http://${host}:${port}/invoke`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ mount, op, payload }),
});
const text = await response.text();
if (!response.ok) throw new Error(`matrix invoke failed (${response.status}): ${text}`);
console.log(JSON.stringify(JSON.parse(text), null, 2));
}
function printHelp(): void {
console.log(`matrix SDK CLI
Commands:
matrix init [dir] [--name name]
matrix add actor <name> [--dir dir] [--mount mount]
matrix actor <name> [--dir dir] [--mount mount]
matrix configure --key key --cloud url --space space [--dir dir] [--runtime-host-bin path]
matrix run [entry-or-dir] [--mount mount] [--export name] [--port port] [--dir dir]
matrix invoke <mount> <op> [json] [--port port] [--dir dir]
matrix up [dir] --runtime-host-bin path [--runtime-host-routing workspace-source|workspace-dist|release]
`);
}
async function upCommand(rawArgs: string[]): Promise<void> {
const parsed = parseArgs(rawArgs);
const root = workspaceDir(parsed.options);
const target = parsed.positional[0] ? resolve(parsed.positional[0]) : root;
if (!existsSync(target) || !statSync(target).isDirectory()) {
throw new Error(`matrix up target must be a package directory: ${target}`);
}
resolvePackageRunSpec(target, parsed.options);
const config = readPackageLocalConfig(root);
const runtimeHostBin = stringOption(parsed.options, 'runtime-host-bin')
?? optionalString(config.runtimeHostBin)
?? process.env.MATRIX_RUNTIME_HOST_BIN;
if (!runtimeHostBin) {
throw new Error('MATRIX_RUNTIME_HOST_BIN_REQUIRED: matrix up requires --runtime-host-bin or MATRIX_RUNTIME_HOST_BIN pointing to the Runtime Host matrix command');
}
const routing = stringOption(parsed.options, 'runtime-host-routing')
?? optionalString(config.runtimeHostRouting)
?? process.env.MATRIX_RUNTIME_HOST_ROUTING
?? 'release';
if (!['release', 'workspace-dist', 'workspace-source'].includes(routing)) {
throw new Error(`--runtime-host-routing must be release, workspace-dist, or workspace-source, got ${routing}`);
}
const runtimeHostPath = isPathLikeCommand(runtimeHostBin) ? resolve(runtimeHostBin) : runtimeHostBin;
const command = runtimeHostPath.endsWith('.js') || runtimeHostPath.endsWith('.mjs')
? process.execPath
: runtimeHostPath;
const prefix = command === process.execPath ? [runtimeHostPath] : [];
const routingArgs = routing === 'workspace-source'
? ['--workspace-source']
: routing === 'workspace-dist'
? ['--workspace-dist']
: [];
const result = spawnSync(command, [
...prefix,
...routingArgs,
'up',
target,
...filterRuntimeHostArgs(rawArgs, parsed.positional.length > 0),
], {
cwd: root,
stdio: 'inherit',
env: process.env,
});
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`matrix up Runtime Host command failed with exit ${result.status ?? 'unknown'}`);
}
}
function readPackageLocalConfig(root: string): Record<string, unknown> {
const configPath = join(matrixDir(root), 'config.json');
if (!existsSync(configPath)) return {};
const value = readJsonFile(configPath);
if (!isRecord(value)) throw new Error(`${configPath} must contain a JSON object`);
return value;
}
function isPathLikeCommand(value: string): boolean {
return value.startsWith('.')
|| value.startsWith('/')
|| value.includes('/');
}
function filterRuntimeHostArgs(rawArgs: readonly string[], hasExplicitTarget: boolean): string[] {
const valueOptions = new Set(['--dir', '--runtime-host-bin', '--runtime-host-routing']);
const filtered: string[] = [];
let consumedTarget = false;
for (let i = 0; i < rawArgs.length; i += 1) {
const arg = rawArgs[i];
if (arg === '--') {
filtered.push(...rawArgs.slice(i));
break;
}
const inlineOption = arg.includes('=') ? arg.slice(0, arg.indexOf('=')) : '';
if (valueOptions.has(arg)) {
i += 1;
continue;
}
if (inlineOption && valueOptions.has(inlineOption)) {
continue;
}
if (hasExplicitTarget && !arg.startsWith('-') && !consumedTarget) {
consumedTarget = true;
continue;
}
filtered.push(arg);
}
return filtered;
}
export async function main(args: string[]): Promise<void> {
const [command, subcommand, ...rest] = args;
if (!command || command === '--help' || command === '-h') {
printHelp();
return;
}
if (command === 'init') {
const parsed = parseArgs([subcommand, ...rest].filter((value): value is string => Boolean(value)));
await initCommand(parsed.options, parsed.positional);
return;
}
if (command === 'add' && subcommand === 'actor') {
const [name, ...tail] = rest;
if (!name) throw new Error('matrix add actor requires <name>');
const parsed = parseArgs(tail);
await addActorCommand(parsed.options, name);
return;
}
if (command === 'actor') {
if (!subcommand) throw new Error('matrix actor requires <name>');
const parsed = parseArgs(rest);
await addActorCommand(parsed.options, subcommand);
return;
}
if (command === 'configure') {
const parsed = parseArgs([subcommand, ...rest].filter((value): value is string => Boolean(value)));
await configureCommand(parsed.options);
return;
}
if (command === 'run') {
const parsed = parseArgs([subcommand, ...rest].filter((value): value is string => Boolean(value)));
await runCommand(parsed.options, parsed.positional);
return;
}
if (command === 'invoke') {
const parsed = parseArgs([subcommand, ...rest].filter((value): value is string => Boolean(value)));
await invokeCommand(parsed.options, parsed.positional);
return;
}
if (command === 'up') {
await upCommand([subcommand, ...rest].filter((value): value is string => Boolean(value)));
return;
}
throw new Error(`Unknown command: ${command}`);
}

View File

@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"strict": true,
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": ["node"],
"noEmit": false,
"tsBuildInfoFile": "dist/.tsbuildinfo"
},
"include": [
"src/**/*.ts"
],
"exclude": [
"dist",
"node_modules"
]
}

View File

@ -0,0 +1,29 @@
#!/usr/bin/env node
/**
* @open-matrix/contracts build
*
* Type contracts + one pure runtime function (decideMatrixPlacementBind).
* Emits .js + .d.ts via tsc. Part of core-decontamination Phase 2.
*/
import { execFileSync } from 'node:child_process';
import { createRequire } from 'node:module';
import { rmSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const distDir = resolve(__dirname, 'dist');
const require = createRequire(import.meta.url);
const typescriptPackageJson = require.resolve('typescript/package.json', { paths: [__dirname] });
const tscBin = resolve(dirname(typescriptPackageJson), 'bin/tsc');
rmSync(distDir, { recursive: true, force: true });
execFileSync(process.execPath, [
tscBin,
'-p',
resolve(__dirname, 'tsconfig.build.json'),
], {
cwd: __dirname,
stdio: 'inherit',
});

View File

@ -0,0 +1,32 @@
{
"name": "@open-matrix/contracts",
"version": "0.1.0",
"description": "Type contracts for Matrix substrate (placement, service registry, runtime presence). Almost zero runtime — one pure function (decideMatrixPlacementBind) plus pure types. Zero @open-matrix/core dependency. Extracted from @open-matrix/core during core-decontamination Phase 2 so schema evolution doesn't rebuild the actor kernel.",
"private": true,
"type": "module",
"sideEffects": false,
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
"./placement": { "types": "./dist/placement.d.ts", "default": "./dist/placement.js" },
"./service-registry": { "types": "./dist/service-registry.d.ts", "default": "./dist/service-registry.js" },
"./runtime-presence": { "types": "./dist/runtime-presence.d.ts", "default": "./dist/runtime-presence.js" },
"./package.json": "./package.json"
},
"scripts": {
"build": "node build.mjs",
"type-check": "tsc --noEmit"
},
"files": [
"dist",
"package.json"
],
"devDependencies": {
"typescript": "^5.7.0"
},
"license": "MIT",
"engines": {
"node": ">=20"
}
}

View File

@ -0,0 +1,18 @@
/**
* @open-matrix/contracts re-export barrel.
*
* Consumers should prefer the subpath imports for tree-shaking and
* for clarity about which contract they depend on:
*
* import type { MatrixMountCardinality } from '@open-matrix/contracts/placement';
* import type { ServiceRegistryEntryV1 } from '@open-matrix/contracts/service-registry';
* import type { RuntimePresenceRecordV1 } from '@open-matrix/contracts/runtime-presence';
*
* This barrel exists for consumers that legitimately need multiple
* contract surfaces, and as a single re-export point during the
* core-decontamination migration.
*/
export * from './placement.js';
export * from './service-registry.js';
export * from './runtime-presence.js';

View File

@ -0,0 +1,161 @@
export type MatrixMountCardinality =
| 'authority-singleton'
| 'device-singleton'
| 'device-local-control'
| 'queue-service'
| 'named-instance'
| 'replicated-read';
export interface MatrixPlacementDeclaration {
readonly cardinality: MatrixMountCardinality;
readonly failover?: 'manual' | 'lease-timeout';
readonly queue?: string;
readonly physicalMount?: string;
}
export type MatrixPlacementClaimMode =
| 'authoritative'
| 'local-only'
| 'pool-member'
| 'mirror';
export interface RuntimeInventoryPlacementMetadata {
readonly cardinality: MatrixMountCardinality;
readonly claimMode: MatrixPlacementClaimMode;
readonly logicalMount: string;
readonly physicalMount: string;
readonly callable: boolean;
readonly standbyReason?: string;
readonly placementEpoch?: number;
}
export type MatrixPlacementBindDenyReason =
| 'SINGLETON_MOUNT_NOT_OWNED'
| 'AUTHORITY_COORDINATOR_LEASE_NOT_OWNED'
| 'UNKNOWN_PLACEMENT'
| 'QUEUE_REQUIRED';
export type AuthorityCoordinatorLeaseStatus =
| 'active'
| 'released'
| 'expired'
| 'fenced';
export type AuthorityCoordinatorLeaseScope =
| 'local-home'
| 'hub-authority'
| 'remote-projection';
export type AuthorityCoordinatorLeaseAuthority =
| 'local'
| 'hub';
export type AuthorityCoordinatorLeaseSource =
| 'local-file'
| 'hub-projection';
export interface AuthorityCoordinatorLease {
readonly version: 1;
readonly authorityRoot: string;
readonly leaseId: string;
readonly ownerNodeId: string;
readonly ownerHostId?: string;
readonly ownerInstallId?: string;
readonly ownerHostLinkId?: string;
readonly placementEpoch: number;
readonly acquiredAt: string;
readonly heartbeatAt: string;
readonly expiresAt: string;
readonly status: AuthorityCoordinatorLeaseStatus;
readonly scope: AuthorityCoordinatorLeaseScope;
readonly leaseAuthority: AuthorityCoordinatorLeaseAuthority;
readonly source: AuthorityCoordinatorLeaseSource;
readonly reason?: string;
}
export interface AuthorityCoordinatorLeaseAcquireInput {
readonly authorityRoot: string;
readonly ownerNodeId: string;
readonly ownerHostId?: string;
readonly ownerInstallId?: string;
readonly ownerHostLinkId?: string;
readonly placementEpoch: number;
readonly scope?: AuthorityCoordinatorLeaseScope;
readonly leaseAuthority?: AuthorityCoordinatorLeaseAuthority;
readonly source?: AuthorityCoordinatorLeaseSource;
readonly ttlMs: number;
readonly now?: Date;
readonly force?: boolean;
}
export interface AuthorityCoordinatorLeaseAcquireResult {
readonly ok: boolean;
readonly acquired: boolean;
readonly lease?: AuthorityCoordinatorLease;
readonly currentOwner?: AuthorityCoordinatorLease;
readonly reason?:
| 'LEASE_HELD_BY_OTHER_OWNER'
| 'LEASE_EXPIRED'
| 'LEASE_RELEASED'
| 'LEASE_FORCED'
| 'LEASE_RENEWED'
| 'LEASE_ACQUIRED';
}
export interface MatrixPlacementBindRequest {
readonly authorityRoot: string;
readonly runtimeId?: string;
readonly packageName?: string;
readonly mount: string;
readonly cardinality: MatrixMountCardinality;
readonly topologyKind?: string;
readonly authorityCoordinatorActive: boolean;
readonly ownerNodeId?: string;
readonly authorityLease?: AuthorityCoordinatorLease | null;
}
export interface MatrixPlacementBindDecision {
readonly allowed: boolean;
readonly callable: boolean;
readonly reason?: MatrixPlacementBindDenyReason;
}
export interface MatrixPlacementPolicy {
canBind(input: MatrixPlacementBindRequest): MatrixPlacementBindDecision;
}
export function decideMatrixPlacementBind(
request: MatrixPlacementBindRequest,
): MatrixPlacementBindDecision {
if (
request.cardinality === 'authority-singleton'
&& request.authorityCoordinatorActive === false
) {
return {
allowed: false,
callable: false,
reason: 'SINGLETON_MOUNT_NOT_OWNED',
};
}
if (request.cardinality === 'authority-singleton' && request.authorityLease) {
if (
request.authorityLease.status !== 'active'
|| (
request.ownerNodeId
&& request.authorityLease.ownerNodeId !== request.ownerNodeId
)
) {
return {
allowed: false,
callable: false,
reason: 'AUTHORITY_COORDINATOR_LEASE_NOT_OWNED',
};
}
}
return {
allowed: true,
callable: true,
};
}

View File

@ -0,0 +1,100 @@
/**
* RuntimePresenceTypes shared types for namespace-scoped runtime discovery.
*
* These types define the contract between runtimes that announce their presence
* and consumers (Director, system.runtimes, monitoring) that discover them.
*
* See: ARCHITECTURE/ARCHITECTURE-NAMESPACE-DISCOVERY-AND-RUNTIME-PRESENCE.md
*/
import type { HealthSnapshotV1 } from './service-registry.js';
import type { RuntimeInventoryPlacementMetadata } from './placement.js';
/** What kind of claim a runtime makes on a namespace prefix. */
export type RuntimeClaimKind = 'authoritative' | 'proxy' | 'mirror' | 'local-only' | 'pool-member';
/** How a runtime exposes its actor inventory. */
export type RuntimeInventoryMode = 'embedded' | 'pull';
/** Known runtime types. */
export type RuntimeType = 'daemon' | 'browser' | 'cell' | 'container' | 'leaf' | 'worker' | 'folder';
/** Runtime presence events published on the namespace-scoped presence stream. */
export type RuntimePresenceEventV1 = 'announce' | 'heartbeat' | 'departed';
/** A single actor/component entry in a runtime's inventory. */
export interface RuntimeInventoryEntryV1 {
/** Mount path within the namespace (e.g., 'system.agents', 'explorer'). */
mount: string;
/** Actor/component type name (e.g., '_AgentsRoot', 'DirectorTree'). */
type?: string;
/** What surface this actor provides. */
surface?: 'headless' | 'dom' | 'webapp';
/** Which runtime hosts this actor. */
runtimeId: string;
/** What kind of claim this actor represents. */
claimKind: RuntimeClaimKind;
/** Placement and callability metadata for this actor. */
placement?: RuntimeInventoryPlacementMetadata;
/** Human-readable description. */
description?: string;
}
/** Full presence record for a runtime in a namespace. */
export interface RuntimePresenceRecordV1 {
version: '1';
/** The namespace this runtime participates in. */
namespaceRoot: string;
/** Unique identity for this runtime instance. */
runtimeId: string;
/** What kind of runtime this is. */
runtimeType: RuntimeType;
/** The NATS transport root this runtime uses. */
runtimeRoot: string;
/** Optional: authority root for deployment/ownership plane. */
authorityRoot?: string;
/** Optional: platform root for HiveCast/hosting plane. */
platformRoot?: string;
/** Optional: human-readable application name. */
app?: string;
/** What namespace prefixes this runtime claims to serve. */
claims: Array<{
prefix: string;
kind: RuntimeClaimKind;
}>;
/** How this runtime exposes its inventory. */
inventoryMode: RuntimeInventoryMode;
/** Inline actor list (when inventoryMode === 'embedded'). */
inventory?: RuntimeInventoryEntryV1[];
/** Mount path of a catalog actor (when inventoryMode === 'pull'). */
catalogMount?: string;
/** Optional: mount path for runtime lifecycle control. */
controlMount?: string;
/** Milliseconds before missing heartbeat means runtime is dead. 0 = no timeout. */
heartbeatTtlMs: number;
/** When this runtime first registered/announced. */
registeredAt: number;
/** Last heartbeat timestamp. */
lastHeartbeat: number;
/** Latest health snapshot from the runtime root actor. */
health?: HealthSnapshotV1;
/** Optional runtime-specific metadata. */
metadata?: Record<string, unknown>;
}

View File

@ -0,0 +1,146 @@
/**
* ServiceRegistryTypes shared V1 contract for live substrate directory entries.
*
* `system.registry` owns live, bus-addressable service/app/feed/actor/runtime
* entries inside one namespace. Durable package metadata, Device enrollment,
* identity, credentials, and Host supervision have separate owners.
*/
import type { RuntimeInventoryPlacementMetadata } from './placement.js';
export type ServiceRegistryEntryKindV1 =
| 'actor'
| 'service'
| 'app'
| 'feed'
| 'runtime'
| 'gateway'
| 'connector'
| 'resource';
export type MountClaimModeV1 =
| 'authoritative'
| 'proxy'
| 'mirror'
| 'local-only'
| 'pool-member';
export type MountClaimResultV1 =
| 'accepted'
| 'rejected'
| 'collision'
| 'joined-pool'
| 'standby'
| 'leader'
| 'mirror'
| 'local-only';
export type ServiceRegistryLoadBalancingModeV1 =
| 'none'
| 'round-robin'
| 'least-loaded'
| 'broadcast';
export type HealthStatusV1 =
| 'ok'
| 'degraded'
| 'error'
| 'unknown'
| 'starting'
| 'stopped';
export type AppSurfaceTypeV1 = 'page' | 'panel' | 'modal' | 'widget';
export type RegistryWatchEventTypeV1 = 'upsert' | 'release' | 'renew' | 'expired';
export interface CapabilityDeclarationV1 {
id: string;
name?: string;
description?: string;
inputSchema?: Record<string, unknown>;
outputSchema?: Record<string, unknown>;
}
export interface IntentDeclarationV1 {
intent: string;
description?: string;
inputSchema?: Record<string, unknown>;
outputSchema?: Record<string, unknown>;
}
export interface ServiceInstanceV1 {
accepts: string[];
emits: string[];
streams: string[];
capabilities: CapabilityDeclarationV1[];
intents: IntentDeclarationV1[];
}
export interface AppSurfaceV1 {
route: string;
title: string;
icon?: string;
surface: AppSurfaceTypeV1;
openable: boolean;
appName?: string;
}
export interface ServiceRegistryPlacementV1 {
deviceId?: string;
hostId?: string;
runtimeId?: string;
packageId?: string;
actorId?: string;
deploymentInstanceId?: string;
instanceName?: string;
packageRef?: string;
cardinality?: RuntimeInventoryPlacementMetadata['cardinality'];
claimMode?: RuntimeInventoryPlacementMetadata['claimMode'];
logicalMount?: string;
physicalMount?: string;
callable?: boolean;
standbyReason?: string;
placementEpoch?: number;
}
export interface MountClaimV1 {
mode: MountClaimModeV1;
result: MountClaimResultV1;
ownerComponentId: string;
electionGroup?: string;
leaderId?: string;
loadBalancing: ServiceRegistryLoadBalancingModeV1;
claimedAt: number;
renewedAt: number;
expiresAt: number;
}
export interface HealthSnapshotV1 {
status: HealthStatusV1;
reason?: string;
updatedAt: number;
}
export interface ServiceRegistryEntryV1 {
version: '1';
namespaceRoot: string;
mount: string;
kind: ServiceRegistryEntryKindV1;
service?: ServiceInstanceV1;
app?: AppSurfaceV1;
placement: ServiceRegistryPlacementV1;
claim: MountClaimV1;
health: HealthSnapshotV1;
ttlMs: number;
metadata?: Record<string, unknown>;
}
export interface RegistryWatchEventV1 {
version: '1';
sequence: number;
type: RegistryWatchEventTypeV1;
namespaceRoot: string;
mount: string;
at: number;
entry?: ServiceRegistryEntryV1;
}

View File

@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"outDir": "dist",
"declaration": true,
"declarationMap": false,
"sourceMap": false
}
}

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"useDefineForClassFields": true
},
"include": ["src"]
}

View File

@ -0,0 +1,25 @@
# Changelog
## 0.1.0 (2026-02-17)
Initial release.
### Features
- `MatrixActor` — headless actor base class with `static accepts` / `static emits` interface declarations
- `MatrixActorHtmlElement` — Web Component base class with shadow DOM, template, and styles
- `MatrixRuntime` — runtime orchestration, child creation, lifecycle management
- `InMemoryBroker` — in-process message broker for local development
- `NatsTransport` — NATS transport for production daemon connectivity
- `FederationTransportAdapter` — cross-realm federation transport
- `TopicRouter` — topic derivation (`$inbox`, `$events`) for accepts/emits topology
- `RequestReply` — request/reply pattern over NATS with correlation IDs
- `FlowBuilder` / `FlowEngine` — data flow pipeline construction and execution
- `Interpreter` / `parseSExpr` — embedded LISP interpreter for declarative actor composition
- `serialize` / `parse` — 5-format serialization (JSON, HTML, S-Expr, Fluent, Topics)
- 135 named exports total with full TypeScript declarations
### Dependencies
- `@open-matrix/federation` ^0.2.0
- `nats.ws` ^1.29.2

21
packages/core/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024-2026 Nicholas Galante
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

82
packages/core/README.md Normal file
View File

@ -0,0 +1,82 @@
# @matrix/core
Matrix runtime SDK — actors, Web Components, transports, serialization, and LISP-on-Protocol.
## Install
```bash
npm install @matrix/core
```
## Usage
### Headless Actor (daemon-side)
```typescript
import { MatrixActor } from '@matrix/core';
class PingActor extends MatrixActor {
static accepts = {
ping: { message: 'string' },
};
static emits = {
pong: { echo: 'string', ts: 'number' },
};
onPing(payload: { message: string }) {
this.emit('pong', { echo: payload.message, ts: Date.now() });
}
}
```
### Browser Web Component
```typescript
import { MatrixActorHtmlElement } from '@matrix/core';
class StatusPanel extends MatrixActorHtmlElement {
static accepts = { showStatus: { text: 'string' } };
static template = `<div id="status"></div>`;
static styles = `#status { font-family: monospace; padding: 8px; }`;
onShowStatus(payload: { text: string }) {
this.shadowRoot!.querySelector('#status')!.textContent = payload.text;
}
}
customElements.define('status-panel', StatusPanel);
```
### Programmatic Runtime
```typescript
import { MatrixRuntime, InMemoryBroker } from '@matrix/core';
const broker = new InMemoryBroker();
const runtime = new MatrixRuntime({ broker });
await runtime.start();
```
## What's Included
| Category | Key Exports |
|----------|------------|
| **Core** | `MatrixActor`, `Supervisor`, `HeadlessComponentRegistry` |
| **Framework** | `MatrixActorHtmlElement`, `MxContract`, `MxInterface`, `MxProxy` |
| **Runtime** | `MatrixRuntime`, `HeadlessHost`, `ConsoleLogger` |
| **Transports** | `InMemoryBroker`, `NatsTransport`, `FederationTransportAdapter` |
| **Messaging** | `TopicRouter`, `SubscriptionBag`, `RequestReply` |
| **LISP** | `Interpreter`, `EvalContext`, `parseSExpr` |
| **Serialization** | `serialize`, `parse`, 5-format parity |
| **Flow** | `FlowBuilder`, `FlowEngine`, `FlowContext` |
135 named exports total. Full type declarations included.
## Requirements
- Node.js >= 20
- TypeScript >= 5.0 (for type checking)
## License
MIT

View File

@ -0,0 +1,77 @@
#!/usr/bin/env node
/**
* Post-build script: Adds .js extensions to relative import specifiers in compiled ESM output.
*
* TypeScript with moduleResolution:"Bundler" emits extensionless imports (e.g., from './foo').
* Node.js ESM requires explicit .js extensions. This script rewrites all relative imports
* in dist/ to add .js extensions where missing.
*/
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs';
import { join, extname, dirname, resolve } from 'path';
const DIST_DIR = new URL('./dist/src/', import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1');
let fixed = 0;
let files = 0;
function processFile(filePath) {
if (!filePath.endsWith('.js') && !filePath.endsWith('.d.ts')) return;
files++;
const fileDir = dirname(filePath);
let content = readFileSync(filePath, 'utf8');
let changed = false;
// Match: from './...' or from '../...' (relative imports without .js extension)
// Also match: import('./...') (dynamic imports in .d.ts files)
// Also match: import './...'; (bare side-effect imports)
const addExt = (match, prefix, specifier, suffix) => {
const ext = extname(specifier);
if (ext === '.js' || ext === '.mjs' || ext === '.cjs' || ext === '.json') {
return match;
}
const resolved = resolve(fileDir, specifier);
const fileCandidates = [
`${resolved}.js`,
`${resolved}.mjs`,
`${resolved}.cjs`,
`${resolved}.d.ts`,
];
if (fileCandidates.some((candidate) => existsSync(candidate))) {
changed = true;
return `${prefix}${specifier}.js${suffix}`;
}
// Only fall back to /index.js when there is no sibling file target.
if (existsSync(resolved) && statSync(resolved).isDirectory()) {
changed = true;
return `${prefix}${specifier}/index.js${suffix}`;
}
changed = true;
return `${prefix}${specifier}.js${suffix}`;
};
const updated = content
.replace(/(from\s+['"])(\.\.?\/[^'"]+)(['"])/g, addExt)
.replace(/(import\(['"])(\.\.?\/[^'"]+)(['"]\))/g, addExt)
.replace(/(import\s+['"])(\.\.?\/[^'"]+)(['"])/g, addExt);
if (changed) {
writeFileSync(filePath, updated);
fixed++;
}
}
function walk(dir) {
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) {
walk(full);
} else {
processFile(full);
}
}
}
walk(DIST_DIR);
console.log(`Fixed ${fixed}/${files} files with .js extensions`);

363
packages/core/package.json Normal file
View File

@ -0,0 +1,363 @@
{
"name": "@open-matrix/core",
"private": true,
"version": "0.1.0",
"description": "Matrix runtime SDK \u2014 actors, Web Components, transports, serialization, and LISP-on-Protocol",
"type": "module",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/nicholasgalante/Matrix-3.git",
"directory": "packages/core"
},
"keywords": [
"matrix",
"actors",
"web-components",
"nats",
"runtime",
"sdk"
],
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"import": "./dist/src/index.js",
"default": "./dist/src/index.js"
},
"./_node-compat": {
"types": "./dist/src/_node-compat.d.ts",
"import": "./dist/src/_node-compat.js",
"default": "./dist/src/_node-compat.js"
},
"./core/IRecordStoreProvider": {
"types": "./dist/src/core/IRecordStoreProvider.d.ts",
"import": "./dist/src/core/IRecordStoreProvider.js",
"default": "./dist/src/core/IRecordStoreProvider.js"
},
"./core/MatrixActor": {
"types": "./dist/src/core/MatrixActor.d.ts",
"import": "./dist/src/core/MatrixActor.js",
"default": "./dist/src/core/MatrixActor.js"
},
"./core/MatrixActor.js": {
"types": "./dist/src/core/MatrixActor.d.ts",
"import": "./dist/src/core/MatrixActor.js",
"default": "./dist/src/core/MatrixActor.js"
},
"./core/composite/IComponentRuntimeMetadata": {
"types": "./dist/src/core/composite/IComponentRuntimeMetadata.d.ts",
"import": "./dist/src/core/composite/IComponentRuntimeMetadata.js",
"default": "./dist/src/core/composite/IComponentRuntimeMetadata.js"
},
"./core/composite": {
"types": "./dist/src/core/composite/index.d.ts",
"import": "./dist/src/core/composite/index.js",
"default": "./dist/src/core/composite/index.js"
},
"./core/security/ICapabilityToken": {
"types": "./dist/src/core/security/ICapabilityToken.d.ts",
"import": "./dist/src/core/security/ICapabilityToken.js",
"default": "./dist/src/core/security/ICapabilityToken.js"
},
"./core/security/ISecurityRealm": {
"types": "./dist/src/core/security/ISecurityRealm.d.ts",
"import": "./dist/src/core/security/ISecurityRealm.js",
"default": "./dist/src/core/security/ISecurityRealm.js"
},
"./core/security/ITopicClaim": {
"types": "./dist/src/core/security/ITopicClaim.d.ts",
"import": "./dist/src/core/security/ITopicClaim.js",
"default": "./dist/src/core/security/ITopicClaim.js"
},
"./core/security/PolicyPresets": {
"types": "./dist/src/core/security/PolicyPresets.d.ts",
"import": "./dist/src/core/security/PolicyPresets.js",
"default": "./dist/src/core/security/PolicyPresets.js"
},
"./core/security/SecurityRealm": {
"types": "./dist/src/core/security/SecurityRealm.d.ts",
"import": "./dist/src/core/security/SecurityRealm.js",
"default": "./dist/src/core/security/SecurityRealm.js"
},
"./core/security/TopicClaimRegistry": {
"types": "./dist/src/core/security/TopicClaimRegistry.d.ts",
"import": "./dist/src/core/security/TopicClaimRegistry.js",
"default": "./dist/src/core/security/TopicClaimRegistry.js"
},
"./core/security/defaults": {
"types": "./dist/src/core/security/defaults.d.ts",
"import": "./dist/src/core/security/defaults.js",
"default": "./dist/src/core/security/defaults.js"
},
"./core/context/RealmContext": {
"types": "./dist/src/core/context/RealmContext.d.ts",
"import": "./dist/src/core/context/RealmContext.js",
"default": "./dist/src/core/context/RealmContext.js"
},
"./services/SessionInfoService": {
"types": "./dist/src/services/SessionInfoService.d.ts",
"import": "./dist/src/services/SessionInfoService.js",
"default": "./dist/src/services/SessionInfoService.js"
},
"./engine/core/IMatrixContext": {
"types": "./dist/src/engine/core/IMatrixContext.d.ts",
"import": "./dist/src/engine/core/IMatrixContext.js",
"default": "./dist/src/engine/core/IMatrixContext.js"
},
"./engine/core/MatrixContext": {
"types": "./dist/src/engine/core/MatrixContext.d.ts",
"import": "./dist/src/engine/core/MatrixContext.js",
"default": "./dist/src/engine/core/MatrixContext.js"
},
"./engine/messaging/TopicRouter": {
"types": "./dist/src/engine/messaging/TopicRouter.d.ts",
"import": "./dist/src/engine/messaging/TopicRouter.js",
"default": "./dist/src/engine/messaging/TopicRouter.js"
},
"./engine/remoting/ComponentProxy": {
"types": "./dist/src/engine/remoting/ComponentProxy.d.ts",
"import": "./dist/src/engine/remoting/ComponentProxy.js",
"default": "./dist/src/engine/remoting/ComponentProxy.js"
},
"./engine/remoting/IIntrospectResult": {
"types": "./dist/src/engine/remoting/IIntrospectResult.d.ts",
"import": "./dist/src/engine/remoting/IIntrospectResult.js",
"default": "./dist/src/engine/remoting/IIntrospectResult.js"
},
"./engine/remoting/ITransportAdapter": {
"types": "./dist/src/engine/remoting/ITransportAdapter.d.ts",
"import": "./dist/src/engine/remoting/ITransportAdapter.js",
"default": "./dist/src/engine/remoting/ITransportAdapter.js"
},
"./engine/remoting/RequestReply": {
"types": "./dist/src/engine/remoting/RequestReply.d.ts",
"import": "./dist/src/engine/remoting/RequestReply.js",
"default": "./dist/src/engine/remoting/RequestReply.js"
},
"./engine/remoting/RequestReply.js": {
"types": "./dist/src/engine/remoting/RequestReply.d.ts",
"import": "./dist/src/engine/remoting/RequestReply.js",
"default": "./dist/src/engine/remoting/RequestReply.js"
},
"./engine/utils/NameUtils": {
"types": "./dist/src/engine/utils/NameUtils.d.ts",
"import": "./dist/src/engine/utils/NameUtils.js",
"default": "./dist/src/engine/utils/NameUtils.js"
},
"./flow/FlowBuilder": {
"types": "./dist/src/flow/FlowBuilder.d.ts",
"import": "./dist/src/flow/FlowBuilder.js",
"default": "./dist/src/flow/FlowBuilder.js"
},
"./flow/FlowEngine": {
"types": "./dist/src/flow/FlowEngine.d.ts",
"import": "./dist/src/flow/FlowEngine.js",
"default": "./dist/src/flow/FlowEngine.js"
},
"./flow/FlowPlan": {
"types": "./dist/src/flow/FlowPlan.d.ts",
"import": "./dist/src/flow/FlowPlan.js",
"default": "./dist/src/flow/FlowPlan.js"
},
"./flow/FlowTypes": {
"types": "./dist/src/flow/FlowTypes.d.ts",
"import": "./dist/src/flow/FlowTypes.js",
"default": "./dist/src/flow/FlowTypes.js"
},
"./flow/viewers": {
"types": "./dist/src/flow/viewers/index.d.ts",
"import": "./dist/src/flow/viewers/index.js",
"default": "./dist/src/flow/viewers/index.js"
},
"./flow/viewers/ViewTypeRegistry": {
"types": "./dist/src/flow/viewers/ViewTypeRegistry.d.ts",
"import": "./dist/src/flow/viewers/ViewTypeRegistry.js",
"default": "./dist/src/flow/viewers/ViewTypeRegistry.js"
},
"./framework/ActivityFrame": {
"types": "./dist/src/framework/ActivityFrame.d.ts",
"import": "./dist/src/framework/ActivityFrame.js",
"default": "./dist/src/framework/ActivityFrame.js"
},
"./framework/IStateStore": {
"types": "./dist/src/framework/IStateStore.d.ts",
"import": "./dist/src/framework/IStateStore.js",
"default": "./dist/src/framework/IStateStore.js"
},
"./framework/MatrixActorHtmlElement": {
"types": "./dist/src/framework/MatrixActorHtmlElement.d.ts",
"import": "./dist/src/framework/MatrixActorHtmlElement.js",
"default": "./dist/src/framework/MatrixActorHtmlElement.js"
},
"./framework/PageManifest": {
"types": "./dist/src/framework/PageManifest.d.ts",
"import": "./dist/src/framework/PageManifest.js",
"default": "./dist/src/framework/PageManifest.js"
},
"./framework/components/MxSplit": {
"types": "./dist/src/framework/components/MxSplit.d.ts",
"import": "./dist/src/framework/components/MxSplit.js",
"default": "./dist/src/framework/components/MxSplit.js"
},
"./framework/components/MxTheme": {
"types": "./dist/src/framework/components/MxTheme.d.ts",
"import": "./dist/src/framework/components/MxTheme.js",
"default": "./dist/src/framework/components/MxTheme.js"
},
"./framework/components/MxTopicTag": {
"types": "./dist/src/framework/components/MxTopicTag.d.ts",
"import": "./dist/src/framework/components/MxTopicTag.js",
"default": "./dist/src/framework/components/MxTopicTag.js"
},
"./framework/components/index": {
"types": "./dist/src/framework/components/index.d.ts",
"import": "./dist/src/framework/components/index.js",
"default": "./dist/src/framework/components/index.js"
},
"./lisp": {
"types": "./dist/src/lisp/index.d.ts",
"import": "./dist/src/lisp/index.js",
"default": "./dist/src/lisp/index.js"
},
"./lisp/vlm": {
"types": "./dist/src/lisp/vlm/index.d.ts",
"import": "./dist/src/lisp/vlm/index.js",
"default": "./dist/src/lisp/vlm/index.js"
},
"./runtime/ComponentFactory": {
"types": "./dist/src/runtime/ComponentFactory.d.ts",
"import": "./dist/src/runtime/ComponentFactory.js",
"default": "./dist/src/runtime/ComponentFactory.js"
},
"./runtime/InstanceBootstrapContext": {
"types": "./dist/src/runtime/InstanceBootstrapContext.d.ts",
"import": "./dist/src/runtime/InstanceBootstrapContext.js",
"default": "./dist/src/runtime/InstanceBootstrapContext.js"
},
"./runtime/InstanceBootstrapContext.js": {
"types": "./dist/src/runtime/InstanceBootstrapContext.d.ts",
"import": "./dist/src/runtime/InstanceBootstrapContext.js",
"default": "./dist/src/runtime/InstanceBootstrapContext.js"
},
"./runtime/InstanceBootstrapRefs": {
"types": "./dist/src/runtime/InstanceBootstrapRefs.d.ts",
"import": "./dist/src/runtime/InstanceBootstrapRefs.js",
"default": "./dist/src/runtime/InstanceBootstrapRefs.js"
},
"./runtime/InstanceBootstrapRefs.js": {
"types": "./dist/src/runtime/InstanceBootstrapRefs.d.ts",
"import": "./dist/src/runtime/InstanceBootstrapRefs.js",
"default": "./dist/src/runtime/InstanceBootstrapRefs.js"
},
"./runtime/MatrixRuntime": {
"types": "./dist/src/runtime/MatrixRuntime.d.ts",
"import": "./dist/src/runtime/MatrixRuntime.js",
"default": "./dist/src/runtime/MatrixRuntime.js"
},
"./runtime/MatrixRuntime.js": {
"types": "./dist/src/runtime/MatrixRuntime.d.ts",
"import": "./dist/src/runtime/MatrixRuntime.js",
"default": "./dist/src/runtime/MatrixRuntime.js"
},
"./runtime/MountedInstanceMetadata": {
"types": "./dist/src/runtime/MountedInstanceMetadata.d.ts",
"import": "./dist/src/runtime/MountedInstanceMetadata.js",
"default": "./dist/src/runtime/MountedInstanceMetadata.js"
},
"./runtime/MountedInstanceMetadata.js": {
"types": "./dist/src/runtime/MountedInstanceMetadata.d.ts",
"import": "./dist/src/runtime/MountedInstanceMetadata.js",
"default": "./dist/src/runtime/MountedInstanceMetadata.js"
},
"./runtime/PackageConfigResolver": {
"types": "./dist/src/runtime/PackageConfigResolver.d.ts",
"import": "./dist/src/runtime/PackageConfigResolver.js",
"default": "./dist/src/runtime/PackageConfigResolver.js"
},
"./serialization": {
"types": "./dist/src/serialization/index.d.ts",
"import": "./dist/src/serialization/index.js",
"default": "./dist/src/serialization/index.js"
},
"./serialization/IComponentSchema": {
"types": "./dist/src/serialization/IComponentSchema.d.ts",
"import": "./dist/src/serialization/IComponentSchema.js",
"default": "./dist/src/serialization/IComponentSchema.js"
},
"./transport/InMemoryBroker": {
"types": "./dist/src/transport/InMemoryBroker.d.ts",
"import": "./dist/src/transport/InMemoryBroker.js",
"default": "./dist/src/transport/InMemoryBroker.js"
},
"./transport/InMemoryBroker.js": {
"types": "./dist/src/transport/InMemoryBroker.d.ts",
"import": "./dist/src/transport/InMemoryBroker.js",
"default": "./dist/src/transport/InMemoryBroker.js"
},
"./transport/InMemoryTransport": {
"types": "./dist/src/transport/InMemoryTransport.d.ts",
"import": "./dist/src/transport/InMemoryTransport.js",
"default": "./dist/src/transport/InMemoryTransport.js"
},
"./transport/InMemoryTransport.js": {
"types": "./dist/src/transport/InMemoryTransport.d.ts",
"import": "./dist/src/transport/InMemoryTransport.js",
"default": "./dist/src/transport/InMemoryTransport.js"
},
"./transport/NatsTransport": {
"types": "./dist/src/transport/NatsTransport.d.ts",
"import": "./dist/src/transport/NatsTransport.js",
"default": "./dist/src/transport/NatsTransport.js"
},
"./transport/NatsTransport.js": {
"types": "./dist/src/transport/NatsTransport.d.ts",
"import": "./dist/src/transport/NatsTransport.js",
"default": "./dist/src/transport/NatsTransport.js"
},
"./transport/TransportProxy": {
"types": "./dist/src/transport/TransportProxy.d.ts",
"import": "./dist/src/transport/TransportProxy.js",
"default": "./dist/src/transport/TransportProxy.js"
},
"./transport/createBrowserNatsTransport": {
"types": "./dist/src/transport/createBrowserNatsTransport.d.ts",
"import": "./dist/src/transport/createBrowserNatsTransport.js",
"default": "./dist/src/transport/createBrowserNatsTransport.js"
},
"./transport/createBrowserNatsTransport.js": {
"types": "./dist/src/transport/createBrowserNatsTransport.d.ts",
"import": "./dist/src/transport/createBrowserNatsTransport.js",
"default": "./dist/src/transport/createBrowserNatsTransport.js"
}
},
"scripts": {
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true,maxRetries:3,retryDelay:50});require('fs').rmSync('.tsbuildinfo',{force:true})\" && tsc -p tsconfig.build.json && node fix-esm-imports.mjs",
"test": "node ../../scripts/run-node-tests.cjs -- src/**/*.spec.ts",
"test:ci": "node ../../scripts/run-node-tests.cjs -- src/**/*.spec.ts",
"test:dom": "node ../../scripts/run-node-tests.cjs -- src/**/*.spec.ts",
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
"type-check": "tsc -p tsconfig.build.json --noEmit",
"test:unit": "tsx --test tests/unit/core/**/*.spec.ts"
},
"files": [
"dist/src",
"README.md",
"LICENSE"
],
"dependencies": {
"@open-matrix/contracts": "workspace:*",
"@open-matrix/federation": "workspace:*",
"@nats-io/nats-core": "^3.3.1",
"nats.ws": "^1.30.3",
"@open-matrix/omega-core": "workspace:*",
"morphdom": "^2.7.8"
},
"devDependencies": {
"tsx": "^4.15.6"
},
"engines": {
"node": ">=20"
}
}

2
packages/core/src/_node-compat.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
declare const g: any;
//# sourceMappingURL=_node-compat.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"_node-compat.d.ts","sourceRoot":"","sources":["_node-compat.ts"],"names":[],"mappings":"AAUA,QAAA,MAAM,CAAC,EAAiB,GAAG,CAAC"}

View File

@ -0,0 +1,274 @@
// src/_node-compat.ts
//
// DOM stubs for Node.js environments.
// When headless tests import the barrel (src/index.ts), browser-targeting classes
// like MatrixActorHtmlElement (extends HTMLElement) and MxInterface (customElements.define)
// fail because the DOM API doesn't exist. This shim provides enough surface
// for class declarations to evaluate AND for headless component tests to run.
//
// In the browser this file is a silent no-op — all globals already exist.
export {};
interface NodeCompatShadowRoot {
mode: string;
innerHTML: string;
getElementById: (id?: string) => null;
querySelector: (selector?: string) => null;
querySelectorAll: (selector?: string) => [];
appendChild: <T>(node: T) => T;
removeChild: <T>(node: T) => T;
replaceChildren: () => void;
addEventListener: (type: string, handler: EventListenerOrEventListenerObject) => void;
removeEventListener: (type: string, handler: EventListenerOrEventListenerObject) => void;
dispatchEvent: (event: Event) => boolean;
contains: (node: unknown) => boolean;
childNodes: unknown[];
children: unknown[];
firstChild: null;
}
interface NodeCompatTemplateElement {
innerHTML: string;
style?: {
setProperty: (name: string, value: string) => void;
getPropertyValue: (name: string) => string;
removeProperty: (name: string) => void;
};
content: {
childNodes: unknown[];
querySelectorAll: (selector?: string) => [];
};
tagName: string;
getAttribute: (name?: string) => null;
setAttribute: (name?: string, value?: string) => void;
}
interface NodeCompatDocument {
createElement: (tag: string) => NodeCompatTemplateElement;
createDocumentFragment: () => { appendChild: <T>(node: T) => T; childNodes: unknown[] };
querySelector: (selector?: string) => null;
querySelectorAll: (selector?: string) => [];
body: {
appendChild: <T>(node: T) => T;
removeChild: <T>(node: T) => T;
};
}
interface NodeCompatCustomElementsRegistry {
define: (name: string, ctor?: CustomElementConstructor) => void;
get: (name: string) => CustomElementConstructor | undefined;
getName?: (ctor: CustomElementConstructor) => string | null;
upgrade?: (root: Node) => void;
whenDefined?: (name: string) => Promise<CustomElementConstructor>;
}
type NodeCompatGlobal = typeof globalThis & {
HTMLElement?: typeof HTMLElement;
document?: NodeCompatDocument;
customElements?: NodeCompatCustomElementsRegistry;
requestAnimationFrame?: (callback: FrameRequestCallback) => number;
cancelAnimationFrame?: (handle: number) => void;
};
const g = globalThis as NodeCompatGlobal;
if (typeof g.HTMLElement === 'undefined') {
const NodeCompatHTMLElement = class {
private _attrs: Map<string, string> = new Map();
private _children: unknown[] = [];
private _style: Map<string, string> = new Map();
shadowRoot: NodeCompatShadowRoot | null = null;
localName = '';
tagName = '';
id = '';
innerHTML = '';
textContent = '';
className = '';
parentElement: Element | null = null;
style = {
setProperty: (name: string, value: string) => {
this._style.set(name, value);
},
getPropertyValue: (name: string) => this._style.get(name) ?? '',
removeProperty: (name: string) => {
this._style.delete(name);
},
};
// NamedNodeMap-like iterable for _bridgeAttributes
get attributes(): Iterable<{ name: string; value: string }> {
return Array.from(this._attrs.entries()).map(([name, value]) => ({ name, value }));
}
attachShadow(_opts?: { mode: string }): NodeCompatShadowRoot {
const childNodes: unknown[] = [];
const root: NodeCompatShadowRoot = {
mode: 'open',
innerHTML: '',
getElementById: () => null,
querySelector: () => null,
querySelectorAll: () => [],
appendChild: <T>(n: T) => {
childNodes.push(n);
return n;
},
removeChild: <T>(n: T) => {
const index = childNodes.indexOf(n);
if (index >= 0) childNodes.splice(index, 1);
return n;
},
replaceChildren: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => true,
contains: (node: unknown) => childNodes.includes(node),
childNodes,
children: childNodes,
firstChild: null,
};
this.shadowRoot = root;
return root;
}
getAttribute(name: string): string | null {
return this._attrs.get(name) ?? null;
}
setAttribute(name: string, value: string): void {
this._attrs.set(name, value);
if (name === 'id') this.id = value;
}
hasAttribute(name: string): boolean {
return this._attrs.has(name);
}
removeAttribute(name: string): void {
this._attrs.delete(name);
}
appendChild<T>(node: T): T {
this._children.push(node);
return node;
}
removeChild<T>(node: T): T {
const idx = this._children.indexOf(node);
if (idx >= 0) this._children.splice(idx, 1);
return node;
}
get children(): unknown[] { return this._children; }
get childNodes(): unknown[] { return this._children; }
closest(_selector: string): Element | null { return null; }
querySelector(_selector: string): Element | null { return null; }
querySelectorAll(_selector: string): Element[] { return []; }
addEventListener(_type: string, _handler: EventListenerOrEventListenerObject): void {}
removeEventListener(_type: string, _handler: EventListenerOrEventListenerObject): void {}
dispatchEvent(_event: Event): boolean { return true; }
};
g.HTMLElement = NodeCompatHTMLElement as unknown as typeof HTMLElement;
}
if (typeof g.document === 'undefined') {
// Minimal DOM shim for Node.js — intentionally incomplete, only what the framework uses.
// These casts are necessary because TypeScript's DOM types are far richer than this shim.
g.document = {
createElement(tag: string): NodeCompatTemplateElement {
const lowerTag = tag.toLowerCase();
const definedCtor = g.customElements?.get?.(lowerTag);
if (definedCtor) {
const element = new definedCtor() as HTMLElement & { localName?: string; tagName?: string };
element.localName = lowerTag;
element.tagName = lowerTag.toUpperCase();
return element as unknown as NodeCompatTemplateElement;
}
const content = {
childNodes: [] as unknown[],
querySelectorAll: (_selector?: string) => [] as unknown as [],
};
const element = new g.HTMLElement() as HTMLElement & NodeCompatTemplateElement & {
localName?: string;
tagName?: string;
content?: typeof content;
};
element.localName = lowerTag;
element.tagName = lowerTag.toUpperCase();
if (lowerTag === 'template') {
element.content = content;
}
return Object.assign(element, {
innerHTML: '',
getAttribute: () => null,
setAttribute: () => {},
});
},
createDocumentFragment() {
return { appendChild: <T>(n: T) => n, childNodes: [] as unknown[] };
},
querySelector() {
return null;
},
querySelectorAll() {
return [] as unknown as [];
},
body: { appendChild: <T>(n: T) => n, removeChild: <T>(n: T) => n },
} as NodeCompatDocument as typeof g.document;
}
if (typeof g.customElements === 'undefined') {
const registry = new Map<string, CustomElementConstructor>();
const pending = new Map<string, Array<(ctor: CustomElementConstructor) => void>>();
g.customElements = {
define(name: string, ctor?: CustomElementConstructor) {
if (!ctor) {
throw new TypeError(`Custom element '${name}' requires a constructor`);
}
if (registry.has(name)) return;
registry.set(name, ctor);
const waiters = pending.get(name);
if (waiters) {
pending.delete(name);
for (const resolve of waiters) {
resolve(ctor);
}
}
},
get(name: string) {
return registry.get(name);
},
getName(ctor: CustomElementConstructor) {
for (const [name, current] of registry.entries()) {
if (current === ctor) return name;
}
return null;
},
upgrade() {},
whenDefined(name: string) {
const existing = registry.get(name);
if (existing) return Promise.resolve(existing);
return new Promise<CustomElementConstructor>((resolve) => {
const waiters = pending.get(name);
if (waiters) {
waiters.push(resolve);
return;
}
pending.set(name, [resolve]);
});
},
} as NodeCompatCustomElementsRegistry as typeof g.customElements;
}
if (typeof g.requestAnimationFrame === 'undefined') {
g.requestAnimationFrame = ((callback: FrameRequestCallback) => {
const handle = setTimeout(() => callback(Date.now()), 16);
return Number(handle);
}) as typeof g.requestAnimationFrame;
}
if (typeof g.cancelAnimationFrame === 'undefined') {
g.cancelAnimationFrame = ((handle: number) => {
clearTimeout(handle);
}) as typeof g.cancelAnimationFrame;
}

View File

@ -0,0 +1,50 @@
export type AppRouteSource =
| 'gateway'
| 'system.devices'
| 'runtime-registry'
| 'projection';
export type AppRouteServedBy =
| 'linked-device'
| 'current-host'
| 'platform-local'
| 'auto-selected'
| 'unavailable';
export type AppRouteTargetSource =
| 'explicit'
| 'namespace'
| 'auto'
| 'linked-device'
| 'current-host'
| 'linked-device-route'
| 'linked-device-offline'
| 'linked-device-app-route-missing'
| 'linked-device-asset-mount-unreachable'
| 'linked-device-runtime-control-unavailable'
| 'current-host-route'
| 'dashboard-section';
export interface IAppRouteView {
readonly appName: string;
readonly url: string;
readonly routePrefix: string;
readonly routeKey?: string;
readonly authorityRoot?: string;
readonly runtimeTargetRoot?: string;
readonly runtimeWireRoot?: string;
readonly deviceId?: string;
readonly runtimeHostViewId?: string;
readonly runtimeId?: string;
readonly packageRef?: string;
readonly routeSource: AppRouteSource;
readonly servedBy: AppRouteServedBy;
readonly targetSource?: AppRouteTargetSource;
readonly requestedTarget?: string;
readonly actualTarget?: string;
readonly assetMount?: string;
readonly origin?: string;
readonly runtimeControlHealthStatus?: string;
readonly runtimeControlHealthReason?: string;
readonly recoveryReason?: string;
}

View File

@ -0,0 +1,42 @@
import type { WellKnownServiceBinding } from '../engine/core/ScopedIntentResolver';
import {
PlatformAuthContract,
PlatformCapabilitiesContract,
UserAgentsContract,
UserDevicesContract,
UserRegistryContract,
UserRuntimesContract,
} from './WellKnownServiceContracts';
export const WELL_KNOWN_SERVICE_BINDINGS: readonly WellKnownServiceBinding[] = [
{
contractId: UserRegistryContract.id,
defaultScope: 'user',
mount: 'system.registry',
},
{
contractId: UserDevicesContract.id,
defaultScope: 'account',
mount: 'system.devices',
},
{
contractId: UserRuntimesContract.id,
defaultScope: 'user',
mount: 'system.runtimes',
},
{
contractId: UserAgentsContract.id,
defaultScope: 'user',
mount: 'system.agents',
},
{
contractId: PlatformAuthContract.id,
defaultScope: 'platform',
mount: 'system.auth',
},
{
contractId: PlatformCapabilitiesContract.id,
defaultScope: 'platform',
mount: 'system.capabilities',
},
];

View File

@ -0,0 +1,190 @@
import type { ServiceContract } from '../engine/core/ServiceContract';
export const UserRegistryContract: ServiceContract = {
id: 'matrix.user.registry',
description: 'Registry for the active user Space.',
ops: {
'registry.list': {
description: 'List actor/service claims for the active user Space.',
payload: {},
response: {
ok: { type: 'boolean', required: true },
claims: { type: 'array', required: false },
entries: { type: 'array', required: false },
},
},
'registry.query': {
description: 'Query actor/service claims for the active user Space.',
payload: {
namespaceRoot: { type: 'string', required: false },
authorityRoot: { type: 'string', required: false },
principalId: { type: 'string', required: false },
busToken: { type: 'string', required: false },
includeStale: { type: 'boolean', required: false },
includeOffline: { type: 'boolean', required: false },
},
response: {
ok: { type: 'boolean', required: true },
claims: { type: 'array', required: false },
entries: { type: 'array', required: false },
events: { type: 'array', required: false },
},
},
},
};
export const UserDevicesContract: ServiceContract = {
id: 'matrix.user.devices',
description: 'Account-level linked Device inventory for the authenticated user.',
ops: {
'devices.list': {
description: 'List linked Devices for the authenticated account.',
payload: {
authorityRoot: { type: 'string', required: false },
principalId: { type: 'string', required: false },
busToken: { type: 'string', required: false },
includeOffline: { type: 'boolean', required: false },
},
response: {
ok: { type: 'boolean', required: true },
devices: { type: 'array', required: true },
},
},
'devices.get': {
description: 'Read one linked Device for the authenticated account.',
payload: {
deviceId: { type: 'string', required: true },
},
response: {
ok: { type: 'boolean', required: true },
device: { type: 'object', required: false },
},
},
'devices.revoke': {
description: 'Revoke/unlink one account-level Device Link.',
payload: {
authorityRoot: { type: 'string', required: false },
deviceId: { type: 'string', required: true },
principalId: { type: 'string', required: false },
busToken: { type: 'string', required: false },
},
response: {
ok: { type: 'boolean', required: true },
},
},
},
};
export const UserRuntimesContract: ServiceContract = {
id: 'matrix.user.runtimes',
description: 'Runtime inventory/control-plane service for the active user Space.',
ops: {
'runtimes.list': {
description: 'List runtimes for the active user Space.',
payload: {
namespaceRoot: { type: 'string', required: false },
authorityRoot: { type: 'string', required: false },
principalId: { type: 'string', required: false },
busToken: { type: 'string', required: false },
includeStale: { type: 'boolean', required: false },
includeOffline: { type: 'boolean', required: false },
},
response: {
ok: { type: 'boolean', required: true },
runtimes: { type: 'array', required: true },
},
},
'runtimes.registered': {
description: 'List registered runtimes for the active user Space.',
payload: {
namespaceRoot: { type: 'string', required: false },
authorityRoot: { type: 'string', required: false },
includeStale: { type: 'boolean', required: false },
},
response: {
ok: { type: 'boolean', required: true },
runtimes: { type: 'array', required: true },
},
},
'$introspect': {
description: 'Introspect a runtime control-plane mount.',
payload: {
depth: { type: 'string', required: false },
},
response: {
ok: { type: 'boolean', required: false },
mount: { type: 'string', required: false },
children: { type: 'array', required: false },
},
},
},
};
export const UserAgentsContract: ServiceContract = {
id: 'matrix.user.agents',
description: 'Agent/session service for the active user Space.',
ops: {
'$sessionList': {
description: 'List agent sessions for the active user Space.',
payload: {},
response: {
ok: { type: 'boolean', required: false },
sessions: { type: 'array', required: true },
},
},
'session_state.read': {
description: 'Read agent session state.',
payload: {
sessionId: { type: 'string', required: true },
},
response: {
ok: { type: 'boolean', required: false },
state: { type: 'object', required: false },
},
},
},
};
export const PlatformAuthContract: ServiceContract = {
id: 'matrix.platform.auth',
description: 'Platform authentication/session service.',
ops: {
'session.current': {
description: 'Read current authenticated platform session.',
payload: {},
response: {
ok: { type: 'boolean', required: true },
principal: { type: 'object', required: false },
},
},
'auth.accountDevices.list': {
description: 'List account-level registered Devices for the authenticated principal.',
payload: {
principalId: { type: 'string', required: false },
busToken: { type: 'string', required: false },
includeGrants: { type: 'boolean', required: false },
},
response: {
ok: { type: 'boolean', required: true },
devices: { type: 'array', required: true },
},
},
},
};
export const PlatformCapabilitiesContract: ServiceContract = {
id: 'matrix.platform.capabilities',
description: 'Platform capability resolution and grant service.',
ops: {
'capability.check': {
description: 'Check whether the caller has a capability.',
payload: {
capability: { type: 'string', required: true },
},
response: {
ok: { type: 'boolean', required: true },
allowed: { type: 'boolean', required: true },
},
},
},
};

View File

@ -0,0 +1,60 @@
type CheatType = 'P1' | 'P2' | 'P3' | 'P4' | 'P5' | 'P6';
interface CheatViolation {
type: CheatType;
message: string;
stack?: string;
timestamp: number;
}
type CheatHandler = (violation: CheatViolation) => void;
declare class CheatDetectorImpl {
private enabled;
private violations;
private handlers;
private mode;
/**
* Enable cheat detection.
* @param mode - 'warn' logs to console, 'throw' throws error, 'collect' silently collects
*/
enable(mode?: 'warn' | 'throw' | 'collect'): void;
disable(): void;
isEnabled(): boolean;
/**
* Report a cheat violation.
*/
report(type: CheatType, message: string): void;
/**
* Get all collected violations.
*/
getViolations(): CheatViolation[];
/**
* Clear collected violations.
*/
clear(): void;
/**
* Add a violation handler.
*/
onViolation(handler: CheatHandler): () => void;
/**
* Assert no violations occurred (for tests).
*/
assertNoViolations(): void;
/**
* P1: Wrap runtime to detect _components access.
*/
wrapRuntime<T extends object>(runtime: T): T;
/**
* P2: Check if a value is a component reference (should be mount string).
*/
checkNotComponentRef(value: unknown, context: string): void;
/**
* P5: Detect _localServices usage.
*/
reportLocalServiceBypass(mount: string): void;
/**
* P6: Detect ghost casting.
*/
reportGhostCasting(shellTag: string): void;
}
export declare const CheatDetector: CheatDetectorImpl;
export {};
//# sourceMappingURL=CheatDetector.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"CheatDetector.d.ts","sourceRoot":"","sources":["CheatDetector.ts"],"names":[],"mappings":"AAaA,KAAK,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEzD,UAAU,cAAc;IACtB,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,KAAK,YAAY,GAAG,CAAC,SAAS,EAAE,cAAc,KAAK,IAAI,CAAC;AAExD,cAAM,iBAAiB;IACrB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAwB;IAC1C,OAAO,CAAC,QAAQ,CAAsB;IACtC,OAAO,CAAC,IAAI,CAAwC;IAEpD;;;OAGG;IACH,MAAM,CAAC,IAAI,GAAE,MAAM,GAAG,OAAO,GAAG,SAAkB,GAAG,IAAI;IAMzD,OAAO,IAAI,IAAI;IAIf,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAgC9C;;OAEG;IACH,aAAa,IAAI,cAAc,EAAE;IAIjC;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,IAAI;IAQ9C;;OAEG;IACH,kBAAkB,IAAI,IAAI;IAa1B;;OAEG;IACH,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC;IAa5C;;OAEG;IACH,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAiB3D;;OAEG;IACH,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI7C;;OAEG;IACH,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;CAG3C;AAGD,eAAO,MAAM,aAAa,mBAA0B,CAAC"}

View File

@ -0,0 +1,184 @@
// src/core/CheatDetector.ts
//
// Detects architectural violations ("cheats") at runtime.
// Enable via: CheatDetector.enable() or MATRIX_STRICT=1 env var
//
// Cheats detected:
// - P1: Accessing runtime._components directly
// - P2: Storing component references instead of mount paths
// - P3: Using __parent chain
// - P4: Accessing children map directly
// - P5: Bypassing transport with _localServices
// - P6: Casting to get internal component from shell
type CheatType = 'P1' | 'P2' | 'P3' | 'P4' | 'P5' | 'P6';
interface CheatViolation {
type: CheatType;
message: string;
stack?: string;
timestamp: number;
}
type CheatHandler = (violation: CheatViolation) => void;
class CheatDetectorImpl {
private enabled = false;
private violations: CheatViolation[] = [];
private handlers: CheatHandler[] = [];
private mode: 'warn' | 'throw' | 'collect' = 'warn';
/**
* Enable cheat detection.
* @param mode - 'warn' logs to console, 'throw' throws error, 'collect' silently collects
*/
enable(mode: 'warn' | 'throw' | 'collect' = 'warn'): void {
this.enabled = true;
this.mode = mode;
// eslint-disable-next-line no-console -- developer-facing cheat detector mode announcement in kernel code
console.log(`[CheatDetector] Enabled in '${mode}' mode`);
}
disable(): void {
this.enabled = false;
}
isEnabled(): boolean {
return this.enabled;
}
/**
* Report a cheat violation.
*/
report(type: CheatType, message: string): void {
if (!this.enabled) return;
const violation: CheatViolation = {
type,
message,
stack: new Error().stack,
timestamp: Date.now(),
};
this.violations.push(violation);
// Notify handlers
for (const handler of this.handlers) {
try {
handler(violation);
} catch {}
}
// Handle based on mode
switch (this.mode) {
case 'throw':
throw new Error(`[CHEAT DETECTED] ${type}: ${message}`);
case 'warn':
// eslint-disable-next-line no-console -- cheat detector violations must surface without actor context
console.warn(`[CHEAT DETECTED] ${type}: ${message}`);
break;
case 'collect':
// Silent collection
break;
}
}
/**
* Get all collected violations.
*/
getViolations(): CheatViolation[] {
return [...this.violations];
}
/**
* Clear collected violations.
*/
clear(): void {
this.violations = [];
}
/**
* Add a violation handler.
*/
onViolation(handler: CheatHandler): () => void {
this.handlers.push(handler);
return () => {
const idx = this.handlers.indexOf(handler);
if (idx >= 0) this.handlers.splice(idx, 1);
};
}
/**
* Assert no violations occurred (for tests).
*/
assertNoViolations(): void {
if (this.violations.length > 0) {
const summary = this.violations
.map(v => ` ${v.type}: ${v.message}`)
.join('\n');
throw new Error(`CheatDetector found ${this.violations.length} violation(s):\n${summary}`);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Specific Cheat Detectors
// ═══════════════════════════════════════════════════════════════════════════
/**
* P1: Wrap runtime to detect _components access.
*/
wrapRuntime<T extends object>(runtime: T): T {
if (!this.enabled) return runtime;
return new Proxy(runtime, {
get: (target, prop, receiver) => {
if (prop === '_components') {
this.report('P1', 'Direct access to runtime._components');
}
return Reflect.get(target, prop, receiver);
},
});
}
/**
* P2: Check if a value is a component reference (should be mount string).
*/
checkNotComponentRef(value: unknown, context: string): void {
if (!this.enabled) return;
if (value && typeof value === 'object') {
// Check for MatrixActor-like shape
const obj = value as Record<string, unknown>;
if (
typeof obj.initialize === 'function' ||
typeof obj.dispose === 'function' ||
typeof obj.onIntrospect === 'function' ||
obj._context !== undefined
) {
this.report('P2', `Storing component reference instead of mount path: ${context}`);
}
}
}
/**
* P5: Detect _localServices usage.
*/
reportLocalServiceBypass(mount: string): void {
this.report('P5', `Bypassing transport with _localServices for: ${mount}`);
}
/**
* P6: Detect ghost casting.
*/
reportGhostCasting(shellTag: string): void {
this.report('P6', `Casting shell to get internal component: ${shellTag}`);
}
}
// Singleton instance
export const CheatDetector = new CheatDetectorImpl();
// Auto-enable if env var set
if (typeof process !== 'undefined' && process.env?.MATRIX_STRICT === '1') {
CheatDetector.enable('throw');
}

View File

@ -0,0 +1,51 @@
import type { IComponentRegistry, ComponentConstructor, ComponentLoader } from './IComponentRegistry';
/**
* ComponentRegistry - Hierarchical component registry implementation.
*
* NO SINGLETONS - this is instantiated per-context/per-composite.
*
* Features:
* - Parent chain inheritance (child shadows parent)
* - Lazy loading via dynamic imports
* - Module path resolution
* - Caching of resolved constructors
*/
export declare class HeadlessComponentRegistry implements IComponentRegistry {
private readonly _registrations;
private readonly _resolved;
readonly parent?: IComponentRegistry;
constructor(parent?: IComponentRegistry);
/**
* Resolve a tag to a component constructor.
*/
resolve(tag: string): Promise<ComponentConstructor | undefined>;
/**
* Register a component loader.
*/
register(tag: string, loader: ComponentLoader): void;
/**
* Check if tag is registered anywhere in the chain.
*/
has(tag: string): boolean;
/**
* List tags registered locally.
*/
localTags(): string[];
/**
* List all available tags (local + parent chain).
*/
allTags(): string[];
/**
* Create a child registry inheriting from this one.
*/
createChild(): IComponentRegistry;
/**
* Resolve a loader to a constructor.
*/
private _resolveLoader;
/**
* Create a root registry with no parent.
*/
static createRoot(): IComponentRegistry;
}
//# sourceMappingURL=ComponentRegistry.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"ComponentRegistry.d.ts","sourceRoot":"","sources":["ComponentRegistry.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EACpB,eAAe,EAChB,MAAM,sBAAsB,CAAC;AAU9B;;;;;;;;;;GAUG;AACH,qBAAa,yBAA0B,YAAW,kBAAkB;IAClE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAsC;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2C;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,kBAAkB,CAAC;gBAEzB,MAAM,CAAC,EAAE,kBAAkB;IAIvC;;OAEG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IA0BrE;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI;IAOpD;;OAEG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAWzB;;OAEG;IACH,SAAS,IAAI,MAAM,EAAE;IAIrB;;OAEG;IACH,OAAO,IAAI,MAAM,EAAE;IAUnB;;OAEG;IACH,WAAW,IAAI,kBAAkB;IAIjC;;OAEG;YACW,cAAc;IA0D5B;;OAEG;IACH,MAAM,CAAC,UAAU,IAAI,kBAAkB;CAGxC"}

View File

@ -0,0 +1,186 @@
// src/core/ComponentRegistry.ts
import type {
IComponentRegistry,
ComponentConstructor,
ComponentLoader,
} from './IComponentRegistry';
/**
* Interface for dynamically imported module result.
*/
interface IDynamicModule {
default?: ComponentConstructor;
[key: string]: unknown;
}
/**
* ComponentRegistry - Hierarchical component registry implementation.
*
* NO SINGLETONS - this is instantiated per-context/per-composite.
*
* Features:
* - Parent chain inheritance (child shadows parent)
* - Lazy loading via dynamic imports
* - Module path resolution
* - Caching of resolved constructors
*/
export class HeadlessComponentRegistry implements IComponentRegistry {
private readonly _registrations = new Map<string, ComponentLoader>();
private readonly _resolved = new Map<string, ComponentConstructor>();
readonly parent?: IComponentRegistry;
constructor(parent?: IComponentRegistry) {
this.parent = parent;
}
/**
* Resolve a tag to a component constructor.
*/
async resolve(tag: string): Promise<ComponentConstructor | undefined> {
const normalizedTag = tag.toLowerCase();
// Check local cache first
if (this._resolved.has(normalizedTag)) {
return this._resolved.get(normalizedTag);
}
// Check local registrations
if (this._registrations.has(normalizedTag)) {
const loader = this._registrations.get(normalizedTag)!;
const ctor = await this._resolveLoader(loader);
if (ctor) {
this._resolved.set(normalizedTag, ctor);
return ctor;
}
}
// Check parent chain
if (this.parent) {
return this.parent.resolve(normalizedTag);
}
return undefined;
}
/**
* Register a component loader.
*/
register(tag: string, loader: ComponentLoader): void {
const normalizedTag = tag.toLowerCase();
this._registrations.set(normalizedTag, loader);
// Clear cache in case it was resolved before
this._resolved.delete(normalizedTag);
}
/**
* Check if tag is registered anywhere in the chain.
*/
has(tag: string): boolean {
const normalizedTag = tag.toLowerCase();
if (this._registrations.has(normalizedTag)) {
return true;
}
if (this.parent) {
return this.parent.has(normalizedTag);
}
return false;
}
/**
* List tags registered locally.
*/
localTags(): string[] {
return Array.from(this._registrations.keys());
}
/**
* List all available tags (local + parent chain).
*/
allTags(): string[] {
const tags = new Set<string>(this._registrations.keys());
if (this.parent) {
for (const tag of this.parent.allTags()) {
tags.add(tag);
}
}
return Array.from(tags);
}
/**
* Create a child registry inheriting from this one.
*/
createChild(): IComponentRegistry {
return new HeadlessComponentRegistry(this);
}
/**
* Resolve a loader to a constructor.
*/
private async _resolveLoader(loader: ComponentLoader): Promise<ComponentConstructor | undefined> {
// Direct constructor
if (typeof loader === 'function' && 'prototype' in loader && loader.prototype) {
// Check if it's a class (has prototype with constructor)
try {
// If it looks like a class constructor (not an arrow function)
const loaderProto = loader.prototype as { constructor?: unknown };
if (loader.toString().startsWith('class') || loaderProto.constructor === loader) {
return loader as ComponentConstructor;
}
} catch {
// Fall through to try as dynamic import
}
}
// Dynamic import function
if (typeof loader === 'function') {
try {
const result = await (loader as () => Promise<unknown>)();
// Handle { default: Constructor } or direct Constructor
if (result && typeof result === 'object' && 'default' in result) {
const moduleResult = result as { default: unknown };
if (typeof moduleResult.default === 'function') {
return moduleResult.default as ComponentConstructor;
}
}
if (typeof result === 'function') {
return result as ComponentConstructor;
}
} catch (err) {
// eslint-disable-next-line no-console -- kernel component loading failures need direct diagnostics
console.error(`[ComponentRegistry] Failed to load component:`, err);
return undefined;
}
}
// Module path string
if (typeof loader === 'string') {
try {
const module = await import(loader) as IDynamicModule;
if (module.default) {
return module.default;
}
// Try to find first exported class
for (const key of Object.keys(module)) {
const exported = module[key];
if (typeof exported === 'function' && 'prototype' in exported && exported.prototype) {
return exported as ComponentConstructor;
}
}
} catch (err) {
// eslint-disable-next-line no-console -- kernel module import failures need direct diagnostics
console.error(`[ComponentRegistry] Failed to import '${loader}':`, err);
return undefined;
}
}
return undefined;
}
/**
* Create a root registry with no parent.
*/
static createRoot(): IComponentRegistry {
return new HeadlessComponentRegistry();
}
}

View File

@ -0,0 +1,61 @@
import type { MatrixActor } from './MatrixActor';
/**
* Component constructor type.
*/
export type ComponentConstructor = new () => MatrixActor;
/**
* Component loader - can be direct class, dynamic import function, or module path.
*/
export type ComponentLoader = ComponentConstructor | (() => Promise<ComponentConstructor>) | (() => Promise<{
default: ComponentConstructor;
}>) | string;
/**
* IComponentRegistry - Hierarchical component registry interface.
*
* KEY PRINCIPLE: NO SINGLETONS
* - Each composite can have its own registry
* - Child registries inherit from parent
* - Local registrations shadow parent registrations
* - Passed through context, never global
*/
export interface IComponentRegistry {
/**
* Resolve a tag name to a component constructor.
* Searches local registry first, then parent chain.
* May trigger dynamic import if loader is a function/path.
*
* @param tag - Tag name (e.g., 'my-component')
* @returns Component constructor or undefined if not found
*/
resolve(tag: string): Promise<ComponentConstructor | undefined>;
/**
* Register a component in this registry.
* Available to this registry and all descendants.
*
* @param tag - Tag name
* @param loader - Component class, dynamic import function, or module path
*/
register(tag: string, loader: ComponentLoader): void;
/**
* Check if a tag is registered (in this registry or ancestors).
*/
has(tag: string): boolean;
/**
* List all tags registered in this registry (not including parent).
*/
localTags(): string[];
/**
* List all tags available (including parent chain).
*/
allTags(): string[];
/**
* Create a child registry that inherits from this one.
* Local registrations in child shadow parent registrations.
*/
createChild(): IComponentRegistry;
/**
* Get the parent registry (if any).
*/
readonly parent?: IComponentRegistry;
}
//# sourceMappingURL=IComponentRegistry.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"IComponentRegistry.d.ts","sourceRoot":"","sources":["IComponentRegistry.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,UAAU,WAAW,CAAC;AAEzD;;GAEG;AACH,MAAM,MAAM,eAAe,GACvB,oBAAoB,GACpB,CAAC,MAAM,OAAO,CAAC,oBAAoB,CAAC,CAAC,GACrC,CAAC,MAAM,OAAO,CAAC;IAAE,OAAO,EAAE,oBAAoB,CAAA;CAAE,CAAC,CAAC,GAClD,MAAM,CAAC;AAEX;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;OAOG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAAC;IAEhE;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC;IAErD;;OAEG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAE1B;;OAEG;IACH,SAAS,IAAI,MAAM,EAAE,CAAC;IAEtB;;OAEG;IACH,OAAO,IAAI,MAAM,EAAE,CAAC;IAEpB;;;OAGG;IACH,WAAW,IAAI,kBAAkB,CAAC;IAElC;;OAEG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,kBAAkB,CAAC;CACtC"}

View File

@ -0,0 +1,73 @@
// src/core/IComponentRegistry.ts
import type { MatrixActor } from './MatrixActor';
/**
* Component constructor type.
*/
export type ComponentConstructor = new () => MatrixActor;
/**
* Component loader - can be direct class, dynamic import function, or module path.
*/
export type ComponentLoader =
| ComponentConstructor
| (() => Promise<ComponentConstructor>)
| (() => Promise<{ default: ComponentConstructor }>)
| string;
/**
* IComponentRegistry - Hierarchical component registry interface.
*
* KEY PRINCIPLE: NO SINGLETONS
* - Each composite can have its own registry
* - Child registries inherit from parent
* - Local registrations shadow parent registrations
* - Passed through context, never global
*/
export interface IComponentRegistry {
/**
* Resolve a tag name to a component constructor.
* Searches local registry first, then parent chain.
* May trigger dynamic import if loader is a function/path.
*
* @param tag - Tag name (e.g., 'my-component')
* @returns Component constructor or undefined if not found
*/
resolve(tag: string): Promise<ComponentConstructor | undefined>;
/**
* Register a component in this registry.
* Available to this registry and all descendants.
*
* @param tag - Tag name
* @param loader - Component class, dynamic import function, or module path
*/
register(tag: string, loader: ComponentLoader): void;
/**
* Check if a tag is registered (in this registry or ancestors).
*/
has(tag: string): boolean;
/**
* List all tags registered in this registry (not including parent).
*/
localTags(): string[];
/**
* List all tags available (including parent chain).
*/
allTags(): string[];
/**
* Create a child registry that inherits from this one.
* Local registrations in child shadow parent registrations.
*/
createChild(): IComponentRegistry;
/**
* Get the parent registry (if any).
*/
readonly parent?: IComponentRegistry;
}

View File

@ -0,0 +1,35 @@
/**
* IRecordStoreProvider pluggable per-package record-store factory.
*
* The runtime registers an implementation on the root context via
* RECORD_STORE_PROVIDER_KEY. PackageRootActor reads the provider from its
* cascading context and asks for a per-package store at bootstrap time. The
* provider returns whatever value the consumer of RECORD_STORE_KEY expects
* (today: a database handle; tomorrow: an IRecordStore wrapper or null).
*
* Core declares the seam. Implementations live in their own packages
* (e.g., @open-matrix/system-sqlite). Core never imports a sibling
* implementation package.
*/
export interface IRecordStoreProvider {
/**
* Produce a per-package record store, or return null if unavailable.
*
* Same observable contract as the legacy inline sqlite call inside
* PackageRootActor: same matrixHome resolution, same domain key, same
* eventual setService(RECORD_STORE_KEY, value) value shape.
*
* Implementations must not throw for "no backend available" cases
* return null and let PackageRootActor swallow it identically to the
* pre-seam behavior.
*/
provide(matrixHome: string, domain: string): Promise<unknown | null>;
}
/**
* Service-registry key for the IRecordStoreProvider. The runtime registers
* the implementation under this key on the root context; PackageRootActor
* reads it via context.getService(RECORD_STORE_PROVIDER_KEY).
*/
export const RECORD_STORE_PROVIDER_KEY = Symbol.for('matrix.record-store.provider');

635
packages/core/src/core/MatrixActor.d.ts vendored Normal file
View File

@ -0,0 +1,635 @@
import type { IMatrixContext } from '../engine/core/IMatrixContext';
import { SubscriptionBag } from '../engine/messaging/SubscriptionBag';
import { ActorBehavior } from './actor/ActorBehavior';
import type { IMailbox, IExitSignal } from '../engine/mailbox/IMailbox';
import { LamportClock } from '../engine/messaging/LamportClock';
import { ChildManager } from './children/ChildManager';
import { type ISupervisionPolicy } from './Supervision';
import { SupervisorBehavior } from './actor/SupervisorBehavior';
import { SecurityRealm, type ISupervisorSecurityPolicy } from './security';
import type { ICapabilityToken } from './security/ICapabilityToken';
import type { ICompositeIntrospectChild, IComponentTreeDescriptor, Position, IAddChildOptions, IDynamicComponentDefinition } from './composite';
export type MessageHandler = (payload: unknown, meta: {
topic: string;
ts: number;
}) => void | Promise<void>;
export type ComponentHandler = (payload: unknown, meta: {
topic: string;
ts: number;
}) => unknown | Promise<unknown>;
/**
* Deep binding descriptor produced by the DSL compiler.
* Maps a descendant event to a handler on the template author's actor.
*/
export interface DeepBindingDescriptor {
/** Dot-delimited descendant path relative to this actor, e.g., 'split.preview.syntax' */
relativePath: string;
/** Event name, e.g., 'HighlightReady' */
event: string;
/** Handler method name on this actor, e.g., 'onHighlightReadyFromSyntax' */
handler: string;
}
export type { Position } from './composite';
export declare abstract class MatrixActor {
/**
* Index signature for dynamic handler method access.
* Enables type-safe lookup of onFoo handlers by name.
*/
[key: string]: unknown;
/**
* Static metadata declaring what DOMAIN commands this actor accepts.
* System commands ($join, $leave, introspect, $ping) are auto-subscribed.
* Override in subclass.
*/
static accepts: Record<string, unknown>;
/**
* Static metadata declaring what events this actor emits.
* Override in subclass.
*/
static emits: Record<string, unknown>;
/**
* Theme tokens this actor declares support for.
* Each key is a token name (without --mx- prefix), value is metadata.
* Used by the $tokens handler and surfaced in $introspect / oracle snapshots.
* Subclass declarations merge with (and override) parent declarations.
*/
static tokens: Record<string, {
default: string;
description: string;
}>;
/**
* Output schemas per op. Describes what each handler returns.
* Surfaced via $introspect { depth: 'full' } alongside acceptsSchema.
*/
static outputSchemas?: Record<string, Record<string, unknown>>;
/**
* Error specifications per op. Documents known error conditions.
* Each entry is an array of { code, description } objects.
*/
static errorSpecs?: Record<string, Array<{
code: string;
description: string;
}>>;
/**
* Optional self-documenting examples surfaced via $introspect { depth: 'full' }.
* Per-op examples with input/output pairs for AI agent discovery.
*/
static examples?: Record<string, Array<{
input: Record<string, unknown>;
output?: Record<string, unknown>;
}>>;
/**
* Optional intent filters for capability-based resolution.
*/
static intentFilters?: unknown[];
/**
* Higher-level skill bundles that compose multiple accepts into meaningful actions.
* Skills are the primary discovery surface for AI agents (R18).
* Each key is a skill name, value describes what it does and which ops it composes.
*
* See: ARCHITECTURE-SKILLS.md §2
*/
static skills: Record<string, {
description: string;
ops?: string[];
examples?: Array<{
input: Record<string, unknown>;
expected?: string;
}>;
}>;
/**
* Optional state schema for named type matching.
*/
static stateSchema?: Record<string, unknown>;
/**
* Persistence scope for this actor's state.
* Determines the KV key prefix used by NatsStateTap and the cascade
* resolution order used by BlackboardActor on read.
*
* Scopes (most specific least specific):
* session per-session (default). Survives F5 reload.
* user per-user. Survives new sessions. Use for preferences.
* page per-page/tab. Scoped to a single browser page.
* workspace per-workspace. Shared across workspace members.
* org per-organization. Company defaults.
* root per-root. Root-wide shared state.
* global cross-root. Federation-wide.
*/
static stateScope: 'session' | 'user' | 'page' | 'workspace' | 'org' | 'root' | 'realm' | 'global';
/**
* Whether this actor's state should be persisted to KV.
* Set to false for ephemeral actors (loading spinners, animations).
*/
static persistState: boolean;
/** The injected context */
protected _context?: IMatrixContext;
/** Optional fact-plane emit capability token. */
protected _factCapability: ICapabilityToken | null;
/** Subscription cleanup bag */
protected _subs: SubscriptionBag;
/** Replica mounts for multi-bind state replication */
protected _replicaMounts: string[];
/** Lamport logical clock for causal message ordering */
protected _lamportClock: LamportClock;
/** Actor mailbox for exclusive message receiving */
protected _mailbox?: IMailbox;
/** Composed actor behavior (mailbox + dispatch) */
protected _actor?: ActorBehavior;
/** LISP dispatch strategy (if using LISP handlers) */
private _lispStrategy?;
/** Shell metadata injected by MatrixActorHtmlElement for introspect */
_shellInfo?: {
shellTag: string;
moduleUrl?: string;
description?: string;
};
/** Shell element reference set by MatrixActorHtmlElement during _receiveContext.
* Used by $source-code to return the shell's source (LLM-written code) instead of
* the inner actor's source (minified DynamicActor). */
_shellElement?: unknown;
/** Whether the actor has been initialized */
private _initialized;
/** Whether the actor has been bootstrapped */
private _bootstrapped;
/** Whether the actor has been disposed */
private _disposed;
/** Callback for shell/adapter to receive state change notifications (FP-0.5). */
_shellUpdateCallback: (() => void) | null;
/** Reference to the shell element's ShadowRoot, so schema handlers can access DOM (e.g. iframes). */
_shellRoot: ShadowRoot | null;
/** Convenience getter for schema handler code: `this.shell.querySelector(...)` */
get shell(): ShadowRoot | null;
/** Promise that resolves when actor is ready */
readonly ready: Promise<void>;
private _resolveReady;
/** Actor name/id */
private _name;
/** Child Manager — NOT allocated until first use */
protected _children?: ChildManager;
/** Supervisor behavior — NOT allocated until first use */
protected _supervisor?: SupervisorBehavior;
/** Security realm — NOT allocated until first use */
protected _securityRealm?: SecurityRealm;
/** Child event binding tracking */
private _childBindings;
/** Monitor references for tracked children */
private _monitors;
/** Dynamic child metadata */
private _dynamicMetadata;
/** Observable state store */
protected _state: Record<string, unknown>;
/** Optional CQRS-style command history for inspector tooling. */
protected _commandHistory: Array<{
op: string;
ts: number;
from?: string;
}>;
/** Optional CQRS-style event history for inspector tooling. */
protected _eventHistory: Array<{
op: string;
ts: number;
}>;
protected _historyLimit: number;
constructor();
get name(): string;
get mount(): string;
get context(): IMatrixContext | undefined;
get isInitialized(): boolean;
get isReady(): boolean;
get isDisposed(): boolean;
get mailbox(): IMailbox | undefined;
get lamportClock(): LamportClock;
get replicaMounts(): readonly string[];
get state(): Readonly<Record<string, unknown>>;
/** Get the child manager (undefined if no children have been added) */
protected get children(): ChildManager | undefined;
/**
* Initialize the actor with a context.
*
* @param context - The matrix context
* @param name - Optional actor name
*/
initialize(context: IMatrixContext, name?: string): Promise<void>;
/**
* Lazy-initialize child management infrastructure.
* Called on first addChild() or $join zero overhead if never used.
*/
protected _ensureChildManagement(): void;
/**
* Handle $join handshake from child shells.
* Lazy-initializes child management on first join.
*/
on$join(payload: {
id: string;
mount: string;
componentClass?: string;
props?: unknown;
}): void;
/**
* Handle $leave when a child disconnects voluntarily.
*/
on$leave(_payload: {
id: string;
mount: string;
}): void;
/**
* Handle $ping liveness check.
*/
on$ping(_payload?: unknown): {
status: string;
mount: string;
};
/**
* Handle $join_ack from parent (when this actor is a child).
* Stub in Phase 20a-1 full implementation in MatrixActorHtmlElement.
*/
on$joinAck(_payload: unknown): void;
/**
* Handle $reply for request/reply pattern.
* Stub in Phase 20a-1 full implementation in FP-20b.
*/
on$reply(_payload: unknown): void;
/**
* Alias for protocol-style $introspect operation.
* Supports payload shape: { depth: 'full' }.
*/
on$introspect(payload?: {
depth?: 'basic' | 'full';
}): ReturnType<MatrixActor['onIntrospect']>;
/**
* $source-code Return this actor's source code via constructor.toString().
*/
on$sourceCode(): {
source: string;
mount: string;
tag?: string;
};
/**
* System-plane state snapshot for Actor Inspector.
*/
on$getState(): Record<string, unknown>;
/**
* System-plane history snapshot for CQRS log viewers.
*/
on$getHistory(): {
commands: Array<{
op: string;
ts: number;
from?: string;
}>;
events: Array<{
op: string;
ts: number;
}>;
};
/**
* System-plane handler for activity frames pushed via `activityTo`.
* Default is no-op. Browser elements and subclasses override for UI feedback.
*/
on$activity(_payload: unknown): void;
/**
* G11: Query effective preferences for this actor's mount.
* Delegates to system.preferences service via RequestReply.
*/
on$preferences(payload?: {
key?: string;
}): Promise<Record<string, unknown>>;
/**
* G11: Query effective config for this actor's mount.
* Delegates to system.config service via RequestReply.
*/
on$config(payload?: {
key?: string;
}): Promise<Record<string, unknown>>;
/**
* Factory: create a child actor from a code string.
*
* Headless (daemon) path: evals the code into a class, uses addChild()
* to create, initialize, and register the child in ChildManager.
*
* MatrixActorHtmlElement overrides this with DOM concerns (custom element
* registration, shadow DOM insertion, versioned tag names).
*/
onFactoryCreateFromCode(payload: {
name: string;
code: string;
}): Promise<Record<string, unknown>>;
/**
* factory.load Load a saved component from ComponentStoreActor.
*
* Invokes component.get-source on system.component-store,
* then delegates to factory.create-from-code.
*/
onFactoryLoad(payload: {
name: string;
}): Promise<Record<string, unknown>>;
/**
* component.save Extract source from a running child and persist it.
*
* Finds the child by mount segment, invokes $source-code on it,
* then sends component.persist to daemon.system.component-store.
*/
onComponentSave(payload: {
mount: string;
name?: string;
surface?: 'dom' | 'headless';
description?: string;
}): Promise<Record<string, unknown>>;
/**
* Framework op handler: $prompt (FP-0.4).
*
* Makes any MatrixActor promptable forwards the prompt to the daemon.
* MatrixActorHtmlElement overrides this with a richer implementation
* (snapshot tree, session management, blocking mode).
*
* For dynamically created components this is the minimum needed to
* receive and forward a prompt to the agent system.
*/
on$prompt(payload: Record<string, unknown>): Promise<Record<string, unknown>>;
/**
* Placeholder protocol hook for dynamic viewer/control creation.
* Concrete runtimes may override this behavior.
*/
on$createControl(payload?: unknown): {
ok: false;
op: '$createControl';
message: string;
requested?: unknown;
};
/**
* Introspect handler returns actor capabilities.
* Always available on every actor.
*/
protected onIntrospect(payload?: {
depth?: 'basic' | 'full';
}): {
mount: string;
componentClass: string;
accepts: string[];
emits: string[];
tracks: string[];
acceptsSchema: Record<string, unknown>;
emitsSchema: Record<string, unknown>;
tokens: string[];
tokensSchema?: Record<string, {
default: string;
description: string;
}>;
children?: Array<ICompositeIntrospectChild>;
shellTag?: string;
moduleUrl?: string;
description?: string;
outputSchemas?: Record<string, Record<string, unknown>>;
errorSpecs?: Record<string, Array<{
code: string;
description: string;
}>>;
examples?: Record<string, Array<{
input: Record<string, unknown>;
output?: Record<string, unknown>;
}>>;
testVectors?: unknown[];
documentation?: string;
intentFilters?: unknown[];
skills?: string[];
skillsSchema?: Record<string, unknown>;
stateSchema?: Record<string, unknown>;
stateScope?: string;
persistState?: boolean;
};
/**
* Update observable state and emit change events.
* Emits $stateChanged with full state, and $propertyChanged per changed key.
*/
setState(patch: Partial<Record<string, unknown>>): void;
/**
* Emit an event from this actor.
*
* Publishes to:
* 1. $events topic (MxEnvelope) new canonical path
* 2. Per-op emits topic (legacy enriched payload) backward compat until 20b-6
* 3. Replica mounts (both formats)
*/
protected emit(event: string, payload?: unknown): void;
/**
* Send a command to another actor by mount path (internal).
*
* Publishes MxEnvelope to target's $inbox.
*/
protected sendTo(targetMount: string, command: string, payload?: unknown): void;
/**
* Send a command with three calling conventions:
*
* 1. Direct child: sendCommand('tree', 'ExpandDir', { path: '/src' })
* 2. Relative path: sendCommand('./split.tree', 'ExpandDir', { path: '/src' })
* 3. Tracked domain (2-arg): sendCommand('ExpandDir', { path: '/src' })
* only supported in MatrixActorHtmlElement (view tracked domain actor)
*
* Publishes MxEnvelope to target's $inbox.
*/
sendCommand(toOrCmd: string, cmdOrData?: string | unknown, data?: unknown): void;
/**
* Cross-root RPC. Delegates to FederationPeer via transport adapter.
*/
invoke(rootPath: string, op: string, data?: unknown): Promise<unknown>;
/**
* Publish a fact into the shared blackboard plane.
*
* Requires an emit-capable token in `_factCapability`.
* Throws synchronously when capability is missing.
*/
private _isFactDomainLiteralSegment;
private _isFactDomainVariableSegment;
private _splitFactDomainSegments;
private _matchesFactCapabilitySubject;
private _canEmitFactDomain;
publishFact(domain: string, data: Record<string, unknown>): Promise<number>;
/**
* Read the latest fact for a domain from the shared fact plane.
*/
readFact(domain: string): Promise<{
data: unknown;
revision: number;
} | null>;
/**
* Subscribe to fact updates.
*
* Uses context-native subscription when available; otherwise falls back to
* polling via readFact() to support transport layers without push hooks.
*/
subscribeFact(domain: string, handler: (data: unknown) => void): () => void;
/**
* Add a child actor.
* Lazy-initializes ChildManager, SupervisorBehavior, SecurityRealm on first call.
*
* @param id - Unique child identifier
* @param ActorClass - Actor class to instantiate
* @param options - Slot, position, props, bindings
* @returns Mount path of the created child
*/
addChild<T extends MatrixActor>(id: string, ActorClass: new () => T, options?: IAddChildOptions): Promise<string>;
/**
* Remove a child actor by id.
*/
removeChild(id: string): Promise<void>;
/**
* Get mount path of a child by id.
*/
getChildMount(id: string): string | undefined;
/**
* Get all child mount paths in a slot, in order.
*/
getChildMountsInSlot(slot?: string): string[];
/**
* Get child ids in order.
*/
childIds(slot?: string): string[];
/**
* Alias for childIds().
*/
getChildIds(slot?: string): string[];
/**
* Move a child to a new position within its slot.
*/
moveChild(id: string, position: Position): Promise<void>;
/**
* Send a command to a child by id (internal convenience).
*/
protected sendToChild(childId: string, command: string, payload?: unknown): void;
/**
* Configure supervision policy.
*/
setSupervisionPolicy(policy: ISupervisionPolicy): void;
/**
* Configure security policy.
*/
setSecurityPolicy(policy: ISupervisorSecurityPolicy): void;
/**
* Configure both supervision and security policies.
*/
setSecurityRealm(supervisionPolicy: ISupervisionPolicy, securityPolicy: ISupervisorSecurityPolicy): void;
/**
* Handle an exit signal from a child actor.
* Applies supervision policy: restart, stop, escalate, or ignore.
*/
handleChildExit(signal: IExitSignal): Promise<void>;
/**
* Wire bindings from the template to ANY descendant's events.
* Used by the DSL compiler to connect template-level event bindings
* to handler methods on the template author's actor.
*
* Unlike _subscribeToChildEvents() (direct children, handler cascade),
* deep bindings use EXPLICIT handler mapping only no cascade.
*/
_wireDeepBindings(bindings: DeepBindingDescriptor[]): void;
/**
* Protocol handler for addDynamicChild command.
*/
onAddDynamicChild(params: IDynamicComponentDefinition): Promise<{
mount: string;
adapterId: string;
shellTag: string;
}>;
/**
* Protocol handler for listDynamicChildren command.
*/
onListDynamicChildren(): Array<{
id: string;
mount: string;
shellTag?: string;
codeHash?: string;
createdAt?: number;
}>;
/**
* Protocol handler for removeDynamicChild command.
*/
onRemoveDynamicChild(params: {
id: string;
}): Promise<{
success: boolean;
}>;
/**
* Add a dynamic child from code at runtime.
*/
addDynamicChild(params: IDynamicComponentDefinition): Promise<{
mount: string;
adapterId: string;
shellTag: string;
}>;
/**
* List all dynamic children.
*/
listDynamicChildren(): Array<{
id: string;
mount: string;
shellTag?: string;
codeHash?: string;
createdAt?: number;
}>;
/**
* Serialize the component tree to a descriptor.
*/
toDescriptor(): IComponentTreeDescriptor;
/**
* Enable actor mode serialized message processing via mailbox.
*/
protected enableActorMode(): void;
/**
* Add a replica binding for state replication.
*/
addReplicaBind(remoteMount: string): Promise<void>;
/**
* Remove a replica binding.
*/
removeReplicaBind(remoteMount: string): void;
/** Override for initialization logic. */
protected onBootstrap(): Promise<void>;
/** Override for cleanup logic. */
protected onDispose(): void;
/** Hook called when context changes (remount). */
protected _onContextChanged(): Promise<void>;
/**
* Dispose the actor and clean up all resources.
*/
dispose(): void;
private _bootstrap;
/**
* Subscribe to inbound messages via $inbox topic.
*
* Builds a dispatch table from static accepts + system commands.
* Routes all inbound MxEnvelopes through a unified dispatch function.
*
* Single subscription: mount/$inbox receives MxEnvelope format.
*/
private _subscribeToInbox;
/**
* Subscribe to events from a child actor.
* Uses ComponentClass.emits to know what topics to subscribe to.
*
* Handler resolution order:
* 1. Declarative binding from options.bindings (highest priority)
* 2. Specific: onEventFromChildId
* 3. Generic: onEvent
*/
private _subscribeToChildEvents;
/**
* Subscribe to child events using stored metadata (for remount).
*/
private _subscribeToChildEventsFromMeta;
/**
* Subscribe to exit signals from a child actor.
*/
private _monitorChild;
/**
* Remount all children with new derived contexts.
*/
protected _remountChildren(): Promise<void>;
private _unbindAll;
private _toSerializable;
private _pushBoundedHistory;
private _attachLamportClock;
private _receiveLamportClock;
private _compileDynamicComponent;
private _hashCode;
}
//# sourceMappingURL=MatrixActor.d.ts.map

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

87
packages/core/src/core/MockRuntime.d.ts vendored Normal file
View File

@ -0,0 +1,87 @@
import type { IMatrixContext } from '../engine/core/IMatrixContext';
import type { IMatrixRuntime } from '../engine/core/IMatrixRuntime';
import type { IComponentDescriptor } from '../engine/core/IComponentDescriptor';
import type { IChildDescriptor } from '../engine/core/IChildDescriptor';
import type { IExternalAdapterOptions } from './composite/IExternalAdapterOptions';
import { MatrixActor } from './MatrixActor';
import type { IMailbox } from '../engine/mailbox/IMailbox';
/**
* Component tracking info (matches MatrixRuntime structure).
*/
interface ComponentInfo {
id: string;
component: MatrixActor;
mount: string;
}
/**
* MockRuntime - Minimal runtime for testing.
*
* Implements IMatrixRuntime with minimal functionality for unit tests
* that don't need full runtime features.
*
* Uses same internal structure as MatrixRuntime (_components Map)
* so MatrixActor can access tracked components.
*
* Usage in tests:
* ```typescript
* const runtime = new MockRuntime();
* const context = MatrixContext.create('test', transport, runtime);
* ```
*/
export declare class MockRuntime implements IMatrixRuntime {
readonly type: 'headless' | 'browser';
readonly supportsDom: boolean;
readonly _components: Map<string, ComponentInfo>;
private readonly _mailboxes;
createChild(parentContext: IMatrixContext, id: string, descriptor: IComponentDescriptor): Promise<string>;
parseTemplate(_html: string): Promise<IChildDescriptor[]>;
/**
* Create an exclusive mailbox for a component (IMatrixRuntime interface).
*/
createMailbox(mount: string): IMailbox;
/**
* Get a component by mount path.
* @deprecated This is a CHEAT - actors should use transport for communication.
* Use RequestReply.execute() or sendTo() instead of getting direct references.
* This method exists only for framework/testing infrastructure.
*/
getComponent<T extends MatrixActor>(mount: string): T | undefined;
/**
* Check if a component exists by mount path.
* This is the PROPER way to check existence without getting a reference.
*/
hasComponent(mount: string): boolean;
/**
* Remove and dispose a component.
*/
remove(mount: string): Promise<void>;
/**
* Register an external adapter (no-op for mock).
*
* Mock runtime doesn't support DOM or visual adapters.
* Returns undefined to indicate no adapter was created.
*/
registerExternalAdapter(_id: string, _ComponentClass: new () => MatrixActor, _options?: IExternalAdapterOptions): string | undefined;
/**
* Unregister an external adapter (no-op for mock).
*/
unregisterExternalAdapter(_tag: string): void;
/**
* Check if an external adapter is registered (always false for mock).
*/
hasExternalAdapter(_tag: string): boolean;
/**
* Check if dynamic component compilation is allowed.
*
* MockRuntime is for testing, so always allows dynamic compilation.
*
* @returns true (always for mock/testing)
*/
isDynamicCompilationAllowed(): boolean;
/**
* Clear all tracked components.
*/
clear(): void;
}
export {};
//# sourceMappingURL=MockRuntime.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"MockRuntime.d.ts","sourceRoot":"","sources":["MockRuntime.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAChF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AACnF,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,OAAO,KAAK,EAAE,QAAQ,EAAiB,MAAM,4BAA4B,CAAC;AAE1E;;GAEG;AACH,UAAU,aAAa;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,WAAW,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACf;AA+DD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,WAAY,YAAW,cAAc;IAChD,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,SAAS,CAAc;IACnD,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAS;IAGtC,QAAQ,CAAC,WAAW,6BAAoC;IAGxD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAEvD,WAAW,CACf,aAAa,EAAE,cAAc,EAC7B,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,oBAAoB,GAC/B,OAAO,CAAC,MAAM,CAAC;IA0BZ,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAI/D;;OAEG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ;IAUtC;;;;;OAKG;IACH,YAAY,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAMjE;;;OAGG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIpC;;OAEG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB1C;;;;;OAKG;IACH,uBAAuB,CACrB,GAAG,EAAE,MAAM,EACX,eAAe,EAAE,UAAU,WAAW,EACtC,QAAQ,CAAC,EAAE,uBAAuB,GACjC,MAAM,GAAG,SAAS;IAIrB;;OAEG;IACH,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI7C;;OAEG;IACH,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAQzC;;;;;;OAMG;IACH,2BAA2B,IAAI,OAAO;IAOtC;;OAEG;IACH,KAAK,IAAI,IAAI;CAGd"}

View File

@ -0,0 +1,249 @@
// src/core/MockRuntime.ts
import type { IMatrixContext } from '../engine/core/IMatrixContext';
import type { IMatrixRuntime } from '../engine/core/IMatrixRuntime';
import type { IComponentDescriptor } from '../engine/core/IComponentDescriptor';
import type { IChildDescriptor } from '../engine/core/IChildDescriptor';
import type { IExternalAdapterOptions } from './composite/IExternalAdapterOptions';
import { MatrixActor } from './MatrixActor';
import { CheatDetector } from './CheatDetector';
import type { IMailbox, IActorMessage } from '../engine/mailbox/IMailbox';
/**
* Component tracking info (matches MatrixRuntime structure).
*/
interface ComponentInfo {
id: string;
component: MatrixActor;
mount: string;
}
/**
* Simple mock mailbox for testing.
*/
class MockMailbox implements IMailbox {
readonly address: string;
private _queue: IActorMessage[] = [];
private _claimed = false;
private _closed = false;
constructor(address: string) {
this.address = address;
}
get depth(): number { return this._queue.length; }
get closed(): boolean { return this._closed; }
get claimed(): boolean { return this._claimed; }
claim(): void {
if (this._closed) throw new Error('Mailbox is closed');
if (this._claimed) throw new Error('Mailbox already claimed');
this._claimed = true;
}
send(message: IActorMessage): void {
if (this._closed) return;
this._queue.push(message);
}
async receive(): Promise<IActorMessage> {
if (!this._claimed) throw new Error('Mailbox not claimed');
if (this._closed) throw new Error('Mailbox closed');
// Simple blocking behavior for mock
while (this._queue.length === 0 && !this._closed) {
await new Promise(resolve => setTimeout(resolve, 10));
}
if (this._closed) throw new Error('Mailbox closed');
return this._queue.shift()!;
}
[Symbol.asyncIterator](): AsyncIterableIterator<IActorMessage> {
const self = this;
return {
async next(): Promise<IteratorResult<IActorMessage>> {
if (self._closed) return { done: true, value: undefined };
try {
const value = await self.receive();
return { done: false, value };
} catch {
return { done: true, value: undefined };
}
},
[Symbol.asyncIterator]() { return this; },
};
}
close(): void {
this._closed = true;
this._queue = [];
}
}
/**
* MockRuntime - Minimal runtime for testing.
*
* Implements IMatrixRuntime with minimal functionality for unit tests
* that don't need full runtime features.
*
* Uses same internal structure as MatrixRuntime (_components Map)
* so MatrixActor can access tracked components.
*
* Usage in tests:
* ```typescript
* const runtime = new MockRuntime();
* const context = MatrixContext.create('test', transport, runtime);
* ```
*/
export class MockRuntime implements IMatrixRuntime {
readonly type: 'headless' | 'browser' = 'headless';
readonly supportsDom: boolean = false;
// Track created components - uses _components to match MatrixRuntime
readonly _components = new Map<string, ComponentInfo>();
// Track mailboxes for actor communication
private readonly _mailboxes = new Map<string, MockMailbox>();
async createChild(
parentContext: IMatrixContext,
id: string,
descriptor: IComponentDescriptor
): Promise<string> {
const { componentClass } = descriptor;
// NOTE: props are NOT directly assigned to component!
// Props flow through lifecycle.childAdded event → shell._initialProps → handler
// This maintains the architecture where all data flows through transport.
// Create the component
const component = new componentClass() as MatrixActor;
// Derive context
const childContext = parentContext.derive(id);
// Initialize
await component.initialize(childContext, id);
// Track with same structure as MatrixRuntime
const info: ComponentInfo = {
id,
component,
mount: childContext.mount,
};
this._components.set(childContext.mount, info);
return childContext.mount;
}
async parseTemplate(_html: string): Promise<IChildDescriptor[]> {
return [];
}
/**
* Create an exclusive mailbox for a component (IMatrixRuntime interface).
*/
createMailbox(mount: string): IMailbox {
const address = `${mount}.$inbox`;
if (this._mailboxes.has(address)) {
return this._mailboxes.get(address)!;
}
const mailbox = new MockMailbox(address);
this._mailboxes.set(address, mailbox);
return mailbox;
}
/**
* Get a component by mount path.
* @deprecated This is a CHEAT - actors should use transport for communication.
* Use RequestReply.execute() or sendTo() instead of getting direct references.
* This method exists only for framework/testing infrastructure.
*/
getComponent<T extends MatrixActor>(mount: string): T | undefined {
// Report cheat if enabled
CheatDetector.report('P1', `MockRuntime.getComponent accessing component directly: ${mount}`);
return this._components.get(mount)?.component as T | undefined;
}
/**
* Check if a component exists by mount path.
* This is the PROPER way to check existence without getting a reference.
*/
hasComponent(mount: string): boolean {
return this._components.has(mount);
}
/**
* Remove and dispose a component.
*/
async remove(mount: string): Promise<void> {
const info = this._components.get(mount);
if (!info) return;
try {
info.component.dispose();
} catch (err) {
// eslint-disable-next-line no-console -- mock runtime cleanup failures need direct test diagnostics
console.error(`[MockRuntime] Error disposing component:`, err);
}
this._components.delete(mount);
}
// ═══════════════════════════════════════════════════════════════════════════
// EXTERNAL ADAPTER PATTERN (No-op for mock)
// Mock runtime doesn't support external visual adapters.
// See: Vision/33-EXTERNAL-ADAPTER-PATTERN.md
// ═══════════════════════════════════════════════════════════════════════════
/**
* Register an external adapter (no-op for mock).
*
* Mock runtime doesn't support DOM or visual adapters.
* Returns undefined to indicate no adapter was created.
*/
registerExternalAdapter(
_id: string,
_ComponentClass: new () => MatrixActor,
_options?: IExternalAdapterOptions
): string | undefined {
return undefined;
}
/**
* Unregister an external adapter (no-op for mock).
*/
unregisterExternalAdapter(_tag: string): void {
// No-op: mock runtime doesn't support external adapters
}
/**
* Check if an external adapter is registered (always false for mock).
*/
hasExternalAdapter(_tag: string): boolean {
return false;
}
// ═══════════════════════════════════════════════════════════════════════════
// SECURITY POLICY
// ═══════════════════════════════════════════════════════════════════════════
/**
* Check if dynamic component compilation is allowed.
*
* MockRuntime is for testing, so always allows dynamic compilation.
*
* @returns true (always for mock/testing)
*/
isDynamicCompilationAllowed(): boolean {
return true;
}
// P1: _getChildComponentInternal DELETED
// Use transport-based communication instead
/**
* Clear all tracked components.
*/
clear(): void {
this._components.clear();
}
}

View File

@ -0,0 +1,226 @@
/**
* PackageRootActor base class for package root actors.
*
* Like a NestJS Module: owns a DI scope, declares children (providers),
* manages lifecycle, provides shared resources (storage, config).
*
* The root actor mounts at the package namespace (e.g., system.observability).
* Children mount under it (e.g., system.observability.logging). Children
* inherit the root's context via cascade if root registers a store,
* children find it automatically.
*
* See: ARCHITECTURE/ARCHITECTURE-PACKAGE-ACTORS.md
*/
import { MatrixActor, type ActorStatusSnapshotV1 } from './MatrixActor';
// ── Types ────────────────────────────────────────────────────────────────
export interface PackageManifestProps {
/** Components to spawn as children. */
components?: Array<{
type: string;
mount: string;
props?: Record<string, unknown>;
}>;
/** Storage configuration from manifest. */
storage?: {
sqlite?: {
domain: string;
schema?: string;
};
};
/** Package metadata. */
packageName?: string;
packageVersion?: string;
}
// ── Base Class ───────────────────────────────────────────────────────────
export class PackageRootActor extends MatrixActor {
/** Override in subclass: npm package name. */
static package = '';
/** Override in subclass: bus namespace (root mount path). */
static namespace = '';
static override accepts = {
'$children': { description: 'List child actors in this package' },
'$restart': { description: 'Restart all children' },
'$status': { description: 'Package health + storage stats' },
};
protected _manifestProps: PackageManifestProps = {};
private _childMounts: string[] = [];
// ── Lifecycle ────────────────────────────────────────────────────────
override async onBootstrap(): Promise<void> {
// Props are passed by PackageLoader from the manifest
this._manifestProps = ((this as unknown as { _props?: PackageManifestProps })._props) ?? {};
// 1. Initialize package-scoped storage (if declared)
await this._initStorage();
// 2. Spawn children declared in manifest
await this._spawnChildren();
}
override onDispose(): void {
// Children are auto-disposed by the actor tree (ChildManager)
this._childMounts = [];
}
// ── Storage ──────────────────────────────────────────────────────────
/**
* Create package-scoped storage and register on this context.
* Children find it via cascade they call context.getService(RECORD_STORE_KEY)
* and walk up to THIS context, finding the package's store (not the global one).
*/
private async _initStorage(): Promise<void> {
const storage = this._manifestProps.storage?.sqlite;
if (!storage) return;
// Ask system.sqlite to ensure the domain's database exists
try {
const { RequestReply } = await import('../engine/remoting/RequestReply');
await RequestReply.execute(
this._context!, 'system.sqlite', 'sqlite.init-schema',
{
domain: storage.domain,
schema: storage.schema || '',
packageName: this._manifestProps.packageName,
},
{ timeoutMs: 10000 },
);
} catch {
// system.sqlite may not be available yet — storage init deferred
}
// Create in-process per-package store via the registered provider seam
// (RECORD_STORE_PROVIDER_KEY) and register the result on THIS context
// so children inherit via cascade. The runtime owns provider registration;
// core never reaches into a sibling implementation package.
try {
const { RECORD_STORE_PROVIDER_KEY } = await import('./IRecordStoreProvider');
const { RECORD_STORE_KEY } = await import('../framework/InteractionRecord');
const provider = this._context!.getService(RECORD_STORE_PROVIDER_KEY) as
| { provide(matrixHome: string, domain: string): Promise<unknown | null> }
| undefined;
if (!provider || typeof provider.provide !== 'function') {
// No provider registered — storage deferred (matches pre-seam behavior
// where missing sqlite was swallowed by the surrounding try/catch).
return;
}
const { join } = await import('node:path');
const matrixHome = process.env.MATRIX_HOME || join(process.cwd(), '.matrix');
const store = await provider.provide(matrixHome, storage.domain);
if (store == null) {
// Provider declined (e.g., no SQLite backend on this host) — defer.
return;
}
this._context!.setService(RECORD_STORE_KEY, store);
} catch {
// Provider lookup or invocation failed — storage deferred (same as
// the pre-seam swallow on dynamic-import failure).
}
}
// ── Child Spawning ───────────────────────────────────────────────────
/**
* Spawn children declared in the manifest. Each child mounts as
* namespace.childMount (e.g., system.observability.logging).
*/
private async _spawnChildren(): Promise<void> {
const components = this._manifestProps.components ?? [];
const registry = this._context?.runtime as unknown as {
_componentRegistry?: { resolve(type: string): (new () => MatrixActor) | undefined };
};
for (const component of components) {
try {
// Resolve the component class from the registry
const ActorClass = registry?._componentRegistry?.resolve(component.type);
if (!ActorClass) {
continue; // Type not registered — skip silently
}
// Mount as child: component.mount is relative (e.g., "logging")
const childMount = await this.addChild(
component.mount,
ActorClass as new () => MatrixActor,
{ props: component.props },
);
this._childMounts.push(childMount);
} catch {
// Child failed to mount — continue with others
}
}
}
// ── Handlers ─────────────────────────────────────────────────────────
/** $children — list package contents (like Assembly.GetTypes()). */
on$children() {
const ctor = this.constructor as typeof PackageRootActor;
return {
ok: true,
package: ctor.package,
namespace: ctor.namespace,
children: this._childMounts,
count: this._childMounts.length,
};
}
/** $restart — dispose all children and re-spawn from manifest. */
async on$restart() {
// Dispose existing children
if (this._children) {
for (const mount of [...this._childMounts]) {
try {
const childId = mount.split('.').pop() || mount;
this._children.removeChild(childId);
} catch { /* best effort */ }
}
}
this._childMounts = [];
// Re-spawn
await this._spawnChildren();
return {
ok: true,
restarted: true,
children: this._childMounts,
count: this._childMounts.length,
};
}
/** $status — package health + storage stats. */
on$status(): ActorStatusSnapshotV1 & {
ok: true;
package: string;
namespace: string;
version: string | undefined;
children: number;
storage: string | null;
} {
const ctor = this.constructor as typeof PackageRootActor;
const base = super.on$status();
return {
...base,
ok: true,
kind: 'package-root',
package: ctor.package,
namespace: ctor.namespace,
version: this._manifestProps.packageVersion,
children: this._childMounts.length,
storage: this._manifestProps.storage?.sqlite?.domain ?? null,
};
}
}

View File

@ -0,0 +1,285 @@
/**
* RootActor the runtime's public face in the actor tree.
*
* Sits at mount "" (empty string = root). Makes the runtime addressable,
* introspectable, and uniform with every other actor in the hierarchy.
*
* Unix analog: init (PID 1) first process, supervises all top-level services.
*
* This actor does NOT replace MatrixRuntime it wraps it, providing the
* actor-model interface on top of the runtime's internal machinery.
*/
import { MatrixActor, type ActorStatusSnapshotV1 } from './MatrixActor.js';
import type { IComponentRuntimeMetadata } from './composite/IComponentRuntimeMetadata.js';
type RuntimeMetaContext = {
realm?: string;
};
type RuntimeRegisteredDeploymentInstance = {
id: string;
packageName: string;
mount: string;
rootKind: string;
class: string;
state: string;
packageRevision?: string;
componentType?: string;
};
type RuntimeRegisteredPackageRequirement = {
packageName: string;
revision?: string;
sourceOverride?: string;
};
type RuntimeRegisteredSnapshot = {
componentTags: string[];
supervisedRoots: string[];
requiredPackages?: RuntimeRegisteredPackageRequirement[];
deploymentInstances?: RuntimeRegisteredDeploymentInstance[];
};
export class RootActor extends MatrixActor {
static accepts = {
'$children': { description: 'List running child actors under this runtime' },
'$status': { description: 'Get runtime health and inventory summary' },
'runtime.status': { description: 'Get full runtime introspection data', options: 'object?' },
'runtime.services': { description: 'List registered service endpoints', options: 'object?' },
'runtime.actors': { description: 'List all mounted actors with status', options: 'object?' },
'runtime.registered': {
description: 'List runtime-registered component tags, supervised roots, required packages, and deployment instances',
options: 'object?',
},
'runtime.start': { mount: 'string' },
'runtime.stop': { mount: 'string' },
'runtime.reload': { mount: 'string' },
'runtime.remove': { mount: 'string' },
'runtime.restart': { mount: 'string' },
};
static description = 'Root actor — the runtime itself. Mount: "" (empty string).';
private _startTime = Date.now();
/**
* Snapshot of all component mounts known to the runtime.
* Set externally by MatrixRuntime after construction.
*/
_componentSnapshot: () => Array<{ id: string; mount: string; metadata?: IComponentRuntimeMetadata }> = () => [];
/**
* Service registry snapshot returns registered service keys.
* Set externally by MatrixRuntime.
*/
_serviceSnapshot: () => Record<string, string> = () => ({});
/**
* Runtime metadata (realm, version, type).
* Set externally by MatrixRuntime.
*/
_runtimeMeta: () => Record<string, unknown> = () => ({});
/**
* Runtime registration snapshot registered component tags and supervised roots.
* Set externally by MatrixRuntime.
*/
_registeredSnapshot: () => RuntimeRegisteredSnapshot = () => ({
componentTags: [],
supervisedRoots: [],
});
/**
* Root component lifecycle hooks. Set externally by MatrixRuntime.
*/
_startComponent: (mount: string) => Promise<boolean> = async () => {
throw new Error('Runtime start hook not configured');
};
_stopComponent: (mount: string) => Promise<boolean> = async () => {
throw new Error('Runtime stop hook not configured');
};
_reloadComponent: (mount: string) => Promise<void> = async () => {
throw new Error('Runtime reload hook not configured');
};
_removeComponent: (mount: string) => Promise<void> = async () => {
throw new Error('Runtime remove hook not configured');
};
_restartComponent: (mount: string) => Promise<void> = async () => {
throw new Error('Runtime restart hook not configured');
};
// ── $introspect override ─────────────────────────────────────────
protected override onIntrospect(payload?: { depth?: 'basic' | 'full' }) {
const base = super.onIntrospect(payload);
const meta = this._runtimeMeta();
const components = this._componentSnapshot();
return {
...base,
type: 'runtime',
realm: meta.realm ?? (this._context as RuntimeMetaContext | undefined)?.realm ?? 'unknown',
version: meta.version ?? '0.0.0',
runtimeType: meta.type ?? 'headless',
running: meta.running ?? true,
uptime: Date.now() - this._startTime,
actors: components.length,
registeredComponents: this._registeredSnapshot().componentTags.length,
supervisedRoots: this._registeredSnapshot().supervisedRoots,
services: this._serviceSnapshot(),
// Override children with actual component list
children: components.map(c => ({
id: c.id,
mount: c.mount,
slot: '',
componentClass: 'unknown',
accepts: [],
emits: [],
tracks: [],
metadata: c.metadata,
})),
};
}
// ── $ping override ───────────────────────────────────────────────
override on$ping() {
return { status: 'ok', mount: '', type: 'runtime' };
}
// ── Domain ops ───────────────────────────────────────────────────
onRuntimeStatus() {
return this.onIntrospect({ depth: 'full' });
}
onRuntimeServices() {
return this._serviceSnapshot();
}
onRuntimeActors() {
return this._componentSnapshot();
}
onRuntimeRegistered() {
return this._registeredSnapshot();
}
on$children() {
const children = this._componentSnapshot().map((entry) => entry.mount);
return {
ok: true,
children,
count: children.length,
};
}
on$status(): ActorStatusSnapshotV1 & {
ok: true;
type: 'runtime';
runtimeType: unknown;
realm: unknown;
running: unknown;
actors: number;
registeredComponents: number;
supervisedRoots: string[];
services: string[];
} {
const meta = this._runtimeMeta();
const registered = this._registeredSnapshot();
const services = this._serviceSnapshot();
const actors = this._componentSnapshot();
const base = super.on$status();
// Slice 1b (control-actor-nav): this is the runtime root actor's own
// $status payload field, NOT a registry runtime-directory claim. The
// runtime directory authority is `system.runtimes` (RuntimeManagerActor);
// consumers must NOT use this kind field for runtime discovery.
return {
...base,
ok: true,
kind: 'runtime',
type: 'runtime',
runtimeType: meta.type ?? 'headless',
realm: meta.realm ?? (this._context as RuntimeMetaContext | undefined)?.realm ?? 'unknown',
running: meta.running ?? true,
uptime: Date.now() - this._startTime,
actors: actors.length,
registeredComponents: registered.componentTags.length,
supervisedRoots: registered.supervisedRoots,
services: Object.keys(services),
};
}
async onRuntimeRemove(payload: { mount?: string }) {
const mount = typeof payload?.mount === 'string' ? payload.mount.trim() : '';
if (!mount) {
throw new Error('runtime.remove requires a non-empty mount');
}
await this._removeComponent(mount);
return {
ok: true,
removed: true,
mount,
};
}
async onRuntimeStart(payload: { mount?: string }) {
const mount = typeof payload?.mount === 'string' ? payload.mount.trim() : '';
if (!mount) {
throw new Error('runtime.start requires a non-empty mount');
}
const started = await this._startComponent(mount);
return {
ok: true,
mount,
started,
alreadyRunning: !started,
};
}
async onRuntimeStop(payload: { mount?: string }) {
const mount = typeof payload?.mount === 'string' ? payload.mount.trim() : '';
if (!mount) {
throw new Error('runtime.stop requires a non-empty mount');
}
const stopped = await this._stopComponent(mount);
return {
ok: true,
mount,
stopped,
alreadyStopped: !stopped,
};
}
async onRuntimeReload(payload: { mount?: string }) {
const mount = typeof payload?.mount === 'string' ? payload.mount.trim() : '';
if (!mount) {
throw new Error('runtime.reload requires a non-empty mount');
}
await this._reloadComponent(mount);
return {
ok: true,
mount,
reloaded: true,
};
}
async onRuntimeRestart(payload: { mount?: string }) {
const mount = typeof payload?.mount === 'string' ? payload.mount.trim() : '';
if (!mount) {
throw new Error('runtime.restart requires a non-empty mount');
}
await this._restartComponent(mount);
return {
ok: true,
restarted: true,
mount,
};
}
}

178
packages/core/src/core/Supervision.d.ts vendored Normal file
View File

@ -0,0 +1,178 @@
import type { IExitSignal, IDownSignal, IMonitorRef } from '../engine/mailbox/IMailbox';
export type { IExitSignal, IDownSignal, IMonitorRef };
/**
* Error information passed to supervision handlers.
*/
export interface ISupervisionError {
/** Child id that failed */
childId: string;
/** Error message */
message: string;
/** Stack trace (if available) */
stack?: string;
/** Original error object - use instanceof or type guards to check type */
original?: unknown;
/** Timestamp */
ts: number;
/** Number of times this child has been restarted */
restartCount: number;
}
/**
* Actions that can be taken in response to a child error.
*/
export type SupervisionAction = 'restart' | 'stop' | 'escalate' | 'ignore';
/**
* Built-in supervision strategies.
*/
export type BuiltinStrategy = 'log' | 'restart' | 'restart-all' | 'stop' | 'escalate';
/**
* Custom supervision function.
*/
export type SupervisionFn = (error: ISupervisionError) => SupervisionAction | Promise<SupervisionAction>;
/**
* Supervision strategy - can be built-in string or custom function.
*/
export type SupervisionStrategy = BuiltinStrategy | SupervisionFn;
/**
* Restart limits configuration.
*/
export interface IRestartLimits {
/** Maximum restarts within time window */
maxRestarts: number;
/** Time window in milliseconds */
withinMs: number;
}
/**
* Supervision policy for a composite component.
*/
export interface ISupervisionPolicy {
/** Strategy for handling errors */
strategy: SupervisionStrategy;
/** Restart limits (for 'restart' strategy) */
restartLimits?: IRestartLimits;
/** Delay before restart in ms */
restartDelayMs?: number;
/** Whether to bubble errors up via emit */
emitErrors?: boolean;
}
/**
* Default supervision policy.
*/
export declare const DEFAULT_SUPERVISION_POLICY: ISupervisionPolicy;
/**
* Supervisor - manages error handling for child components.
*
* ## DESIGN NOTE: Exit Signals via Topics (Joe Armstrong's Concern)
*
* Exit signals are delivered via pub/sub topics, not actor mailboxes:
* ```typescript
* // In MatrixActor._monitorChild():
* const exitTopic = `${childInfo.mount}.$exit`;
* transport.subscribe(exitTopic, handleExitSignal);
* ```
*
* **Why topics instead of mailboxes:**
*
* 1. **Multiple observers** - In Erlang, multiple processes can monitor
* the same process. Topics enable parent + siblings + external observers
* to all receive the exit signal.
*
* 2. **System-level vs application-level** - Exit signals are runtime
* infrastructure, not application messages. Erlang's monitors are also
* delivered by the runtime, not via the actor's message queue.
*
* 3. **Separation of concerns** - The mailbox is for business logic messages.
* Supervision is orthogonal infrastructure.
*
* **Tradeoff:**
* - Topics: Anyone who knows the topic can listen (no exclusivity)
* - Mailbox: Exclusive delivery, but only one supervisor per actor
*
* **If mailbox-based supervision is needed**, the IDownSignal could be
* sent to the supervisor's mailbox as a message:
* ```typescript
* supervisorMailbox.send({
* command: '$down',
* payload: { target: 'child.$inbox', reason: 'error' },
* meta: { ts: Date.now() }
* });
* ```
* This would require supervisors to run processMailbox() loops.
*/
export declare class Supervisor {
private readonly _policy;
private readonly _restartCounts;
private readonly _restartTimestamps;
constructor(policy?: ISupervisionPolicy);
/**
* Handle an error from a child.
* Returns the action to take.
*/
handleError(error: ISupervisionError): Promise<SupervisionAction>;
/**
* Check if child is over restart limit.
*/
private _isOverRestartLimit;
/**
* Record a restart for tracking limits.
*/
private _recordRestart;
/**
* Get restart delay in ms.
*/
getRestartDelayMs(): number;
/**
* Check if errors should be emitted.
*/
shouldEmitErrors(): boolean;
/**
* Check if strategy is 'restart-all'.
*/
isRestartAllStrategy(): boolean;
/**
* Reset restart tracking for a child.
*/
resetChild(childId: string): void;
/**
* Reset all tracking.
*/
reset(): void;
/**
* Convert an actor IExitSignal to a SupervisionError.
*
* This bridges the actor mailbox system (IExitSignal) with the
* supervision system (SupervisionError), enabling unified error handling.
*
* @param signal - Exit signal from actor termination
* @returns SupervisionError for processing by handleError()
*/
exitSignalToError(signal: IExitSignal): ISupervisionError;
/**
* Handle an actor exit signal.
*
* Convenience method that converts the signal and determines action.
*
* @param signal - Exit signal from actor termination
* @returns Action to take
*/
handleExitSignal(signal: IExitSignal): Promise<SupervisionAction>;
}
/**
* Create a IDownSignal from an IExitSignal.
*
* Used by supervisors to notify monitors about child termination.
*
* @param exit - Exit signal from terminated actor
* @param observerMount - Mount path of the observer
* @returns IDownSignal to send to observers
*/
export declare function createDownSignal(exit: IExitSignal, observerMount: string): IDownSignal;
/**
* Create a IMonitorRef for tracking monitors.
*
* @param target - Mount path being monitored
* @param observer - Mount path of observer
* @returns IMonitorRef for tracking
*/
export declare function createMonitorRef(target: string, observer: string): IMonitorRef;
//# sourceMappingURL=Supervision.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"Supervision.d.ts","sourceRoot":"","sources":["Supervision.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAGxF,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC;AAEtD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,oBAAoB;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,oDAAoD;IACpD,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GACzB,SAAS,GACT,MAAM,GACN,UAAU,GACV,QAAQ,CAAC;AAEb;;GAEG;AACH,MAAM,MAAM,eAAe,GACvB,KAAK,GACL,SAAS,GACT,aAAa,GACb,MAAM,GACN,UAAU,CAAC;AAEf;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAEzG;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,eAAe,GAAG,aAAa,CAAC;AAElE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,mCAAmC;IACnC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,8CAA8C;IAC9C,aAAa,CAAC,EAAE,cAAc,CAAC;IAC/B,iCAAiC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;GAEG;AACH,eAAO,MAAM,0BAA0B,EAAE,kBAGxC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA+B;gBAEtD,MAAM,GAAE,kBAA+C;IAInE;;;OAGG;IACG,WAAW,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAmDvE;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAa3B;;OAEG;IACH,OAAO,CAAC,cAAc;IAkBtB;;OAEG;IACH,iBAAiB,IAAI,MAAM;IAI3B;;OAEG;IACH,gBAAgB,IAAI,OAAO;IAI3B;;OAEG;IACH,oBAAoB,IAAI,OAAO;IAI/B;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAKjC;;OAEG;IACH,KAAK,IAAI,IAAI;IASb;;;;;;;;OAQG;IACH,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,iBAAiB;IAczD;;;;;;;OAOG;IACG,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,iBAAiB,CAAC;CASxE;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,GAAG,WAAW,CAOtF;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,CAM9E"}

View File

@ -0,0 +1,344 @@
// src/core/Supervision.ts
import type { IExitSignal, IDownSignal, IMonitorRef } from '../engine/mailbox/IMailbox';
// Re-export mailbox types for convenience
export type { IExitSignal, IDownSignal, IMonitorRef };
/**
* Error information passed to supervision handlers.
*/
export interface ISupervisionError {
/** Child id that failed */
childId: string;
/** Error message */
message: string;
/** Stack trace (if available) */
stack?: string;
/** Original error object - use instanceof or type guards to check type */
original?: unknown;
/** Timestamp */
ts: number;
/** Number of times this child has been restarted */
restartCount: number;
}
/**
* Actions that can be taken in response to a child error.
*/
export type SupervisionAction =
| 'restart' // Restart the failed child
| 'stop' // Stop the failed child (remove it)
| 'escalate' // Propagate error to parent
| 'ignore'; // Log and continue
/**
* Built-in supervision strategies.
*/
export type BuiltinStrategy =
| 'log' // Just log errors (default)
| 'restart' // Always restart failed children
| 'restart-all' // Restart all children on any failure
| 'stop' // Stop failed children
| 'escalate'; // Always escalate to parent
/**
* Custom supervision function.
*/
export type SupervisionFn = (error: ISupervisionError) => SupervisionAction | Promise<SupervisionAction>;
/**
* Supervision strategy - can be built-in string or custom function.
*/
export type SupervisionStrategy = BuiltinStrategy | SupervisionFn;
/**
* Restart limits configuration.
*/
export interface IRestartLimits {
/** Maximum restarts within time window */
maxRestarts: number;
/** Time window in milliseconds */
withinMs: number;
}
/**
* Supervision policy for a composite component.
*/
export interface ISupervisionPolicy {
/** Strategy for handling errors */
strategy: SupervisionStrategy;
/** Restart limits (for 'restart' strategy) */
restartLimits?: IRestartLimits;
/** Delay before restart in ms */
restartDelayMs?: number;
/** Whether to bubble errors up via emit */
emitErrors?: boolean;
}
/**
* Default supervision policy.
*/
export const DEFAULT_SUPERVISION_POLICY: ISupervisionPolicy = {
strategy: 'log',
emitErrors: false,
};
/**
* Supervisor - manages error handling for child components.
*
* ## DESIGN NOTE: Exit Signals via Topics (Joe Armstrong's Concern)
*
* Exit signals are delivered via pub/sub topics, not actor mailboxes:
* ```typescript
* // In MatrixActor._monitorChild():
* const exitTopic = `${childInfo.mount}.$exit`;
* transport.subscribe(exitTopic, handleExitSignal);
* ```
*
* **Why topics instead of mailboxes:**
*
* 1. **Multiple observers** - In Erlang, multiple processes can monitor
* the same process. Topics enable parent + siblings + external observers
* to all receive the exit signal.
*
* 2. **System-level vs application-level** - Exit signals are runtime
* infrastructure, not application messages. Erlang's monitors are also
* delivered by the runtime, not via the actor's message queue.
*
* 3. **Separation of concerns** - The mailbox is for business logic messages.
* Supervision is orthogonal infrastructure.
*
* **Tradeoff:**
* - Topics: Anyone who knows the topic can listen (no exclusivity)
* - Mailbox: Exclusive delivery, but only one supervisor per actor
*
* **If mailbox-based supervision is needed**, the IDownSignal could be
* sent to the supervisor's mailbox as a message:
* ```typescript
* supervisorMailbox.send({
* command: '$down',
* payload: { target: 'child.$inbox', reason: 'error' },
* meta: { ts: Date.now() }
* });
* ```
* This would require supervisors to run processMailbox() loops.
*/
export class Supervisor {
private readonly _policy: ISupervisionPolicy;
private readonly _restartCounts = new Map<string, number>();
private readonly _restartTimestamps = new Map<string, number[]>();
constructor(policy: ISupervisionPolicy = DEFAULT_SUPERVISION_POLICY) {
this._policy = policy;
}
/**
* Handle an error from a child.
* Returns the action to take.
*/
async handleError(error: ISupervisionError): Promise<SupervisionAction> {
const { strategy } = this._policy;
// Update restart count
const currentCount = this._restartCounts.get(error.childId) ?? 0;
error.restartCount = currentCount;
// Check restart limits
if (this._isOverRestartLimit(error.childId)) {
// eslint-disable-next-line no-console -- supervisor restart-limit violations need direct kernel diagnostics
console.error(
`[Supervisor] Child '${error.childId}' exceeded restart limits, stopping`
);
return 'stop';
}
// Determine action
let action: SupervisionAction;
if (typeof strategy === 'function') {
action = await strategy(error);
} else {
switch (strategy) {
case 'log':
// eslint-disable-next-line no-console -- log supervision strategy intentionally writes child failures to console
console.error(`[Supervisor] Child '${error.childId}' error:`, error.message);
action = 'ignore';
break;
case 'restart':
action = 'restart';
break;
case 'restart-all':
action = 'restart'; // Caller handles 'all' logic
break;
case 'stop':
action = 'stop';
break;
case 'escalate':
action = 'escalate';
break;
default:
action = 'ignore';
}
}
// Track restart if that's the action
if (action === 'restart') {
this._recordRestart(error.childId);
}
return action;
}
/**
* Check if child is over restart limit.
*/
private _isOverRestartLimit(childId: string): boolean {
const limits = this._policy.restartLimits;
if (!limits) return false;
const timestamps = this._restartTimestamps.get(childId) ?? [];
const now = Date.now();
const windowStart = now - limits.withinMs;
// Count restarts within window
const recentRestarts = timestamps.filter(ts => ts > windowStart).length;
return recentRestarts >= limits.maxRestarts;
}
/**
* Record a restart for tracking limits.
*/
private _recordRestart(childId: string): void {
const count = (this._restartCounts.get(childId) ?? 0) + 1;
this._restartCounts.set(childId, count);
const timestamps = this._restartTimestamps.get(childId) ?? [];
timestamps.push(Date.now());
// Clean old timestamps
const limits = this._policy.restartLimits;
if (limits) {
const windowStart = Date.now() - limits.withinMs;
const recent = timestamps.filter(ts => ts > windowStart);
this._restartTimestamps.set(childId, recent);
} else {
this._restartTimestamps.set(childId, timestamps);
}
}
/**
* Get restart delay in ms.
*/
getRestartDelayMs(): number {
return this._policy.restartDelayMs ?? 100;
}
/**
* Check if errors should be emitted.
*/
shouldEmitErrors(): boolean {
return this._policy.emitErrors ?? false;
}
/**
* Check if strategy is 'restart-all'.
*/
isRestartAllStrategy(): boolean {
return this._policy.strategy === 'restart-all';
}
/**
* Reset restart tracking for a child.
*/
resetChild(childId: string): void {
this._restartCounts.delete(childId);
this._restartTimestamps.delete(childId);
}
/**
* Reset all tracking.
*/
reset(): void {
this._restartCounts.clear();
this._restartTimestamps.clear();
}
// ═══════════════════════════════════════════════════════════════════════
// Actor Exit Signal Integration
// ═══════════════════════════════════════════════════════════════════════
/**
* Convert an actor IExitSignal to a SupervisionError.
*
* This bridges the actor mailbox system (IExitSignal) with the
* supervision system (SupervisionError), enabling unified error handling.
*
* @param signal - Exit signal from actor termination
* @returns SupervisionError for processing by handleError()
*/
exitSignalToError(signal: IExitSignal): ISupervisionError {
// Extract child id from mount path (last segment)
const childId = signal.from.split('.').pop() ?? signal.from;
return {
childId,
message: signal.error?.message ?? `Actor terminated with reason: ${signal.reason}`,
stack: signal.error?.stack,
original: signal,
ts: Date.now(),
restartCount: this._restartCounts.get(childId) ?? 0,
};
}
/**
* Handle an actor exit signal.
*
* Convenience method that converts the signal and determines action.
*
* @param signal - Exit signal from actor termination
* @returns Action to take
*/
async handleExitSignal(signal: IExitSignal): Promise<SupervisionAction> {
// Normal termination doesn't need supervision action
if (signal.reason === 'normal') {
return 'ignore';
}
const error = this.exitSignalToError(signal);
return this.handleError(error);
}
}
/**
* Create a IDownSignal from an IExitSignal.
*
* Used by supervisors to notify monitors about child termination.
*
* @param exit - Exit signal from terminated actor
* @param observerMount - Mount path of the observer
* @returns IDownSignal to send to observers
*/
export function createDownSignal(exit: IExitSignal, observerMount: string): IDownSignal {
return {
type: 'down',
target: exit.from,
reason: exit.reason,
error: exit.error,
};
}
/**
* Create a IMonitorRef for tracking monitors.
*
* @param target - Mount path being monitored
* @param observer - Mount path of observer
* @returns IMonitorRef for tracking
*/
export function createMonitorRef(target: string, observer: string): IMonitorRef {
return {
id: `${observer}->${target}-${Date.now()}`,
target,
observer,
};
}

View File

@ -0,0 +1,359 @@
/**
* WorkstreamActor base class for workstream-level cognitive actors.
*
* Extracted from SmithersSupervisor (Phase 3 reference implementation).
* Provides: memory persistence, plan lifecycle, context recovery on wake,
* and a standard cognitive field pattern for any workstream.
*
* Typically registers with system.workstreams, but may mount anywhere in the
* domain hierarchy. The coordinator is a registry/protocol surface, not the
* required parent mount for every workstream instance.
*
* Subclasses declare:
* - static purpose (required)
* - static systemPrompt (optional)
* - static promptTriggers (optional)
* - static skills (optional)
* - onBootstrap(): initialize domain-specific state
*
* Constraints satisfied:
* C14: One actor per workstream
* C16: Memory survives context window death
* C17: Extracted from proven Smithers pattern
*/
import { MatrixActor } from './MatrixActor';
import { RequestReply } from '../engine/remoting/RequestReply';
/** Plan step structure */
export interface PlanStep {
name: string;
status: 'pending' | 'in_progress' | 'completed' | 'failed' | 'skipped';
}
/** Active plan recovered from memory */
export interface ActivePlan {
planId?: string;
name: string;
steps: PlanStep[];
}
interface WorkstreamRequestRecord {
id: string;
text: string;
from: string | null;
priority: string | null;
status: 'pending';
ts: number;
}
/**
* Base class for workstream actors. Extends MatrixActor with
* memory persistence, plan lifecycle, and context recovery.
*/
export class WorkstreamActor extends MatrixActor {
// ─── Cognitive Defaults ──────────────────────────────────────────────
// Subclasses MUST override purpose. All other fields have sensible defaults.
static promptable = true;
static regime = 'focused' as const;
static memoryScope = 'actor' as const;
static planScope = 'actor' as const;
static continuationType = 'persistent' as const;
static kind = 'workstream';
static contract = {
interfaces: {
workstream: {
description: 'Generic durable workstream actor surface',
ops: {
status: {
description: 'Get workstream status summary',
payload: {},
response: {
ok: { type: 'boolean', required: true },
name: { type: 'string', required: true },
purpose: { type: 'string', required: true },
regime: { type: 'string', required: true },
promptable: { type: 'boolean', required: true },
pendingRequests: { type: 'number', required: true },
activeAgentCount: { type: 'number', required: true },
},
},
request: {
description: 'Submit an external request into the workstream inbox',
payload: {
text: { type: 'string', required: true, description: 'Request text or instruction' },
from: { type: 'string', required: false, description: 'Requesting actor or operator' },
priority: { type: 'string', required: false, description: 'Optional priority label' },
},
response: {
ok: { type: 'boolean', required: true },
requestId: { type: 'string', required: true },
status: { type: 'string', required: true },
},
},
plan: {
description: 'Get active plan',
payload: {},
response: {
ok: { type: 'boolean', required: true },
plan: { type: 'object', required: false, description: 'Active plan when present' },
},
},
agents: {
description: 'List the workstream actor and visible child agents',
payload: {},
response: {
ok: { type: 'boolean', required: true },
agents: { type: 'array', required: true },
},
},
},
},
},
};
static accepts = {
'workstream.status': { description: 'Get workstream status summary' },
'workstream.request': { description: 'Submit a request into the workstream inbox' },
'workstream.memory': { description: 'Query workstream memory' },
'workstream.plan': { description: 'Get active plan' },
'workstream.agents': { description: 'List the workstream actor and visible child agents' },
};
static emits = {
'workstream.phaseChanged': { description: 'Published when workstream phase changes' },
'workstream.memoryWritten': { description: 'Published when a memory record is saved' },
'workstream.requested': { description: 'Published when a new workstream request is queued' },
};
// ─── Instance State ──────────────────────────────────────────────────
/** Active plan recovered from memory on wake */
protected _activePlan: ActivePlan | null = null;
/** Workstream name (derived from mount path) */
protected _workstreamName = '';
/** Minimal in-memory request inbox for generic operator tooling */
protected _requests: WorkstreamRequestRecord[] = [];
/** Best-effort flag to avoid unregister spam when registration never succeeded */
protected _registeredWithCoordinator = false;
// ─── Lifecycle ───────────────────────────────────────────────────────
protected override async onBootstrap(): Promise<void> {
// Derive workstream name from the actor mount, regardless of namespace.
const mount = this._context?.mount ?? '';
const parts = mount.split('.');
this._workstreamName = parts[parts.length - 1] || 'unknown';
// Phase 3/C16: Recover context from memory on wake
await this._recoverFromMemory();
await this._registerWithCoordinator();
}
protected override onDispose(): void {
this._unregisterFromCoordinator();
}
/** Recover active plan and recent memory on actor wake. */
private async _recoverFromMemory(): Promise<void> {
try {
const plan = await this.queryActivePlan();
if (plan) {
this._activePlan = plan;
const currentStep = plan.steps.findIndex(s => s.status === 'in_progress');
// eslint-disable-next-line no-console -- bootstrap recovery notice for kernel workstream actor
console.log(`[${this._workstreamName}] Recovered active plan: ${plan.name}, step ${currentStep + 1}/${plan.steps.length}`);
}
} catch { /* memory service may not be ready at bootstrap — non-fatal */ }
}
// ─── Op Handlers ─────────────────────────────────────────────────────
onWorkstreamStatus(): unknown {
const ctor = this.constructor as typeof WorkstreamActor;
const childCount = this._children?.getAllChildren()?.length ?? 0;
return {
ok: true,
name: this._workstreamName,
purpose: ctor.purpose ?? '(none)',
regime: ctor.regime,
promptable: ctor.promptable,
kind: ctor.kind,
pendingRequests: this._requests.length,
activeAgentCount: 1 + childCount,
activePlan: this._activePlan ? {
name: this._activePlan.name,
steps: this._activePlan.steps,
currentStep: this._activePlan.steps.find(s => s.status === 'in_progress')?.name ?? null,
} : null,
triggers: ctor.promptTriggers?.length ?? 0,
};
}
onWorkstreamRequest(payload: Record<string, unknown>): unknown {
const text = typeof payload.text === 'string' ? payload.text.trim() : '';
if (!text) return { ok: false, error: 'text required' };
const request: WorkstreamRequestRecord = {
id: `req-${Date.now()}-${this._requests.length + 1}`,
text,
from: typeof payload.from === 'string' ? payload.from : null,
priority: typeof payload.priority === 'string' ? payload.priority : null,
status: 'pending',
ts: Date.now(),
};
this._requests.push(request);
this.emit('workstream.requested', request);
return { ok: true, requestId: request.id, status: request.status };
}
private async _registerWithCoordinator(): Promise<void> {
if (!this._context) return;
const ctor = this.constructor as typeof WorkstreamActor;
try {
await RequestReply.execute(this._context, 'system.workstreams', 'workstream.register', {
name: this._workstreamName,
mount: this._context.mount,
type: ctor.name,
purpose: ctor.purpose ?? '(none)',
promptable: ctor.promptable,
regime: ctor.regime,
triggerCount: ctor.promptTriggers?.length ?? 0,
}, { timeoutMs: 2000 });
this._registeredWithCoordinator = true;
} catch {
this._registeredWithCoordinator = false;
}
}
private _unregisterFromCoordinator(): void {
if (!this._context || !this._registeredWithCoordinator) return;
RequestReply.execute(this._context, 'system.workstreams', 'workstream.unregister', {
mount: this._context.mount,
}, { timeoutMs: 1000 }).catch(() => {});
}
async onWorkstreamMemory(payload: Record<string, unknown>): Promise<unknown> {
const limit = typeof payload.limit === 'number' ? payload.limit : 10;
return this.queryRecentMemory(limit);
}
async onWorkstreamPlan(): Promise<unknown> {
const plan = this._activePlan ?? await this.queryActivePlan();
return plan ? { ok: true, plan } : { ok: false, error: 'No active plan' };
}
onWorkstreamAgents(): unknown {
const childAgents = (this._children?.getAllChildren() ?? []).map((child) => ({
id: child.id,
mount: child.mount,
componentClass: child.componentClass,
role: 'child',
}));
return {
ok: true,
agents: [
{
id: this._workstreamName,
mount: this._context?.mount ?? '',
componentClass: (this.constructor as typeof WorkstreamActor).name,
role: 'workstream',
},
...childAgents,
],
};
}
// ─── Memory Helpers (protected — available to subclasses) ────────────
/** Save a memory record at a key decision point. Non-fatal on failure. */
protected async saveMemory(content: string, tags: string[] = []): Promise<void> {
if (!this._context) return;
try {
await RequestReply.execute(this._context, 'system.agents.memory', 'memory.save', {
mount: this._context.mount,
content,
tags: [this._workstreamName, ...tags],
}, { timeoutMs: 5000 });
this.emit('workstream.memoryWritten', { content: content.slice(0, 100), tags });
// Stigmergy: publish workstream state to blackboard
RequestReply.sendToInbox(this._context, 'system.blackboard', 'fact.assert', {
domain: `workstream.${this._workstreamName}`,
data: { lastMemory: content.slice(0, 100), tags, updatedAt: Date.now() },
});
} catch { /* memory service may be unavailable — non-fatal */ }
}
/** Query active plan from system.memory. */
protected async queryActivePlan(): Promise<ActivePlan | null> {
if (!this._context) return null;
try {
const result = await RequestReply.execute(this._context, 'system.agents.memory', 'plan.active', {
mount: this._context.mount,
}, { timeoutMs: 3000 }) as Record<string, unknown>;
if (result?.ok && result?.name) {
return result as unknown as ActivePlan;
}
} catch { /* memory service unavailable */ }
return null;
}
/** Query recent memory records. */
protected async queryRecentMemory(limit = 10): Promise<unknown> {
if (!this._context) return { ok: false, error: 'No context' };
try {
return await RequestReply.execute(this._context, 'system.agents.memory', 'memory.history', {
mount: this._context.mount, limit,
}, { timeoutMs: 3000 });
} catch { return { ok: false, error: 'Memory service unavailable' }; }
}
// ─── Plan Lifecycle (protected — available to subclasses) ────────────
/** Create a plan with named steps. Returns planId or null. */
protected async createPlan(name: string, steps: Array<{ name: string }>): Promise<string | null> {
if (!this._context) return null;
try {
const result = await RequestReply.execute(this._context, 'system.agents.memory', 'plan.create', {
mount: this._context.mount,
name,
steps: steps.map(s => ({ name: s.name, status: 'pending' })),
}, { timeoutMs: 5000 }) as Record<string, unknown>;
const planId = (result?.planId as string) ?? null;
if (planId && result?.name) {
this._activePlan = result as unknown as ActivePlan;
}
return planId;
} catch { return null; }
}
/** Update a plan step status. */
protected async updatePlanStep(planId: string, stepIndex: number, status: string): Promise<void> {
if (!this._context) return;
try {
await RequestReply.execute(this._context, 'system.agents.memory', 'plan.update', {
planId, stepIndex, status,
}, { timeoutMs: 3000 });
// Update local cache
if (this._activePlan?.planId === planId && this._activePlan.steps[stepIndex]) {
this._activePlan.steps[stepIndex].status = status as PlanStep['status'];
}
this.emit('workstream.phaseChanged', { planId, stepIndex, status });
} catch { /* non-fatal */ }
}
/** Mark a plan as complete. */
protected async completePlan(planId: string): Promise<void> {
if (!this._context) return;
try {
await RequestReply.execute(this._context, 'system.agents.memory', 'plan.complete', {
planId,
}, { timeoutMs: 3000 });
if (this._activePlan?.planId === planId) {
this._activePlan = null; // Clear active plan
}
} catch { /* non-fatal */ }
}
}

View File

@ -0,0 +1,258 @@
/**
* Behavioral tests for PackageRootActor storage seam (HARVEST-001 / HARVEST-002).
*
* Two layers:
*
* 1. Fast unit tests of _initStorage against a minimal fake context.
* No MatrixRuntime, no transport, no $join, no shutdown wait.
* Proves the contract:
* - provider present + returns store -> RECORD_STORE_KEY set
* - no provider -> deferred (no RECORD_STORE_KEY, no throw)
* - provider returns null -> deferred
* - provider throws -> deferred (bootstrap unaffected)
* - manifest declares no storage -> provider not invoked
* - provider invoked with (matrixHome, domain) where domain comes from manifest
* Each test runs in <10ms.
*
* 2. Runtime-wiring test: a single test that proves MatrixRuntime registers
* the provider on its rootContext under RECORD_STORE_PROVIDER_KEY when
* `recordStoreProvider` is supplied via IRuntimeConfig. Uses runtime.shutdown()
* to exit cleanly. This is the only test that touches a real runtime.
*
* Why this split:
* - The seam is a context-service contract. The unit tests test the contract.
* - The runtime-wiring test proves IRuntimeConfig.recordStoreProvider lands
* where PackageRootActor will look for it, without dragging the actor
* bootstrap path (and its $join wait) into the assertion loop.
*/
import '../../_node-compat.js';
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { MatrixRuntime } from '../../runtime/MatrixRuntime.js';
import { PackageRootActor, type PackageManifestProps } from '../PackageRootActor.js';
import { RECORD_STORE_PROVIDER_KEY, type IRecordStoreProvider } from '../IRecordStoreProvider.js';
import { RECORD_STORE_KEY } from '../../framework/InteractionRecord.js';
// ── Fake context (cascading service registry, no runtime) ───────────────────
interface IServiceLookup {
getService(key: string | symbol): unknown;
setService(key: string | symbol, value: unknown): void;
}
class FakeContext implements IServiceLookup {
private readonly services = new Map<string | symbol, unknown>();
constructor(private readonly parent?: IServiceLookup) {}
getService(key: string | symbol): unknown {
if (this.services.has(key)) return this.services.get(key);
return this.parent?.getService(key);
}
setService(key: string | symbol, value: unknown): void {
this.services.set(key, value);
}
}
// ── Test PackageRootActor that bypasses MatrixActor mounting ────────────────
class TestPackageRoot extends PackageRootActor {
static override package = '@open-matrix/test-package';
static override namespace = 'test.pkg';
/**
* Set both _props (what onBootstrap reads) and _manifestProps (what
* _initStorage reads). onBootstrap normally copies _props -> _manifestProps;
* the test bypasses bootstrap so we copy here.
*/
setManifestProps(props: PackageManifestProps): void {
(this as unknown as { _props: PackageManifestProps })._props = props;
(this as unknown as { _manifestProps: PackageManifestProps })._manifestProps = props;
}
attachContext(ctx: IServiceLookup): void {
(this as unknown as { _context: IServiceLookup })._context = ctx;
}
async runInitStorage(): Promise<void> {
await (this as unknown as { _initStorage(): Promise<void> })._initStorage();
}
}
function makeContextWithProvider(provider: IRecordStoreProvider): FakeContext {
const root = new FakeContext();
root.setService(RECORD_STORE_PROVIDER_KEY, provider);
return new FakeContext(root); // child cascades to root
}
function makeContextNoProvider(): FakeContext {
const root = new FakeContext();
return new FakeContext(root);
}
// ── Unit tests (fast: no runtime) ────────────────────────────────────────────
describe('PackageRootActor storage seam — unit', () => {
it('sets RECORD_STORE_KEY when provider returns a store; passes (matrixHome, domain)', async () => {
const fakeStore = { __probe: 'fake-store-handle' };
const calls: Array<{ matrixHome: string; domain: string }> = [];
const provider: IRecordStoreProvider = {
async provide(matrixHome, domain) {
calls.push({ matrixHome, domain });
return fakeStore;
},
};
const ctx = makeContextWithProvider(provider);
const actor = new TestPackageRoot();
actor.setManifestProps({
storage: { sqlite: { domain: 'test-domain' } },
packageName: '@open-matrix/test-package',
});
actor.attachContext(ctx);
await actor.runInitStorage();
assert.equal(calls.length, 1, 'provider invoked exactly once');
assert.equal(calls[0]!.domain, 'test-domain', 'domain matches manifest');
assert.ok(calls[0]!.matrixHome.length > 0, 'matrixHome resolved (non-empty)');
assert.deepEqual(ctx.getService(RECORD_STORE_KEY), fakeStore, 'store registered on context');
});
it('does not set RECORD_STORE_KEY when no provider is registered (deferred)', async () => {
const ctx = makeContextNoProvider();
const actor = new TestPackageRoot();
actor.setManifestProps({
storage: { sqlite: { domain: 'test-domain-2' } },
packageName: '@open-matrix/test-package',
});
actor.attachContext(ctx);
await actor.runInitStorage();
assert.equal(ctx.getService(RECORD_STORE_KEY), undefined, 'no store; deferred');
});
it('does not set RECORD_STORE_KEY when provider returns null (declined)', async () => {
const provider: IRecordStoreProvider = { async provide() { return null; } };
const ctx = makeContextWithProvider(provider);
const actor = new TestPackageRoot();
actor.setManifestProps({
storage: { sqlite: { domain: 'test-domain-3' } },
packageName: '@open-matrix/test-package',
});
actor.attachContext(ctx);
await actor.runInitStorage();
assert.equal(ctx.getService(RECORD_STORE_KEY), undefined, 'declined provider; deferred');
});
it('swallows provider errors; bootstrap not affected (matches pre-seam swallow)', async () => {
const provider: IRecordStoreProvider = {
async provide() { throw new Error('synthetic provider failure'); },
};
const ctx = makeContextWithProvider(provider);
const actor = new TestPackageRoot();
actor.setManifestProps({
storage: { sqlite: { domain: 'test-domain-4' } },
packageName: '@open-matrix/test-package',
});
actor.attachContext(ctx);
await actor.runInitStorage(); // must not throw
assert.equal(ctx.getService(RECORD_STORE_KEY), undefined, 'failed provider; deferred');
});
it('does not invoke provider when manifest has no storage declaration', async () => {
let providerCalled = false;
const provider: IRecordStoreProvider = {
async provide() { providerCalled = true; return { dummy: true }; },
};
const ctx = makeContextWithProvider(provider);
const actor = new TestPackageRoot();
actor.setManifestProps({
packageName: '@open-matrix/test-package',
});
actor.attachContext(ctx);
await actor.runInitStorage();
assert.equal(providerCalled, false, 'no storage declared -> provider not invoked');
assert.equal(ctx.getService(RECORD_STORE_KEY), undefined);
});
it('passes the exact domain string from manifest (per-package data isolation)', async () => {
// Defensive: HARVEST-SQLITE-ARCH says each package/domain owns its own
// <matrixHome>/data/<domain>/store.db — provider must receive the domain
// verbatim so two packages with different domains never collide.
const observedDomains: string[] = [];
const provider: IRecordStoreProvider = {
async provide(_matrixHome, domain) {
observedDomains.push(domain);
return { handle: domain };
},
};
const root = new FakeContext();
root.setService(RECORD_STORE_PROVIDER_KEY, provider);
for (const domain of ['pkg-a', 'pkg-b', 'system.observability']) {
const ctx = new FakeContext(root);
const actor = new TestPackageRoot();
actor.setManifestProps({ storage: { sqlite: { domain } }, packageName: domain });
actor.attachContext(ctx);
await actor.runInitStorage();
}
assert.deepEqual(observedDomains, ['pkg-a', 'pkg-b', 'system.observability']);
});
it('seam key matches the symbol the runtime config registers under', () => {
// Symbol.for is global so this is identity-equal across modules; the
// assertion guards against accidental divergence in future edits.
assert.equal(
RECORD_STORE_PROVIDER_KEY,
Symbol.for('matrix.record-store.provider'),
);
});
});
// ── Runtime wiring (single integration assertion) ────────────────────────────
describe('MatrixRuntime registers IRecordStoreProvider on rootContext', () => {
it('places config.recordStoreProvider at RECORD_STORE_PROVIDER_KEY on rootContext', async () => {
const provider: IRecordStoreProvider = { async provide() { return null; } };
const runtime = new MatrixRuntime({
logging: false,
recordStoreProvider: provider,
});
try {
const ctx = (runtime as unknown as { rootContext: { getService(k: symbol): unknown } }).rootContext;
const found = ctx.getService(RECORD_STORE_PROVIDER_KEY);
assert.equal(found, provider, 'runtime registered provider under expected key');
} finally {
await runtime.shutdown();
}
});
it('does not register a provider when config.recordStoreProvider is omitted', async () => {
const runtime = new MatrixRuntime({ logging: false });
try {
const ctx = (runtime as unknown as { rootContext: { getService(k: symbol): unknown } }).rootContext;
const found = ctx.getService(RECORD_STORE_PROVIDER_KEY);
assert.equal(found, undefined, 'no provider registered; consumers see undefined');
} finally {
await runtime.shutdown();
}
});
});

View File

@ -0,0 +1,6 @@
/**
* Typed accepts definition for compile-time safety.
* Maps command names to their expected payload types.
*/
export type AcceptsDefinition = Record<string, unknown>;
//# sourceMappingURL=AcceptsDefinition.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"AcceptsDefinition.d.ts","sourceRoot":"","sources":["AcceptsDefinition.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC"}

View File

@ -0,0 +1,7 @@
// src/core/actor/AcceptsDefinition.ts
/**
* Typed accepts definition for compile-time safety.
* Maps command names to their expected payload types.
*/
export type AcceptsDefinition = Record<string, unknown>;

View File

@ -0,0 +1,134 @@
import type { IMailbox } from '../../engine/mailbox/IMailbox';
import type { IMatrixContext } from '../../engine/core/IMatrixContext';
import type { AcceptsDefinition } from './AcceptsDefinition';
import type { IActorBehavior } from './IActorBehavior';
import type { IDispatchStrategy } from './IDispatchStrategy';
import type { IMessageMeta } from './IMessageMeta';
import type { IDispatchPriorities } from './IDispatchPriorities';
/** @deprecated Use IMessageMeta instead */
type MessageMeta = IMessageMeta;
/** @deprecated Use IDispatchPriorities instead */
type DispatchPriorities = IDispatchPriorities;
/**
* Options for ActorBehavior construction.
*/
export interface IActorBehaviorOptions {
/**
* Whether to yield between messages (cooperative scheduling).
* Default: true
*/
cooperativeYield?: boolean;
}
/** @deprecated Use IActorBehaviorOptions instead */
export type ActorBehaviorOptions = IActorBehaviorOptions;
/**
* ActorBehavior - Encapsulates mailbox and handler dispatch.
*
* This class implements the composed actor behavior pattern:
* - MatrixActor composes ActorBehavior (not extends)
* - Mailbox ownership is exclusive to this behavior
* - Handlers are registered and dispatched through strategies
* - Erlang-style become() enables state machine patterns
*
* @example
* ```typescript
* class MyActor extends MatrixActor {
* protected onBootstrap() {
* this.enableActorMode({
* increment: this.handleIncrement.bind(this),
* query: this.handleQuery.bind(this),
* });
* }
* }
* ```
*/
export declare class ActorBehavior<TAccepts extends AcceptsDefinition = AcceptsDefinition> implements IActorBehavior<TAccepts> {
private readonly mailbox;
private readonly context;
private readonly handlers;
private readonly strategies;
private readonly cooperativeYield;
private priorities;
private running;
constructor(context: IMatrixContext, options?: IActorBehaviorOptions);
get isRunning(): boolean;
get address(): string;
/**
* Get the underlying mailbox for message queuing.
*
* This allows external code (like MatrixActor._bindAccepts) to queue
* messages to the actor's mailbox. The ActorBehavior maintains ownership
* and processes messages through its dispatch loop.
*/
getMailbox(): IMailbox;
registerHandler<K extends keyof TAccepts & string>(command: K, handler: (payload: TAccepts[K], meta: MessageMeta) => Promise<unknown>): void;
unregisterHandler(command: string): void;
become(handlers: Partial<{
[K in keyof TAccepts]: (payload: TAccepts[K], meta: MessageMeta) => Promise<unknown>;
}>): void;
addDispatchStrategy(strategy: IDispatchStrategy): void;
removeDispatchStrategy(name: string): void;
/**
* Configure dispatch priorities.
* Re-sorts strategies after update.
*/
configurePriorities(priorities: Partial<DispatchPriorities>): void;
/**
* Check if a handler exists for a command.
*/
hasHandler(command: string): boolean;
/**
* Get list of registered handler commands.
*/
listHandlers(): string[];
start(): void;
stop(): void;
/**
* Main message processing loop.
*
* Dispatch order:
* 1. System commands ($* prefix) - built-in handling
* 2. Dispatch strategies (in priority order)
* 3. TypeScript handlers (fallback)
*/
private runLoop;
/**
* Dispatch a single message.
*
* Vision 27 Security: Messages are verified before dispatch.
* Joe Armstrong: "Every message must be verified at the boundary."
*/
private dispatch;
/**
* Type guard for BecomePayload.
*/
private isBecomePayload;
/**
* Handle system commands ($* prefix).
*
* System commands:
* - $close: Stop the actor
* - $ping: Health check
* - $introspect: Return handler info
* - $become: Change behavior at runtime
*/
private handleSystemCommand;
/**
* Determine message direction for security verification.
*
* Vision 27: Direction determines trust level:
* - parent-to-child: TRUSTED (supervisor relationship)
* - child-to-parent: VERIFIED (requires capability)
* - sibling: VERIFIED (requires capability)
* - external: UNTRUSTED (requires capability + signature)
*
* Joe Armstrong (Erlang, "Programming Erlang"):
* "In Erlang, every message knows who sent it."
*
* @param from - Sender's mount path
* @returns Message direction for policy lookup
*/
private _determineDirection;
}
export {};
//# sourceMappingURL=ActorBehavior.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"ActorBehavior.d.ts","sourceRoot":"","sources":["ActorBehavior.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAiB,MAAM,+BAA+B,CAAC;AAC7E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAEvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAKjE,2CAA2C;AAC3C,KAAK,WAAW,GAAG,YAAY,CAAC;AAChC,kDAAkD;AAClD,KAAK,kBAAkB,GAAG,mBAAmB,CAAC;AAI9C;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,oDAAoD;AACpD,MAAM,MAAM,oBAAoB,GAAG,qBAAqB,CAAC;AAOzD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,aAAa,CAAC,QAAQ,SAAS,iBAAiB,GAAG,iBAAiB,CAC/E,YAAW,cAAc,CAAC,QAAQ,CAAC;IAEnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAW;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA8B;IACvD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA2B;IACtD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAU;IAC3C,OAAO,CAAC,UAAU,CAAiD;IACnE,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,EAAE,cAAc,EAAE,OAAO,GAAE,qBAA0B;IAMxE,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED;;;;;;OAMG;IACH,UAAU,IAAI,QAAQ;IAItB,eAAe,CAAC,CAAC,SAAS,MAAM,QAAQ,GAAG,MAAM,EAC/C,OAAO,EAAE,CAAC,EACV,OAAO,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,GACrE,IAAI;IAIP,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAIxC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC;SACtB,CAAC,IAAI,MAAM,QAAQ,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC;KACrF,CAAC,GAAG,IAAI;IAST,mBAAmB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI;IAKtD,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAO1C;;;OAGG;IACH,mBAAmB,CAAC,UAAU,EAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG,IAAI;IAKlE;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAKpC;;OAEG;IACH,YAAY,IAAI,MAAM,EAAE;IAIxB,KAAK,IAAI,IAAI;IAOb,IAAI,IAAI,IAAI;IAKZ;;;;;;;OAOG;YACW,OAAO;IAmBrB;;;;;OAKG;YACW,QAAQ;IAoGtB;;OAEG;IACH,OAAO,CAAC,eAAe;IASvB;;;;;;;;OAQG;YACW,mBAAmB;IA2CjC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,mBAAmB;CA6B5B"}

View File

@ -0,0 +1,412 @@
// src/core/actor/ActorBehavior.ts
import type { IMailbox, IActorMessage } from '../../engine/mailbox/IMailbox';
import type { IMatrixContext } from '../../engine/core/IMatrixContext';
import { RequestReplyHelper } from '../messaging/RequestReplyHelper';
import type { AcceptsDefinition } from './AcceptsDefinition';
import type { IActorBehavior } from './IActorBehavior';
import type { IDispatchStrategy } from './IDispatchStrategy';
import type { IMessageMeta } from './IMessageMeta';
import type { IDispatchPriorities } from './IDispatchPriorities';
import { DEFAULT_PRIORITIES } from './IDispatchPriorities';
import type { IBecomePayload } from './IBecomePayload';
import type { MessageDirection } from '../security/ISecurityRealm';
/** @deprecated Use IMessageMeta instead */
type MessageMeta = IMessageMeta;
/** @deprecated Use IDispatchPriorities instead */
type DispatchPriorities = IDispatchPriorities;
/** @deprecated Use IBecomePayload instead */
type BecomePayload<T extends AcceptsDefinition = AcceptsDefinition> = IBecomePayload<T>;
/**
* Options for ActorBehavior construction.
*/
export interface IActorBehaviorOptions {
/**
* Whether to yield between messages (cooperative scheduling).
* Default: true
*/
cooperativeYield?: boolean;
}
/** @deprecated Use IActorBehaviorOptions instead */
export type ActorBehaviorOptions = IActorBehaviorOptions;
/**
* Handler function type.
*/
type Handler = (payload: unknown, meta: MessageMeta) => Promise<unknown>;
/**
* ActorBehavior - Encapsulates mailbox and handler dispatch.
*
* This class implements the composed actor behavior pattern:
* - MatrixActor composes ActorBehavior (not extends)
* - Mailbox ownership is exclusive to this behavior
* - Handlers are registered and dispatched through strategies
* - Erlang-style become() enables state machine patterns
*
* @example
* ```typescript
* class MyActor extends MatrixActor {
* protected onBootstrap() {
* this.enableActorMode({
* increment: this.handleIncrement.bind(this),
* query: this.handleQuery.bind(this),
* });
* }
* }
* ```
*/
export class ActorBehavior<TAccepts extends AcceptsDefinition = AcceptsDefinition>
implements IActorBehavior<TAccepts>
{
private readonly mailbox: IMailbox;
private readonly context: IMatrixContext;
private readonly handlers = new Map<string, Handler>();
private readonly strategies: IDispatchStrategy[] = [];
private readonly cooperativeYield: boolean;
private priorities: DispatchPriorities = { ...DEFAULT_PRIORITIES };
private running = false;
constructor(context: IMatrixContext, options: IActorBehaviorOptions = {}) {
this.context = context;
this.mailbox = context.runtime.createMailbox(context.mount);
this.cooperativeYield = options.cooperativeYield ?? true;
}
get isRunning(): boolean {
return this.running;
}
get address(): string {
return this.mailbox.address;
}
/**
* Get the underlying mailbox for message queuing.
*
* This allows external code (like MatrixActor._bindAccepts) to queue
* messages to the actor's mailbox. The ActorBehavior maintains ownership
* and processes messages through its dispatch loop.
*/
getMailbox(): IMailbox {
return this.mailbox;
}
registerHandler<K extends keyof TAccepts & string>(
command: K,
handler: (payload: TAccepts[K], meta: MessageMeta) => Promise<unknown>
): void {
this.handlers.set(command, handler as Handler);
}
unregisterHandler(command: string): void {
this.handlers.delete(command);
}
become(handlers: Partial<{
[K in keyof TAccepts]: (payload: TAccepts[K], meta: MessageMeta) => Promise<unknown>
}>): void {
this.handlers.clear();
for (const [cmd, handler] of Object.entries(handlers)) {
if (handler) {
this.handlers.set(cmd, handler as Handler);
}
}
}
addDispatchStrategy(strategy: IDispatchStrategy): void {
this.strategies.push(strategy);
this.strategies.sort((a, b) => a.priority - b.priority);
}
removeDispatchStrategy(name: string): void {
const idx = this.strategies.findIndex(s => s.name === name);
if (idx >= 0) {
this.strategies.splice(idx, 1);
}
}
/**
* Configure dispatch priorities.
* Re-sorts strategies after update.
*/
configurePriorities(priorities: Partial<DispatchPriorities>): void {
this.priorities = { ...this.priorities, ...priorities };
this.strategies.sort((a, b) => a.priority - b.priority);
}
/**
* Check if a handler exists for a command.
*/
hasHandler(command: string): boolean {
if (this.handlers.has(command)) return true;
return this.strategies.some(s => s.canHandle(command));
}
/**
* Get list of registered handler commands.
*/
listHandlers(): string[] {
return Array.from(this.handlers.keys());
}
start(): void {
if (this.running) return;
this.running = true;
this.mailbox.claim();
this.runLoop();
}
stop(): void {
this.running = false;
this.mailbox.close();
}
/**
* Main message processing loop.
*
* Dispatch order:
* 1. System commands ($* prefix) - built-in handling
* 2. Dispatch strategies (in priority order)
* 3. TypeScript handlers (fallback)
*/
private async runLoop(): Promise<void> {
try {
for await (const message of this.mailbox) {
if (!this.running) break;
await this.dispatch(message);
}
} catch (err) {
if (this.running) {
// Publish exit signal
this.context.transport.publish(`${this.context.mount}.$exit`, {
type: 'exit',
from: this.context.mount,
reason: 'error',
error: { message: err instanceof Error ? err.message : String(err) },
});
}
}
}
/**
* Dispatch a single message.
*
* Vision 27 Security: Messages are verified before dispatch.
* Joe Armstrong: "Every message must be verified at the boundary."
*/
private async dispatch(message: IActorMessage): Promise<void> {
// Cooperative yield - allow other tasks to run
if (this.cooperativeYield) {
await Promise.resolve();
}
const { command, payload } = message;
const meta: MessageMeta = {
ts: message.meta.ts,
from: message.meta.from,
correlationId: message.meta.correlationId,
topic: `${this.context.mount}.accepts.${command}`,
};
// ═══════════════════════════════════════════════════════════════════
// PRIORITY 1: System commands ($* prefix) - bypass normal dispatch
// ═══════════════════════════════════════════════════════════════════
if (command.startsWith('$')) {
await this.handleSystemCommand(command, payload, meta);
return;
}
// ═══════════════════════════════════════════════════════════════════
// PRIORITY 1.5: SECURITY VERIFICATION (Vision 27)
// Mark Miller: "The receiver decides whether to trust the sender."
// ═══════════════════════════════════════════════════════════════════
if (this.context.securityRealm) {
const direction = this._determineDirection(message.meta.from);
const capabilityId = message.meta.capabilityId;
// Look up capability token if ID provided
// Note: For cross-root verification, capability lookup would need
// to consult a shared registry. For now, we verify against our security realm.
const verificationResult = this.context.securityRealm.verifyMessage(
direction,
undefined, // TODO: Look up capability by ID from shared registry
{
principal: message.meta.from ?? 'unknown',
target: this.context.mount,
command,
}
);
if (!verificationResult.allowed) {
// eslint-disable-next-line no-console -- security rejection diagnostics in kernel behavior layer
console.warn(
`[ActorBehavior] Security: Message rejected at ${this.context.mount}`,
{
command,
from: message.meta.from,
direction,
reason: verificationResult.reason,
}
);
// Send error reply if this was a request-reply
if (RequestReplyHelper.isEnvelope(payload)) {
RequestReplyHelper.sendErrorReply(
this.context.transport,
payload,
new Error(`Security: ${verificationResult.reason}`)
);
}
return; // BLOCK the message
}
}
// ═══════════════════════════════════════════════════════════════════
// PRIORITY 2-N: Dispatch strategies (in priority order)
// ═══════════════════════════════════════════════════════════════════
for (const strategy of this.strategies) {
if (strategy.canHandle(command)) {
// Use executeWithReply (throws on error) instead of executeWithReplySafe
// This allows errors to propagate to runLoop() for $exit signal
await RequestReplyHelper.executeWithReply(
this.context.transport,
payload,
(params) => strategy.handle(command, params ?? payload, meta)
);
return;
}
}
// ═══════════════════════════════════════════════════════════════════
// FALLBACK: TypeScript handlers
// ═══════════════════════════════════════════════════════════════════
const handler = this.handlers.get(command);
if (handler) {
// Use executeWithReply (throws on error) instead of executeWithReplySafe
// This allows errors to propagate to runLoop() for $exit signal
await RequestReplyHelper.executeWithReply(
this.context.transport,
payload,
(params) => handler(params ?? payload, meta)
);
return;
}
// No handler found - log warning
// eslint-disable-next-line no-console -- missing command handlers should remain visible in kernel logs
console.warn(`[ActorBehavior] No handler for command: ${command} at ${this.context.mount}`);
}
/**
* Type guard for BecomePayload.
*/
private isBecomePayload(payload: unknown): payload is BecomePayload<TAccepts> {
return (
payload !== null &&
typeof payload === 'object' &&
'handlers' in payload &&
typeof (payload as BecomePayload).handlers === 'object'
);
}
/**
* Handle system commands ($* prefix).
*
* System commands:
* - $close: Stop the actor
* - $ping: Health check
* - $introspect: Return handler info
* - $become: Change behavior at runtime
*/
private async handleSystemCommand(
command: string,
payload: unknown,
meta: MessageMeta
): Promise<void> {
switch (command) {
case '$close':
this.stop();
break;
case '$ping':
if (RequestReplyHelper.isEnvelope(payload)) {
RequestReplyHelper.sendSuccessReply(this.context.transport, payload, {
pong: true,
mount: this.context.mount,
ts: Date.now(),
});
}
break;
case '$introspect':
if (RequestReplyHelper.isEnvelope(payload)) {
RequestReplyHelper.sendSuccessReply(this.context.transport, payload, {
mount: this.context.mount,
handlers: this.listHandlers(),
strategies: this.strategies.map(s => ({ name: s.name, priority: s.priority })),
running: this.running,
});
}
break;
case '$become':
// Allow runtime behavior change via message
if (this.isBecomePayload(payload)) {
this.become(payload.handlers);
}
break;
default:
// eslint-disable-next-line no-console -- unknown system commands should remain visible in kernel logs
console.warn(`[ActorBehavior] Unknown system command: ${command}`);
}
}
/**
* Determine message direction for security verification.
*
* Vision 27: Direction determines trust level:
* - parent-to-child: TRUSTED (supervisor relationship)
* - child-to-parent: VERIFIED (requires capability)
* - sibling: VERIFIED (requires capability)
* - external: UNTRUSTED (requires capability + signature)
*
* Joe Armstrong (Erlang, "Programming Erlang"):
* "In Erlang, every message knows who sent it."
*
* @param from - Sender's mount path
* @returns Message direction for policy lookup
*/
private _determineDirection(from: string | undefined): MessageDirection {
if (!from) {
// Unknown sender = external (most restrictive)
return 'external';
}
const myMount = this.context.mount;
const parentMount = this.context.parent?.mount;
// Parent-to-child: Sender is our parent
if (parentMount && from === parentMount) {
return 'parent-to-child';
}
// Child-to-parent: Sender's mount starts with our mount
// e.g., myMount = "app.parent", from = "app.parent.child"
if (from.startsWith(myMount + '.')) {
return 'child-to-parent';
}
// Sibling: Same parent, different child
// e.g., parentMount = "app", myMount = "app.a", from = "app.b"
if (parentMount && from.startsWith(parentMount + '.') && from !== myMount) {
return 'sibling';
}
// Everything else is external
return 'external';
}
}

View File

@ -0,0 +1,52 @@
import type { AcceptsDefinition } from './AcceptsDefinition';
import type { IMessageMeta } from './IMessageMeta';
import type { IDispatchStrategy } from './IDispatchStrategy';
/** @deprecated Use IMessageMeta instead */
type MessageMeta = IMessageMeta;
/**
* Actor behavior interface.
*
* This is the composed behavior pattern - components don't extend ActorBehavior,
* they compose it via enableActorMode().
*/
export interface IActorBehavior<TAccepts extends AcceptsDefinition = AcceptsDefinition> {
/**
* Register a typed handler for a command.
*/
registerHandler<K extends keyof TAccepts & string>(command: K, handler: (payload: TAccepts[K], meta: MessageMeta) => Promise<unknown>): void;
/**
* Change all handlers atomically (Erlang's become).
*
* This replaces ALL handlers at once, enabling state machine patterns
* where the actor's entire behavior changes based on state.
*/
become(handlers: Partial<{
[K in keyof TAccepts]: (payload: TAccepts[K], meta: MessageMeta) => Promise<unknown>;
}>): void;
/**
* Start processing messages from the mailbox.
*/
start(): void;
/**
* Stop processing and close the mailbox.
*/
stop(): void;
/**
* Add a dispatch strategy for pluggable message routing.
*/
addDispatchStrategy(strategy: IDispatchStrategy): void;
/**
* Remove a dispatch strategy by name.
*/
removeDispatchStrategy(name: string): void;
/**
* Whether the actor is currently running.
*/
readonly isRunning: boolean;
/**
* The actor's mailbox address.
*/
readonly address: string;
}
export {};
//# sourceMappingURL=IActorBehavior.d.ts.map

View File

@ -0,0 +1 @@
{"version":3,"file":"IActorBehavior.d.ts","sourceRoot":"","sources":["IActorBehavior.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE7D,2CAA2C;AAC3C,KAAK,WAAW,GAAG,YAAY,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,WAAW,cAAc,CAAC,QAAQ,SAAS,iBAAiB,GAAG,iBAAiB;IACpF;;OAEG;IACH,eAAe,CAAC,CAAC,SAAS,MAAM,QAAQ,GAAG,MAAM,EAC/C,OAAO,EAAE,CAAC,EACV,OAAO,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,GACrE,IAAI,CAAC;IAER;;;;;OAKG;IACH,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC;SACtB,CAAC,IAAI,MAAM,QAAQ,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC;KACrF,CAAC,GAAG,IAAI,CAAC;IAEV;;OAEG;IACH,KAAK,IAAI,IAAI,CAAC;IAEd;;OAEG;IACH,IAAI,IAAI,IAAI,CAAC;IAEb;;OAEG;IACH,mBAAmB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAEvD;;OAEG;IACH,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAE3C;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAE5B;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B"}

Some files were not shown because too many files have changed in this diff Show More