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.
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/typesHarnessErrorCode 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`.
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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
docs/plans/2026-08-11-p3-pi-first-harness-adapter-design.md.docs/plans/2026-08-11-p3-pi-first-harness-adapter-implementation.md, § Task Three.next@a186922e(SLICE_ZERO_TASK3_BASE_SHA= the Task-2 merge), unchanged at PR time.Surfaces (all
@UseGuards(AuthGuard), scope fromscopeFromUser(CurrentUser))GET /api/harnesses— registered harnesses, safe fields only.GET /api/harnesses/:harnessId/catalog— provider/model catalog; unknown harness → typedadapter_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.tsmodified)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, andapp.module.ts(single+HarnessModuleimport + registration).Design conformance (independently reviewed)
scopeFromUser(CurrentUser); no userId/tenantId/seat is ever read from body or query. Fails closed (AuthGuardthrows on no session).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.seatId/tenantId/userId/nativeSessionPath/executable/home/cwd) rejected 400 by the production globalValidationPipe({whitelist, forbidNonWhitelisted, transform})(main.ts), distinct from the 422 typed-domain errors.toHarnessSummary/toSafeCatalog/toSafeCatalogEntryare allow-list mappers (field-by-field, no source spread); responses carry no executable/home/cwd/nativeSessionPath/env/secret/token/apiKey.(userId, tenantId)(NUL-separated to prevent key-collision); documented as replaced by the durable store in Task Seven./api/providersor/api/providers/testcall; no import ofIProviderAdapter/AgentRuntimeProvider/packages/types/src/agent/**.HarnessModuleprovides every injected token; exports limited toHARNESS_REGISTRY/HARNESS_SERVICE(repository + selection service stay internal); no duplicate/circular/globally-leaked provider;app.module.tsimports it once.Independent gates (author = pi ≠ integrator-verifier ≠ code/security reviewer)
@mosaicstack/gatewaysuite 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 theselection_invalid-unchanged test RED (422→200); (b) deriving scope from@Queryturns the server-scoped-GET test RED; (c) removingHarnessSelectionServicefromharness.module.tsfails both boot suites atapp.init()with a genuine Nest unresolved-dependency error — the #1145-class DI guard is real, not decorative.@mosaicstack/typesHarnessErrorCodeunion (satisfies); no prodany; no secrets; DI wiring correct/minimal.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 initialFailed 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.