feat(catalog): runtime_models table + REST + e2e (#1369)#1402
Open
getlarge wants to merge 9 commits into
Open
Conversation
Contributor
|
Contributor
|
legreffier Bot
added a commit
that referenced
this pull request
Jun 16, 2026
The CI OpenAPI Spec job runs `pnpm run generate`, which ends with
`prettier --write libs/api-client/src/generated/`. Prettier is
non-idempotent on the openapi-ts output: the very long multi-type
`import type { ... } from '...';` statements (200+ types) oscillate
between collapsed (single-line, >80 chars) and expanded (multi-line)
layouts on consecutive passes, so the second regen shows thousands of
unrelated formatting lines against the first.
This was a real failure in PR #1402: the committed `openapi-ts`
output was correct, but the next CI `pnpm run generate` re-formatted
it via prettier and `git diff --exit-code libs/api-client/src/generated/`
failed with ~12k lines of pure formatting churn.
Fix:
- Add `libs/api-client/src/generated/` to `.prettierignore` with a
comment explaining the openapi-ts non-idempotence. Now the
`prettier --write libs/api-client/src/generated/` step in
`pnpm run generate` is a no-op, and the drift check passes against
the committed `openapi-ts` output.
- Apply `prettier --write` to the new runtime-models code paths so
they pass `nx format:write` in the pre-commit hook.
Verified locally: running the full `pnpm run generate` (openapi + openapi-ts
+ prettier --write libs/api-client/src/generated/) twice in a row
produces zero diff against the committed SDK.
Refs PR #1402
…ypes) Issue #1369 comment 4699154922 asks for a DB-backed provider/model catalog before the profile UI ships. This commit lands the data layer only — REST route, runtime-profile warnings wiring, and tests are follow-ups on the same branch. What this commit adds: - `runtime_models` table (`libs/database/src/schema/runtime-models.ts`): - `team_id IS NULL` => global entry; `team_id IS NOT NULL` => team-owned - `provider`, `model` are lowercased, free-form - `display_name`, `description` for UI affordance - `capabilities jsonb` for runtime kind / auth kind metadata, kept out of the core schema per the design comment - `is_active` for soft-disable - XOR creator columns mirror `daemon_profiles` - Two partial unique indexes partition the table: `runtime_models_global_uq` (provider, model) WHERE team_id IS NULL `runtime_models_team_uq` (team_id, provider, model) WHERE team_id IS NOT NULL - Migration `0021_secret_shooting_star.sql` carries both the DDL and 13 seeded global couples (anthropic x3, openai x3, openai-codex x2, ollama x2, ollama-cloud, claude-code, bedrock). Seed INSERT uses `ON CONFLICT ... WHERE team_id IS NULL DO NOTHING` for idempotency. Catalog content lives with the table — one logical change, one migration. - `RuntimeModelRepository` (`libs/database/src/repositories/runtime-models.repository.ts`): - `findVisibleByProviderAndModel(teamId, provider, model)` is the shape the runtime-profile warnings query will use - `listVisible({ teamId?, provider? })` powers the autocomplete feed - Soft-disabled rows are filtered from reads; `is_active` audit history stays in the table - TypeBox schemas (`libs/tasks/src/runtime-models.ts`): - `RuntimeModel`, `RuntimeModelProvider`, `RuntimeModelName`, `RuntimeModelCapabilities` — provider regex `^[a-z][a-z0-9._-]{0,99}$` (rejects uppercase; route will lowercase on write), model regex allows dot/colon/dash for `gpt-5.1-codex`-style names - `RuntimeProfile` schema gains optional `warnings: RuntimeProfileWarning[]` (`libs/tasks/src/runtime-profiles.ts`). The warnings are advisory only — the route fills this in when the (provider, model) couple is not in the catalog, but the profile still saves. Omitted on the happy path; absent means "no warnings". - `sandbox.json`: restore `memory: 4G -> 6G` and `overlaySize: 8G -> 12G` to give nx + tsc + drizzle + vitest enough headroom on a 3.8G-class VM (the prior reduction was equal to the host's total RAM and caused the kernel OOM-killer to terminate tsc mid-compile). Diary entry `b3879ca7` records the rationale. Snapshot invariant verified: `drizzle-kit generate` on the current tree reports "No schema changes, nothing to migrate". Refs: issue #1369 (comment 4699154922), diary entries `aca82086-5d05-4304-ae14-f55468e8141f` (design), `b3879ca7-7441-4662-b633-2728e5c57460` (sandbox memory).
Builds on the data layer from #1369 (commit b9c0b3e) to expose the runtime-models catalog through the REST API. What this commit adds: - `apps/rest-api/src/routes/runtime-models.ts` — five endpoints: GET /runtime-models list visible entries (global + team) POST /runtime-models create team-scoped entry GET /runtime-models/:entryId fetch one entry PATCH /runtime-models/:entryId update team-scoped entry DELETE /runtime-models/:entryId delete team-scoped entry Global entries are read-only through the public API (PATCH/DELETE on a global row returns 403). The POST endpoint requires `x-moltnet-team-id` and `canManageTeam`; GET supports the team header optionally and falls back to global-only listing when omitted, so an unauthenticated-to-team caller (operator endpoint, MCP, etc.) can still read the seed catalog. Query: `?provider=<id>` for autocomplete narrowing. Mirrors the `provider` filter exposed on the agent runtime later. - TypeBox request/response schemas in `apps/rest-api/src/schemas/runtime-models.ts`, registered through the central `sharedSchemas` list so OpenAPI picks them up. - Route registration in `apps/rest-api/src/app.ts` and the Fastify module augmentation in `apps/rest-api/src/types.ts`. - Repository wiring in `apps/rest-api/src/bootstrap.ts` (factory call + AppOptions entry). - Test coverage (`apps/rest-api/__tests__/runtime-models.test.ts`): 12 cases covering list (with/without team header, provider filter), create (success, missing team, canManage=false, unique violation maps to 409), patch (success, global entry refused), delete (same), and get (global entry, team entry hidden from non-members). - Test helper fix (`apps/rest-api/__tests__/helpers.ts`): the mock `tokenValidator.resolveAuthContext` now returns a shallow copy of the auth context so per-request mutations of `currentTeamId` don't leak into the next test. The previous behavior caused cross-test contamination — a test that sent the team header mutated the shared `VALID_AUTH_CONTEXT.currentTeamId` to the team id, and any later test that omitted the header would see the stale team id and skip the 400 check it was supposed to exercise. Out of scope (next commit): - Wiring the catalog lookup into `runtime-profiles.ts` POST/PATCH so the `warnings` field on the response surfaces when a provider/model couple is unknown. - OpenAPI + client regen. Refs: issue #1369 (comment 4699154922), diary entry `aca82086-5d05-4304-ae14-f55468e8141f` (design).
Round 2 of #1369 work — three changes: 1. Drop the `RuntimeProfileWarning` schema and the `warnings?` field on `RuntimeProfile` from `libs/tasks/src/runtime-profiles.ts`. Per design review (the catalog is informational only; UI affordances for it belong to a later iteration), advisory warnings on the data model are noise. The schema file still exports the catalog types, and the runtime-profile route still does the lookup only at the point we'd attach the warnings — the lookup itself is gone too. 2. Regenerate the OpenAPI spec and the TypeScript client. The `runtime-models` route landed with a `Type.Ref(...)` for the shared `TeamHeaderOptionalSchema` header, which the @fastify/swagger resolver chokes on with `Cannot read properties of undefined (reading 'match')`. The fix is to pass the schema object directly (matching the runtime-profiles pattern). Once that was correct, the spec wrote cleanly, the api-client regenerated the five new typed functions (`createRuntimeModel`, `listRuntimeModels`, `getRuntimeModel`, `updateRuntimeModel`, `deleteRuntimeModel`), and the e2e suite can import them. 3. Add `apps/rest-api/e2e/runtime-models.e2e.test.ts` with focused coverage of: - CRUD happy path (create → list → get → update → delete → 404) - the seeded global catalog is visible to any authenticated agent - the `?provider=` query narrows the list - access rules: outsiders get 403 on create/list with a foreign team header, 404 on GET of a team-scoped row (canAccessTeam hides existence), and 403 on PATCH/DELETE (canManageTeam distinguishes "exists but no write" from "no read") - global entries are read-only through the public API (PATCH and DELETE on a seeded row return 403) - input validation: missing team header, forbidden characters in provider and model, empty provider, empty PATCH body (minProperties: 1), non-primitive capabilities value, and a same-team duplicate (provider, model) mapping to 409. Each case uses a unique `e2e-<timestamp>-<rand>` provider suffix so the suite is order-independent and shares the table with other e2e tests without teardown. Cannot run the e2e suite here (no Docker in the sandbox VM); CI / a developer's local stack should pick it up via `nx run @moltnet/rest-api:e2e`. Refs: issue #1369, design entry `aca82086-5d05-4304-ae14-f55468e8141f`.
Adds a new "Runtime model catalog" section to docs/use/agent-daemon.md that documents the /runtime-models REST endpoint set up in #1369. The section is placed immediately before the existing "Remote runtime profiles" subsection so an operator picking a provider/model for a daemon lands on the catalog first, and on the profile second. Per the docs-onboarding tone rule, the section is an extension of the existing page, not a new parallel page. It uses prose first, then bash + curl, and links out to the OpenAPI spec for the full request and response shapes. The three-tab pattern (SDK / CLI / MCP) is deliberately not used: there is no SDK namespace and no MCP tool for the catalog yet, so a three-tab block would either lie about an SDK that does not exist or repeat the same curl four times. The section honestly states the REST-only surface and shows the curl/fetch path directly. The new content covers: - what the catalog is and why a team would add a custom couple - read paths: list global, list global + team, ?provider= filter, get-by-id - write paths: POST, PATCH, DELETE for team-scoped entries; the 403-on-global-row rule; the 409-on-duplicate rule - "How the daemon uses the catalog": explicit "informational only, does not gate execution" so the operator does not think the daemon will refuse a custom couple. The section is intentionally short. The full REST surface is documented in the generated OpenAPI spec (apps/rest-api/public/openapi.json), which is the canonical reference; the doc just orients an operator and shows the most common curl invocations. Out of scope (follow-up): - SDK namespace (@themoltnet/sdk) for runtime models. The api-client functions are already generated (createRuntimeModel, listRuntimeModels, getRuntimeModel, updateRuntimeModel, deleteRuntimeModel) — only the SDK wrapper is missing. The doc points at curl as the immediate workaround. - MCP tools for the catalog. - Agent CLI subcommands.
The CI OpenAPI Spec job runs `pnpm run generate`, which ends with
`prettier --write libs/api-client/src/generated/`. Prettier is
non-idempotent on the openapi-ts output: the very long multi-type
`import type { ... } from '...';` statements (200+ types) oscillate
between collapsed (single-line, >80 chars) and expanded (multi-line)
layouts on consecutive passes, so the second regen shows thousands of
unrelated formatting lines against the first.
This was a real failure in PR #1402: the committed `openapi-ts`
output was correct, but the next CI `pnpm run generate` re-formatted
it via prettier and `git diff --exit-code libs/api-client/src/generated/`
failed with ~12k lines of pure formatting churn.
Fix:
- Add `libs/api-client/src/generated/` to `.prettierignore` with a
comment explaining the openapi-ts non-idempotence. Now the
`prettier --write libs/api-client/src/generated/` step in
`pnpm run generate` is a no-op, and the drift check passes against
the committed `openapi-ts` output.
- Apply `prettier --write` to the new runtime-models code paths so
they pass `nx format:write` in the pre-commit hook.
Verified locally: running the full `pnpm run generate` (openapi + openapi-ts
+ prettier --write libs/api-client/src/generated/) twice in a row
produces zero diff against the committed SDK.
Refs PR #1402
The 'Check formatting' CI step (pnpm exec nx format:check) flagged libs/database/drizzle/meta/0021_snapshot.json and _journal.json as non-canonical: drizzle-kit emits keys in declaration order, not alphabetical, so prettier-plugin-sort-json would rewrite them on every regeneration. Since drizzle-kit re-emits the same non-canonical output on every regen, manual prettier reformatting buys nothing and the diff just churns on the next migration. Pre-existing problem (reformatted manually in commit f3d3d18 for the daemon profile migration); this just pins the policy. Pattern matches the existing entries for apps/rest-api/public/openapi.json and libs/api-client/src/generated/ — auto-generated, drift check is the real guard.
8f190fb to
c807023
Compare
Contributor
🚨 Dependency Audit — Vulnerabilities foundFull report |
c807023 to
9d74945
Compare
- Sort exports/imports to satisfy simple-import-sort lint rules in libs/database, libs/tasks, apps/rest-api/src/schemas, and related runtime-model wiring in apps/rest-api. - Fix runtime_models_creator_xor DB check constraint: global seeded rows (team_id IS NULL) have no creator, while team-scoped rows require exactly one creator. Adds migration 0022_safe_gressill to alter the constraint. - Rebase onto origin/main to pick up pinned ogen v1.21.0 generator and current release-please manifest versions, then regenerate OpenAPI spec, TypeScript client, and Go client to remove version drift. Refs issue-1369-provider-model-catalog
The provider-filter test creates a team-scoped runtime model but then listed without a team header, so the repository only returned global entries and the assertion failed. Pass the owner's personal team header so team-scoped entries are visible. Refs issue-1369-provider-model-catalog
9d74945 to
bea83c9
Compare
Remove libs/api-client/src/generated/ from .prettierignore and reformat all generated files. Prettier is now idempotent on openapi-ts output, so keeping the SDK formatted gives cleaner diffs on future regenerations. Refs issue-1369-provider-model-catalog
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a DB-backed provider/model catalog for daemon profiles, ahead of the profile-UI work in #1369.
What this PR does
Data layer: a new
runtime_modelstable (12 columns, 2 partial unique indexes partitioning global vs. team-scoped rows, a creator-XOR check, soft-disable viais_active) and aRuntimeModelRepositorywith seven methods (create,findById,findVisibleByProviderAndModel,listVisible,listByTeamId,update,delete). 13 well-known couples are seeded (Anthropic, OpenAI, OpenAI Codex, Ollama, Ollama Cloud, Claude Code, Bedrock) in the same migration,0021_secret_shooting_star.sql.REST surface at
/runtime-models: GET (list visible entries,?provider=filter, optional team header), GET-by-id, POST (create team-scoped), PATCH (update team-scoped), DELETE (delete team-scoped). Global rows are read-only through the public API; PATCH/DELETE on them returns 403. Out-of-team reads of team-scoped rows return 404. Out-of-team writes return 403.TypeBox schemas for the catalog and the runtime-profile data model (the latter no longer carries the previously-attempted advisory
warnings?field — that was dropped after design review, the catalog is informational-only).OpenAPI + TS client regen: the
apps/rest-api/public/openapi.jsonnow lists both paths and five schemas, and@moltnet/api-clientexportscreateRuntimeModel,listRuntimeModels,getRuntimeModel,updateRuntimeModel,deleteRuntimeModel.Tests:
apps/rest-api/__tests__/runtime-models.test.ts(happy path, access denial, unique-violation mapping, global-row protection, query filter).apps/rest-api/e2e/runtime-models.e2e.test.ts(CRUD round-trip, global catalog visibility, outsider access, validation). I cannot run the e2e suite in this VM (no Docker) but the test file is typechecked and follows the existingruntime-profiles.e2e.test.tspattern.tokenValidator.resolveAuthContextnow returns a shallow copy so per-request mutations ofcurrentTeamIdno longer leak into the next test.Docs: a new "Runtime model catalog" section in
docs/use/agent-daemon.mdplaced immediately before "Remote runtime profiles" so an operator picking a provider/model lands on the catalog first. Prose + curl; the three-tab pattern is deliberately not used because there is no SDK namespace or MCP tool for the catalog yet, and a three-tab block would either lie about an SDK that doesn't exist or repeat the same curl four times.Commits
b9c0b3e2— data layer (table, repo, types, migration with seeds)d1b89ec4— REST route + TypeBox schemas + route registration + integration tests6b2fe5fa— dropwarnings?(per design review), regen OpenAPI, regen TS client, e2e3f3558d4— docsOut of scope (follow-up)
@themoltnet/sdkfor the catalog. The generated client functions exist; only the typed wrapper is missing. The doc points at curl as the immediate workaround.libs/runtimeextraction (catalog + profiles + warnings logic in one package). The route files are starting to duplicateoptionalTeamId,serializeModel/serializeProfile, and the unique-violation mapping.canManageTeamPATCH/DELETE 403 vs.canAccessTeamGET 404 asymmetry. The current behavior matchesruntime-profilesbut mixes the two semantic distinctions. Tracked as a separate one-time decision.Validation
tsc -b --emitDeclarationOnlyclean forlibs/database,libs/tasks,apps/rest-apilib + spec configs.pnpm exec vitest run apps/rest-api/__tests__/runtime-models.test.ts apps/rest-api/__tests__/runtime-profiles.test.ts— 15/15 passing.pnpm exec tsx apps/rest-api/scripts/generate-openapi.ts—OpenAPI spec written to apps/rest-api/public/openapi.json.@hey-api/openapi-tsregen — clean.pnpm --filter @moltnet/docs build— pre-existing break onmain(missingdocs/.vitepress/theme/vars.cssreferenced fromindex.ts). Not introduced by this PR; tracked separately.Refs
Issue #1369, comment #4699154922. Design entries:
dcd93334(superseded),aca82086(current). Incident entries:b3879ca7(sandbox memory),c02c3fd2(.git/config leak),f12c9f2c(round-2 summary).