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
47 lines
1.9 KiB
TypeScript
47 lines
1.9 KiB
TypeScript
import { Body, Controller, Get, HttpException, HttpStatus, Put, UseGuards } from '@nestjs/common';
|
|
import { AuthGuard } from '../auth/auth.guard.js';
|
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
|
import { scopeFromUser, type AuthenticatedUserLike } from '../auth/session-scope.js';
|
|
import { HarnessOperationError } from './harness.registry.js';
|
|
import { HarnessSelectionService } from './harness-selection.service.js';
|
|
import { HarnessSelectionInputDto, type SelectionResponseDto } from './harness.dto.js';
|
|
|
|
/**
|
|
* Chat-preferences selection surface. The scope is ALWAYS derived on the server
|
|
* from the authenticated user (`scopeFromUser(CurrentUser)`); the request body and
|
|
* query string can never name another user, tenant, or seat. A typed selection
|
|
* failure (unknown tuple → `selection_invalid`, known-but-unavailable →
|
|
* `model_unavailable`) is returned as 422 with the requested tuple echoed back
|
|
* unchanged, and never mutates the stored selection.
|
|
*/
|
|
@Controller('api/chat/preferences/selection')
|
|
@UseGuards(AuthGuard)
|
|
export class HarnessSelectionController {
|
|
constructor(private readonly selection: HarnessSelectionService) {}
|
|
|
|
@Get()
|
|
get(@CurrentUser() user: AuthenticatedUserLike): SelectionResponseDto {
|
|
return { selection: this.selection.getSelection(scopeFromUser(user)) };
|
|
}
|
|
|
|
@Put()
|
|
async put(
|
|
@CurrentUser() user: AuthenticatedUserLike,
|
|
@Body() dto: HarnessSelectionInputDto,
|
|
): Promise<SelectionResponseDto> {
|
|
try {
|
|
const stored = await this.selection.setSelection(scopeFromUser(user), {
|
|
harnessId: dto.harnessId,
|
|
providerId: dto.providerId,
|
|
modelId: dto.modelId,
|
|
});
|
|
return { selection: stored };
|
|
} catch (error) {
|
|
if (error instanceof HarnessOperationError) {
|
|
throw new HttpException(error.dto, HttpStatus.UNPROCESSABLE_ENTITY);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
}
|