ci/woodpecker/pr/ci Pipeline failed
Add the Slice-Zero catalog and selection HTTP surfaces for P3 Task 3: GET /api/harnesses, GET /api/harnesses/:harnessId/catalog, GET+PUT /api/chat/preferences/selection. Scope is always server-derived via scopeFromUser(CurrentUser); selection tuples are validated against the live catalog with no fallback substitution and persisted in a transitional owner-scoped in-memory store. HarnessModule is wired into AppModule. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01ESFAnh2t9HmLwng8oW95St
117 lines
3.7 KiB
TypeScript
117 lines
3.7 KiB
TypeScript
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,
|
|
};
|
|
}
|