feat(gateway): generic harness catalog + selection HTTP surfaces (P3 Slice Zero, Task 3) #1169

Merged
jason.woltje merged 1 commits from feat/p3-slice0-task3-catalog-selection into next 2026-08-12 01:11:22 +00:00
Owner

P3 Slice Zero, Task 3 — generic harness catalog + selection HTTP surfaces

Adds the authenticated Gateway HTTP surfaces for the generic harness layer: list harnesses, read a harness catalog, and get/set the caller's provider/model selection — all server-scoped, structured-tuple only, no substitution. Builds on the Task-1 contract types (@mosaicstack/types, #1166) and the Task-2 registry/service/fake-adapter (#1168). This is the first real Nest module wiring in the harness feature (HarnessModule + app.module.ts). No real adapter, no Pi wiring, no Web changes — those are later Slice Zero tasks.

Traceability

  • Approved design: jarvis-brain docs/plans/2026-08-11-p3-pi-first-harness-adapter-design.md.
  • Approved implementation plan: jarvis-brain docs/plans/2026-08-11-p3-pi-first-harness-adapter-implementation.md, § Task Three.
  • Base: branched off next @ a186922e (SLICE_ZERO_TASK3_BASE_SHA = the Task-2 merge), unchanged at PR time.

Surfaces (all @UseGuards(AuthGuard), scope from scopeFromUser(CurrentUser))

  • GET /api/harnesses — registered harnesses, safe fields only.
  • GET /api/harnesses/:harnessId/catalog — provider/model catalog; unknown harness → typed adapter_unavailable (404), never a fallback catalog.
  • GET /api/chat/preferences/selection — the caller's current selection (server-scoped; a query naming another user/tenant/seat has no effect).
  • PUT /api/chat/preferences/selection — set selection from a structured {harnessId, providerId, modelId} tuple (nested class-validator), not free text.

Files (9, path-fenced — 8 created under apps/gateway/src/harness/ + app.module.ts modified)

harness.dto.ts, harness.controller.ts (+ .spec.ts), harness-selection.repository.ts, harness-selection.service.ts, harness-selection.controller.ts (+ .spec.ts), harness.module.ts, and app.module.ts (single +HarnessModule import + registration).

Design conformance (independently reviewed)

  • Server-derived scope: every handler derives scope from scopeFromUser(CurrentUser); no userId/tenantId/seat is ever read from body or query. Fails closed (AuthGuard throws on no session).
  • No substitution / no fallback: exact-triple match; unknown tuple → selection_invalid, known-but-unavailable → model_unavailable; the error DTO echoes the requested tuple unchanged — no divergent "effective selection" field, no first-catalog-row / first-provider fallback. The store is untouched on any error.
  • Total DTO validation: structured tuple only; free text and extra authority-bearing fields (seatId/tenantId/userId/nativeSessionPath/executable/home/cwd) rejected 400 by the production global ValidationPipe({whitelist, forbidNonWhitelisted, transform}) (main.ts), distinct from the 422 typed-domain errors.
  • Safe egress: toHarnessSummary/toSafeCatalog/toSafeCatalogEntry are allow-list mappers (field-by-field, no source spread); responses carry no executable/home/cwd/nativeSessionPath/env/secret/token/apiKey.
  • Transitional repository: in-memory, non-durable, owner-scoped — keyed strictly by server-derived (userId, tenantId) (NUL-separated to prevent key-collision); documented as replaced by the durable store in Task Seven.
  • No legacy coupling: no /api/providers or /api/providers/test call; no import of IProviderAdapter/AgentRuntimeProvider/packages/types/src/agent/**.
  • Nest DI (first module wiring — #1145 class): HarnessModule provides every injected token; exports limited to HARNESS_REGISTRY/HARNESS_SERVICE (repository + selection service stay internal); no duplicate/circular/globally-leaked provider; app.module.ts imports it once.

Independent gates (author = pi ≠ integrator-verifier ≠ code/security reviewer)

  • Integrator-verify: PASS — lineage + 9-file path-fence + scope-fence; focused 16/16; full @mosaicstack/gateway suite 730 passed / 17 skipped / 0 failed; typecheck rc 0; build rc 0. Anti-masking — all THREE genuine (working-tree-only, restored): (a) forcing a first-catalog-row substitution turns the selection_invalid-unchanged test RED (422→200); (b) deriving scope from @Query turns the server-scoped-GET test RED; (c) removing HarnessSelectionService from harness.module.ts fails both boot suites at app.init() with a genuine Nest unresolved-dependency error — the #1145-class DI guard is real, not decorative.
  • Code + security review: PASS — no blocker/major; fails closed; safe stable error DTOs (tuple preserved exactly); allow-list egress; error codes reuse the @mosaicstack/types HarnessErrorCode union (satisfies); no prod any; no secrets; DI wiring correct/minimal.
  • Red-first: both new spec suites failed to load at base (modules/controllers/repository absent) — the honest red for from-scratch modules.

Reviewer/verifier env note (for the merge-gate)

A fresh worktree needs the gateway's gitignored workspace-dep dist/ built first:
corepack pnpm --filter "@mosaicstack/gateway^..." build. An initial Failed to resolve entry for package "@mosaicstack/auth" is that missing dist, not harness behavior.

Pre-push hook (typecheck/lint/format) passed on push. Merge-gate re-derivation to follow before merge. Disjoint from merged #1166/#1167/#1168. Refs P3 Slice Zero Task 3.

## P3 Slice Zero, Task 3 — generic harness catalog + selection HTTP surfaces Adds the authenticated Gateway HTTP surfaces for the generic harness layer: list harnesses, read a harness catalog, and get/set the caller's provider/model **selection** — all server-scoped, structured-tuple only, no substitution. Builds on the Task-1 contract types (`@mosaicstack/types`, #1166) and the Task-2 registry/service/fake-adapter (#1168). This is the **first real Nest module wiring** in the harness feature (`HarnessModule` + `app.module.ts`). No real adapter, no Pi wiring, no Web changes — those are later Slice Zero tasks. ### Traceability - **Approved design:** jarvis-brain `docs/plans/2026-08-11-p3-pi-first-harness-adapter-design.md`. - **Approved implementation plan:** jarvis-brain `docs/plans/2026-08-11-p3-pi-first-harness-adapter-implementation.md`, **§ Task Three**. - **Base:** branched off `next` @ `a186922e` (`SLICE_ZERO_TASK3_BASE_SHA` = the Task-2 merge), unchanged at PR time. ### Surfaces (all `@UseGuards(AuthGuard)`, scope from `scopeFromUser(CurrentUser)`) - `GET /api/harnesses` — registered harnesses, safe fields only. - `GET /api/harnesses/:harnessId/catalog` — provider/model catalog; unknown harness → typed `adapter_unavailable` (404), never a fallback catalog. - `GET /api/chat/preferences/selection` — the caller's current selection (server-scoped; a query naming another user/tenant/seat has no effect). - `PUT /api/chat/preferences/selection` — set selection from a **structured `{harnessId, providerId, modelId}` tuple** (nested class-validator), not free text. ### Files (9, path-fenced — 8 created under `apps/gateway/src/harness/` + `app.module.ts` modified) `harness.dto.ts`, `harness.controller.ts` (+ `.spec.ts`), `harness-selection.repository.ts`, `harness-selection.service.ts`, `harness-selection.controller.ts` (+ `.spec.ts`), `harness.module.ts`, and `app.module.ts` (single `+HarnessModule` import + registration). ### Design conformance (independently reviewed) - **Server-derived scope:** every handler derives scope from `scopeFromUser(CurrentUser)`; no userId/tenantId/seat is ever read from body or query. Fails closed (`AuthGuard` throws on no session). - **No substitution / no fallback:** exact-triple match; unknown tuple → `selection_invalid`, known-but-unavailable → `model_unavailable`; the error DTO echoes the **requested tuple unchanged** — no divergent "effective selection" field, no first-catalog-row / first-provider fallback. The store is untouched on any error. - **Total DTO validation:** structured tuple only; free text and extra authority-bearing fields (`seatId`/`tenantId`/`userId`/`nativeSessionPath`/`executable`/`home`/`cwd`) rejected **400** by the production global `ValidationPipe({whitelist, forbidNonWhitelisted, transform})` (`main.ts`), distinct from the **422** typed-domain errors. - **Safe egress:** `toHarnessSummary`/`toSafeCatalog`/`toSafeCatalogEntry` are allow-list mappers (field-by-field, no source spread); responses carry no executable/home/cwd/nativeSessionPath/env/secret/token/apiKey. - **Transitional repository:** in-memory, non-durable, owner-scoped — keyed strictly by server-derived `(userId, tenantId)` (NUL-separated to prevent key-collision); documented as replaced by the durable store in Task Seven. - **No legacy coupling:** no `/api/providers` or `/api/providers/test` call; no import of `IProviderAdapter`/`AgentRuntimeProvider`/`packages/types/src/agent/**`. - **Nest DI (first module wiring — #1145 class):** `HarnessModule` provides every injected token; exports limited to `HARNESS_REGISTRY`/`HARNESS_SERVICE` (repository + selection service stay internal); no duplicate/circular/globally-leaked provider; `app.module.ts` imports it once. ### Independent gates (author = pi ≠ integrator-verifier ≠ code/security reviewer) - **Integrator-verify:** PASS — lineage + 9-file path-fence + scope-fence; focused 16/16; **full `@mosaicstack/gateway` suite 730 passed / 17 skipped / 0 failed**; typecheck rc 0; build rc 0. **Anti-masking — all THREE genuine (working-tree-only, restored):** (a) forcing a first-catalog-row substitution turns the `selection_invalid`-unchanged test RED (422→200); (b) deriving scope from `@Query` turns the server-scoped-GET test RED; (c) **removing `HarnessSelectionService` from `harness.module.ts` fails both boot suites at `app.init()`** with a genuine Nest unresolved-dependency error — the #1145-class DI guard is real, not decorative. - **Code + security review:** PASS — no blocker/major; fails closed; safe stable error DTOs (tuple preserved exactly); allow-list egress; error codes reuse the `@mosaicstack/types` `HarnessErrorCode` union (`satisfies`); no prod `any`; no secrets; DI wiring correct/minimal. - **Red-first:** both new spec suites failed to load at base (modules/controllers/repository absent) — the honest red for from-scratch modules. ### Reviewer/verifier env note (for the merge-gate) A fresh worktree needs the gateway's gitignored workspace-dep `dist/` built first: `corepack pnpm --filter "@mosaicstack/gateway^..." build`. An initial `Failed to resolve entry for package "@mosaicstack/auth"` is that missing dist, **not** harness behavior. Pre-push hook (typecheck/lint/format) passed on push. Merge-gate re-derivation to follow before merge. Disjoint from merged #1166/#1167/#1168. `Refs P3 Slice Zero Task 3`.
jason.woltje added 1 commit 2026-08-12 01:08:31 +00:00
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
jason.woltje merged commit ebe415132e into next 2026-08-12 01:11:22 +00:00
Sign in to join this conversation.