352 lines
11 KiB
TypeScript
352 lines
11 KiB
TypeScript
import { randomUUID } from 'node:crypto';
|
|
import type {
|
|
AgentRuntimeProvider,
|
|
RuntimeAttachHandle,
|
|
RuntimeAttachMode,
|
|
RuntimeCapabilitySet,
|
|
RuntimeHealth,
|
|
RuntimeMessage,
|
|
RuntimeScope,
|
|
RuntimeSession,
|
|
RuntimeSessionTree,
|
|
RuntimeStreamEvent,
|
|
} from '@mosaicstack/types';
|
|
|
|
const MATRIX_PROVIDER_ID = 'runtime.matrix';
|
|
const ATTACHMENT_TTL_MS = 5 * 60 * 1_000;
|
|
|
|
/** A verified native runtime session. Matrix room and event details remain transport-local. */
|
|
export interface MatrixRuntimeSession {
|
|
id: string;
|
|
runtimeId: string;
|
|
parentSessionId?: string;
|
|
state: RuntimeSession['state'];
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
/**
|
|
* Narrow native Matrix boundary. The concrete Mosaic transport owns homeserver
|
|
* authentication, exact room mapping, Matrix identity checks, and replay cursors.
|
|
*/
|
|
export interface MatrixRuntimeTransport {
|
|
health(scope: RuntimeScope): Promise<RuntimeHealth>;
|
|
listSessions(scope: RuntimeScope): Promise<MatrixRuntimeSession[]>;
|
|
verifySession(sessionId: string, scope: RuntimeScope): Promise<MatrixRuntimeSession>;
|
|
stream(
|
|
sessionId: string,
|
|
cursor: string | undefined,
|
|
scope: RuntimeScope,
|
|
): AsyncIterable<RuntimeStreamEvent>;
|
|
send(sessionId: string, message: RuntimeMessage, scope: RuntimeScope): Promise<void>;
|
|
terminate(sessionId: string, approvalRef: string, scope: RuntimeScope): Promise<void>;
|
|
}
|
|
|
|
export type MatrixRuntimeProviderErrorCode =
|
|
| 'capability_unsupported'
|
|
| 'forbidden'
|
|
| 'invalid_request'
|
|
| 'not_found';
|
|
|
|
export class MatrixRuntimeProviderError extends Error {
|
|
constructor(
|
|
readonly code: MatrixRuntimeProviderErrorCode,
|
|
message: string,
|
|
) {
|
|
super(message);
|
|
this.name = MatrixRuntimeProviderError.name;
|
|
}
|
|
}
|
|
|
|
export type MatrixReadOperation =
|
|
| 'runtime.health'
|
|
| 'session.list'
|
|
| 'session.tree'
|
|
| 'session.stream'
|
|
| 'session.attach';
|
|
|
|
export interface MatrixReadAuthority {
|
|
canRead(input: {
|
|
operation: MatrixReadOperation;
|
|
scope: RuntimeScope;
|
|
sessionId?: string;
|
|
}): Promise<boolean>;
|
|
}
|
|
|
|
export interface MatrixWriteAuthority {
|
|
canWrite(input: {
|
|
operation: 'session.send' | 'session.terminate';
|
|
sessionId: string;
|
|
scope: RuntimeScope;
|
|
approvalRef?: string;
|
|
}): Promise<boolean>;
|
|
assertAuthorized(input: {
|
|
operation: 'session.send' | 'session.terminate';
|
|
sessionId: string;
|
|
scope: RuntimeScope;
|
|
approvalRef?: string;
|
|
}): Promise<void>;
|
|
}
|
|
|
|
export interface MatrixNativeRuntimeProviderOptions {
|
|
transport: MatrixRuntimeTransport;
|
|
readAuthority?: MatrixReadAuthority;
|
|
writeAuthority?: MatrixWriteAuthority;
|
|
attachmentIdFactory?: () => string;
|
|
now?: () => Date;
|
|
attachmentTtlMs?: number;
|
|
}
|
|
|
|
interface Attachment {
|
|
sessionId: string;
|
|
scope: RuntimeScope;
|
|
expiresAtMs: number;
|
|
}
|
|
|
|
class DenyMatrixReadAuthority implements MatrixReadAuthority {
|
|
async canRead(): Promise<boolean> {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
class DenyMatrixWriteAuthority implements MatrixWriteAuthority {
|
|
async canWrite(): Promise<boolean> {
|
|
return false;
|
|
}
|
|
|
|
async assertAuthorized(): Promise<void> {
|
|
throw new MatrixRuntimeProviderError(
|
|
'forbidden',
|
|
'Matrix runtime writes require orchestrator authority',
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Native Matrix adapter behind the Mosaic runtime contract. It accepts only
|
|
* stable session IDs; room identifiers, Matrix event schemas, and credentials
|
|
* are deliberately confined to the concrete transport implementation.
|
|
*/
|
|
export class MatrixNativeRuntimeProvider implements AgentRuntimeProvider {
|
|
readonly id = MATRIX_PROVIDER_ID;
|
|
private readonly readAuthority: MatrixReadAuthority;
|
|
private readonly writeAuthority: MatrixWriteAuthority;
|
|
private readonly attachmentIdFactory: () => string;
|
|
private readonly now: () => Date;
|
|
private readonly attachmentTtlMs: number;
|
|
private readonly attachments = new Map<string, Attachment>();
|
|
|
|
constructor(private readonly options: MatrixNativeRuntimeProviderOptions) {
|
|
this.readAuthority = options.readAuthority ?? new DenyMatrixReadAuthority();
|
|
this.writeAuthority = options.writeAuthority ?? new DenyMatrixWriteAuthority();
|
|
this.attachmentIdFactory = options.attachmentIdFactory ?? randomUUID;
|
|
this.now = options.now ?? (() => new Date());
|
|
this.attachmentTtlMs = options.attachmentTtlMs ?? ATTACHMENT_TTL_MS;
|
|
}
|
|
|
|
async capabilities(_scope: RuntimeScope): Promise<RuntimeCapabilitySet> {
|
|
return {
|
|
supported: [
|
|
'session.list',
|
|
'session.tree',
|
|
'session.stream',
|
|
'session.send',
|
|
'session.attach',
|
|
'session.terminate',
|
|
],
|
|
};
|
|
}
|
|
|
|
async health(scope: RuntimeScope): Promise<RuntimeHealth> {
|
|
await this.assertRead('runtime.health', undefined, scope);
|
|
return this.options.transport.health(scope);
|
|
}
|
|
|
|
async listSessions(scope: RuntimeScope): Promise<RuntimeSession[]> {
|
|
await this.assertRead('session.list', undefined, scope);
|
|
const sessions = await this.options.transport.listSessions(scope);
|
|
const visible = await Promise.all(
|
|
sessions.map((session) =>
|
|
this.readAuthority.canRead({ operation: 'session.list', sessionId: session.id, scope }),
|
|
),
|
|
);
|
|
return sessions
|
|
.filter((_session, index) => visible[index] === true)
|
|
.map((session) => this.runtimeSession(session));
|
|
}
|
|
|
|
async getSessionTree(scope: RuntimeScope): Promise<RuntimeSessionTree[]> {
|
|
await this.assertRead('session.tree', undefined, scope);
|
|
const sessions = await this.options.transport.listSessions(scope);
|
|
const visible = await Promise.all(
|
|
sessions.map((session) =>
|
|
this.readAuthority.canRead({ operation: 'session.tree', sessionId: session.id, scope }),
|
|
),
|
|
);
|
|
const runtimeSessions = sessions
|
|
.filter((_session, index) => visible[index] === true)
|
|
.map((session) => this.runtimeSession(session));
|
|
const nodes = new Map<string, RuntimeSessionTree>(
|
|
runtimeSessions.map((session) => [session.id, { session, children: [] }]),
|
|
);
|
|
const roots: RuntimeSessionTree[] = [];
|
|
for (const session of runtimeSessions) {
|
|
const node = nodes.get(session.id)!;
|
|
const parent = session.parentSessionId ? nodes.get(session.parentSessionId) : undefined;
|
|
if (parent) parent.children.push(node);
|
|
else roots.push(node);
|
|
}
|
|
return roots;
|
|
}
|
|
|
|
async *streamSession(
|
|
sessionId: string,
|
|
cursor: string | undefined,
|
|
scope: RuntimeScope,
|
|
): AsyncIterable<RuntimeStreamEvent> {
|
|
await this.assertRead('session.stream', sessionId, scope);
|
|
const session = await this.options.transport.verifySession(sessionId, scope);
|
|
await this.assertRead('session.stream', session.id, scope);
|
|
yield* this.options.transport.stream(session.id, cursor, scope);
|
|
}
|
|
|
|
async sendMessage(
|
|
sessionId: string,
|
|
message: RuntimeMessage,
|
|
scope: RuntimeScope,
|
|
): Promise<void> {
|
|
if (!message.content.trim()) {
|
|
throw new MatrixRuntimeProviderError(
|
|
'invalid_request',
|
|
'Matrix runtime message content is required',
|
|
);
|
|
}
|
|
await this.assertWritePermitted('session.send', sessionId, scope);
|
|
const session = await this.options.transport.verifySession(sessionId, scope);
|
|
await this.assertWriteAuthorized('session.send', session.id, scope);
|
|
await this.options.transport.send(session.id, message, scope);
|
|
}
|
|
|
|
async attach(
|
|
sessionId: string,
|
|
mode: RuntimeAttachMode,
|
|
scope: RuntimeScope,
|
|
): Promise<RuntimeAttachHandle> {
|
|
if (mode !== 'read') {
|
|
throw new MatrixRuntimeProviderError('forbidden', 'Matrix control attach is not permitted');
|
|
}
|
|
await this.assertRead('session.attach', sessionId, scope);
|
|
const session = await this.options.transport.verifySession(sessionId, scope);
|
|
await this.assertRead('session.attach', session.id, scope);
|
|
const nowMs = this.now().getTime();
|
|
this.pruneExpired(nowMs);
|
|
const attachmentId = this.attachmentIdFactory();
|
|
const expiresAtMs = nowMs + this.attachmentTtlMs;
|
|
this.attachments.set(attachmentId, {
|
|
sessionId: session.id,
|
|
scope: snapshotScope(scope),
|
|
expiresAtMs,
|
|
});
|
|
return {
|
|
attachmentId,
|
|
sessionId: session.id,
|
|
mode,
|
|
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
};
|
|
}
|
|
|
|
async detach(attachmentId: string, scope: RuntimeScope): Promise<void> {
|
|
const attachment = this.attachments.get(attachmentId);
|
|
if (!attachment)
|
|
throw new MatrixRuntimeProviderError('not_found', 'Matrix attachment is not active');
|
|
if (this.now().getTime() >= attachment.expiresAtMs) {
|
|
this.attachments.delete(attachmentId);
|
|
throw new MatrixRuntimeProviderError('forbidden', 'Matrix attachment has expired');
|
|
}
|
|
if (!sameScope(attachment.scope, scope)) {
|
|
throw new MatrixRuntimeProviderError('forbidden', 'Matrix attachment scope does not match');
|
|
}
|
|
this.attachments.delete(attachmentId);
|
|
}
|
|
|
|
async terminate(sessionId: string, approvalRef: string, scope: RuntimeScope): Promise<void> {
|
|
if (!approvalRef.trim()) {
|
|
throw new MatrixRuntimeProviderError(
|
|
'invalid_request',
|
|
'Matrix termination approval is required',
|
|
);
|
|
}
|
|
await this.assertWritePermitted('session.terminate', sessionId, scope, approvalRef);
|
|
const session = await this.options.transport.verifySession(sessionId, scope);
|
|
await this.assertWriteAuthorized('session.terminate', session.id, scope, approvalRef);
|
|
await this.options.transport.terminate(session.id, approvalRef, scope);
|
|
}
|
|
|
|
private runtimeSession(session: MatrixRuntimeSession): RuntimeSession {
|
|
return { ...session, providerId: this.id };
|
|
}
|
|
|
|
private async assertRead(
|
|
operation: MatrixReadOperation,
|
|
sessionId: string | undefined,
|
|
scope: RuntimeScope,
|
|
): Promise<void> {
|
|
const allowed = await this.readAuthority.canRead({
|
|
operation,
|
|
scope,
|
|
...(sessionId ? { sessionId } : {}),
|
|
});
|
|
if (!allowed)
|
|
throw new MatrixRuntimeProviderError('forbidden', 'Matrix runtime read is not authorized');
|
|
}
|
|
|
|
private async assertWritePermitted(
|
|
operation: 'session.send' | 'session.terminate',
|
|
sessionId: string,
|
|
scope: RuntimeScope,
|
|
approvalRef?: string,
|
|
): Promise<void> {
|
|
const allowed = await this.writeAuthority.canWrite({
|
|
operation,
|
|
sessionId,
|
|
scope,
|
|
...(approvalRef ? { approvalRef } : {}),
|
|
});
|
|
if (!allowed)
|
|
throw new MatrixRuntimeProviderError('forbidden', 'Matrix runtime write is not authorized');
|
|
}
|
|
|
|
private async assertWriteAuthorized(
|
|
operation: 'session.send' | 'session.terminate',
|
|
sessionId: string,
|
|
scope: RuntimeScope,
|
|
approvalRef?: string,
|
|
): Promise<void> {
|
|
await this.writeAuthority.assertAuthorized({
|
|
operation,
|
|
sessionId,
|
|
scope,
|
|
...(approvalRef ? { approvalRef } : {}),
|
|
});
|
|
}
|
|
|
|
private pruneExpired(nowMs: number): void {
|
|
for (const [id, attachment] of this.attachments) {
|
|
if (attachment.expiresAtMs <= nowMs) this.attachments.delete(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
function snapshotScope(scope: RuntimeScope): RuntimeScope {
|
|
return Object.freeze({ ...scope });
|
|
}
|
|
|
|
function sameScope(left: RuntimeScope, right: RuntimeScope): boolean {
|
|
return (
|
|
left.actorId === right.actorId &&
|
|
left.tenantId === right.tenantId &&
|
|
left.channelId === right.channelId &&
|
|
left.correlationId === right.correlationId
|
|
);
|
|
}
|