import { randomUUID } from 'node:crypto'; import { IsNotEmpty, IsString } from 'class-validator'; import type { HarnessActorContext, HarnessAuthState, HarnessCapability, HarnessCatalog, HarnessCatalogEntry, HarnessDescriptor, HarnessInputType, HarnessModelAvailability, HarnessSelection, } from '@mosaicstack/types'; import type { ActorTenantScope } from '../auth/session-scope.js'; /** * Structured selection tuple accepted on `PUT /api/chat/preferences/selection`. * * The body is a STRUCTURED tuple (harness + provider + model), never a free-text * model string. With `ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })` * any extra property — including smuggled server-authority fields such as * `seatId`, `tenantId`, `userId`, `nativeSessionPath`, `executable`, `home`, `cwd` — * is rejected with 400. There is deliberately no field through which a caller can * name a scope; scope is derived on the server from the authenticated session. */ export class HarnessSelectionInputDto { @IsString() @IsNotEmpty() harnessId!: string; @IsString() @IsNotEmpty() providerId!: string; @IsString() @IsNotEmpty() modelId!: string; } /** Browser-safe harness summary — identity and capabilities only. */ export interface HarnessSummaryDto { readonly id: string; readonly displayName: string; readonly capabilities: readonly HarnessCapability[]; } /** Browser-safe catalog entry — no executables, paths, secrets, or env. */ export interface HarnessCatalogEntryDto { readonly harnessId: string; readonly providerId: string; readonly modelId: string; readonly displayName: string; readonly reasoningCapability: boolean; readonly inputTypes: readonly HarnessInputType[]; readonly authState: HarnessAuthState; readonly availability: HarnessModelAvailability; } /** Browser-safe catalog envelope. */ export interface HarnessCatalogDto { readonly harnessId: string; readonly version: string; readonly fingerprint: string; readonly models: readonly HarnessCatalogEntryDto[]; } /** Response envelope for the caller's current selection (null when unset). */ export interface SelectionResponseDto { readonly selection: HarnessSelection | null; } /** * Derive a server-trusted {@link HarnessActorContext} for read operations from the * session-derived {@link ActorTenantScope}. All authority originates on the server; * nothing here is caller-supplied. A fresh correlation id is minted per call. */ export function readContextFromScope(scope: ActorTenantScope): HarnessActorContext { return { actorId: scope.userId, tenantId: scope.tenantId, seatId: scope.userId, correlationId: randomUUID(), }; } /** Project a descriptor onto the browser-safe summary shape (whitelist by construction). */ export function toHarnessSummary(descriptor: HarnessDescriptor): HarnessSummaryDto { return { id: descriptor.id, displayName: descriptor.displayName, capabilities: [...descriptor.capabilities], }; } /** Project a catalog onto the browser-safe shape (whitelist by construction). */ export function toSafeCatalog(catalog: HarnessCatalog): HarnessCatalogDto { return { harnessId: catalog.harnessId, version: catalog.version, fingerprint: catalog.fingerprint, models: catalog.models.map(toSafeCatalogEntry), }; } function toSafeCatalogEntry(entry: HarnessCatalogEntry): HarnessCatalogEntryDto { return { harnessId: entry.harnessId, providerId: entry.providerId, modelId: entry.modelId, displayName: entry.displayName, reasoningCapability: entry.reasoningCapability, inputTypes: [...entry.inputTypes], authState: entry.authState, availability: entry.availability, }; }