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
66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
import {
|
|
Controller,
|
|
Get,
|
|
HttpException,
|
|
HttpStatus,
|
|
Inject,
|
|
Param,
|
|
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 { HarnessAdapterUnavailableError, HarnessRegistry } from './harness.registry.js';
|
|
import { HARNESS_REGISTRY } from './harness.tokens.js';
|
|
import {
|
|
readContextFromScope,
|
|
toHarnessSummary,
|
|
toSafeCatalog,
|
|
type HarnessCatalogDto,
|
|
type HarnessSummaryDto,
|
|
} from './harness.dto.js';
|
|
|
|
/**
|
|
* Generic harness catalog surface. It exposes only harness-neutral, browser-safe
|
|
* fields (identity, capabilities, provider/model catalog) — never executables,
|
|
* native paths, home/cwd, env, or secrets. There is NO provider-probe route here;
|
|
* `/api/providers` and `POST /api/providers/test` are intentionally out of scope.
|
|
*/
|
|
@Controller('api/harnesses')
|
|
@UseGuards(AuthGuard)
|
|
export class HarnessController {
|
|
constructor(@Inject(HARNESS_REGISTRY) private readonly registry: HarnessRegistry) {}
|
|
|
|
@Get()
|
|
async list(@CurrentUser() user: AuthenticatedUserLike): Promise<HarnessSummaryDto[]> {
|
|
const context = readContextFromScope(scopeFromUser(user));
|
|
const summaries: HarnessSummaryDto[] = [];
|
|
for (const adapter of this.registry.list()) {
|
|
summaries.push(toHarnessSummary(await adapter.describe(context)));
|
|
}
|
|
return summaries;
|
|
}
|
|
|
|
@Get(':harnessId/catalog')
|
|
async catalog(
|
|
@CurrentUser() user: AuthenticatedUserLike,
|
|
@Param('harnessId') harnessId: string,
|
|
): Promise<HarnessCatalogDto> {
|
|
const context = readContextFromScope(scopeFromUser(user));
|
|
let adapter;
|
|
try {
|
|
adapter = this.registry.get(harnessId);
|
|
} catch (error) {
|
|
if (error instanceof HarnessAdapterUnavailableError) {
|
|
// Typed failure — NEVER a fallback catalog for an unknown harness id.
|
|
throw new HttpException(
|
|
{ code: error.code, message: error.message, harnessId },
|
|
HttpStatus.NOT_FOUND,
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
return toSafeCatalog(await adapter.catalog(context));
|
|
}
|
|
}
|