import type { HermesLegacySession, HermesRuntimeTransport } from '@mosaicstack/agent'; import type { RuntimeAttachHandle, RuntimeAttachMode, RuntimeMessage, RuntimeScope, RuntimeStreamEvent, } from '@mosaicstack/types'; /** Concrete HTTP transport for a configured legacy Hermes runtime endpoint. */ export class GatewayHermesRuntimeTransport implements HermesRuntimeTransport { constructor( private readonly baseUrl = process.env['MOSAIC_HERMES_RUNTIME_URL']?.trim(), private readonly serviceToken = process.env['MOSAIC_HERMES_RUNTIME_TOKEN']?.trim(), private readonly fetchFn: typeof fetch = fetch, ) {} async capabilities(scope: RuntimeScope): Promise { return this.request('/capabilities', scope); } async health(scope: RuntimeScope): Promise<{ status: string; detail?: string }> { return this.request<{ status: string; detail?: string }>('/health', scope); } async sessions(scope: RuntimeScope): Promise { return this.request('/sessions', scope); } async *stream( sessionId: string, cursor: string | undefined, scope: RuntimeScope, ): AsyncIterable { const params = new URLSearchParams(cursor ? { cursor } : {}); const events = await this.request( `/sessions/${encodeURIComponent(sessionId)}/stream?${params.toString()}`, scope, ); yield* events; } async send(sessionId: string, message: RuntimeMessage, scope: RuntimeScope): Promise { await this.request(`/sessions/${encodeURIComponent(sessionId)}/messages`, scope, { method: 'POST', body: message, }); } async attach( sessionId: string, mode: RuntimeAttachMode, scope: RuntimeScope, ): Promise { return this.request( `/sessions/${encodeURIComponent(sessionId)}/attach`, scope, { method: 'POST', body: { mode }, }, ); } async detach(attachmentId: string, scope: RuntimeScope): Promise { await this.request(`/attachments/${encodeURIComponent(attachmentId)}`, scope, { method: 'DELETE', }); } async terminate(sessionId: string, approvalRef: string, scope: RuntimeScope): Promise { await this.request(`/sessions/${encodeURIComponent(sessionId)}/terminate`, scope, { method: 'POST', body: { approvalRef }, }); } private async request( path: string, scope: RuntimeScope, init: { method?: string; body?: unknown } = {}, ): Promise { if (!this.baseUrl || !this.serviceToken) { throw new Error( 'MOSAIC_HERMES_RUNTIME_URL and MOSAIC_HERMES_RUNTIME_TOKEN must configure Hermes transport', ); } const endpoint = new URL(this.baseUrl); if (endpoint.protocol !== 'https:' && !isLoopbackHttp(endpoint)) { throw new Error('Hermes runtime transport requires HTTPS outside loopback'); } const response = await this.fetchFn( new URL(path.replace(/^\//, ''), `${endpoint.toString().replace(/\/$/, '')}/`), { method: init.method ?? 'GET', headers: { accept: 'application/json', authorization: `Bearer ${this.serviceToken}`, 'x-mosaic-actor-id': scope.actorId, 'x-mosaic-tenant-id': scope.tenantId, 'x-mosaic-channel-id': scope.channelId, 'x-correlation-id': scope.correlationId, ...(init.body ? { 'content-type': 'application/json' } : {}), }, ...(init.body ? { body: JSON.stringify(init.body) } : {}), }, ); if (!response.ok) throw new Error(`Hermes runtime request failed: ${response.status}`); if (response.status === 204) return undefined as T; return (await response.json()) as T; } } function isLoopbackHttp(endpoint: URL): boolean { return ( endpoint.protocol === 'http:' && (endpoint.hostname === 'localhost' || endpoint.hostname === '127.0.0.1' || endpoint.hostname === '::1') ); }