Skip to content

Commit e7fefea

Browse files
jsilvanusclaude
andauthored
Phases 129-130: admin model allow-lists and BYOK (#118)
* Implement Phases 129-130: admin-gated model allow-lists and BYOK Add gitsema admin models list|allow|deny|reset for superadmin control over which embedding profiles and narrator/guide configs are usable, server-wide or per-org (Phase 129). Add request-scoped BYOK credentials to narrate/explain/guide across CLI, HTTP, and MCP that bypass the allow-list entirely and are never persisted (Phase 130). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MDWd8BkGMSwZiMeDBVfvk8 * Simplify: dedupe BYOK CLI parsing and admin allow/deny policy logic Extract parseByokCliOpts/addByokOptions into src/cli/lib/byok.ts, shared by narrate.ts/explain and guide.ts (was triplicated). Collapse modelPolicy.ts's near-duplicate allowServer/denyServer/allowOrg/denyOrg into shared applyAllow/applyDeny helpers, preserving all existing exported signatures. Logs two lower-value findings (pinned-profile exemption placement, BYOK body-shape normalization) to feature-ideas.md per the simplify-pass policy. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 68845bf commit e7fefea

23 files changed

Lines changed: 1228 additions & 44 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gitsema": minor
3+
---
4+
5+
Add superadmin-controlled model allow-lists and bring-your-own-key (BYOK) support. Operators can now restrict which embedding profiles or narrator/guide model configs are usable, server-wide or per-org, via `gitsema admin models list|allow|deny|reset --kind <embedding|narrator|guide> [--org <name>]`. Independently, `narrate`/`explain`/`guide` (CLI, HTTP, and MCP) accept request-scoped BYOK credentials (`--byok-http-url`/`--byok-api-key`/`--byok-model`/`--byok-max-tokens`/`--byok-temperature` and equivalent HTTP/MCP fields) that bypass the allow-list entirely and are never persisted.

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ All commands support a top-level `--verbose` flag (or `GITSEMA_VERBOSE=1`) for d
130130
| `gitsema auth sso unlink <provider> <external-id>` | Unlink an external identity (operator-only) |
131131
| `gitsema auth sso list <username>` | List SSO identities linked to a user (operator-only) |
132132
| `gitsema audit log [--org <org>] [--repo <repo-id>] [--limit <n>]` | Query the identity/authorization audit trail (grant/token/login/org-membership/repo-move events), newest first (operator-only) |
133+
| `gitsema admin models list --kind <embedding\|narrator\|guide> [--org <name>]` | Show the effective allowed set for a model kind, server-wide or for an org (Phase 129, operator-only) |
134+
| `gitsema admin models allow\|deny <identifier> --kind <kind> [--org <name>]` | Enable/disable a defined embedding profile or narrator/guide model config, server-wide or narrowed for an org (never widening past the server-wide set; operator-only) |
135+
| `gitsema admin models reset --kind <kind> [--org <name>]` | Revert a model kind's policy to default-allow-all, server-wide or for an org (operator-only) |
133136

134137
#### `gitsema index start [options]`
135138

@@ -340,8 +343,13 @@ Both commands are **safe-by-default**: with no narrator model configured (or wit
340343
| `--model <name>` || Narrator model name to use (overrides active selection) |
341344
| `--narrate` | off | Call the configured LLM narrator and return prose (default: return evidence only) |
342345
| `--evidence-only` | on | Return raw commit evidence without calling the LLM (this is the default) |
346+
| `--byok-http-url <url>` || Request-scoped narrator LLM endpoint (bring-your-own-key, Phase 130); bypasses configured/allow-listed models and the DB entirely, never persisted |
347+
| `--byok-api-key <key>` || Bearer token for `--byok-http-url` |
348+
| `--byok-model <name>` || Model id sent to `--byok-http-url` (defaults to the endpoint default) |
349+
| `--byok-max-tokens <n>` || Max tokens per BYOK call |
350+
| `--byok-temperature <n>` || Temperature for BYOK calls |
343351

344-
Configure a narrator model with `gitsema models add <name> --narrator --http-url <url> [--key <token>] --activate`, with a local Ollama model (`gitsema models add <name> --narrator --provider ollama [--global-name <tag>] --activate`, see "Ollama" below), or with a local CLI AI tool: `gitsema models add <name> --narrator --provider cli --cli-command <tool> [--cli-args "<args>"] --activate` (see "CLI-based AI tool backends" below).
352+
Configure a narrator model with `gitsema models add <name> --narrator --http-url <url> [--key <token>] --activate`, with a local Ollama model (`gitsema models add <name> --narrator --provider ollama [--global-name <tag>] --activate`, see "Ollama" below), or with a local CLI AI tool: `gitsema models add <name> --narrator --provider cli --cli-command <tool> [--cli-args "<args>"] --activate` (see "CLI-based AI tool backends" below). Alternatively, pass `--byok-http-url` (and optionally `--byok-api-key`/`--byok-model`) for a one-off, never-persisted request-scoped model — it bypasses the configured/allow-listed model selection entirely (Phase 130).
345353

346354
#### `--narrate` on other commands
347355

@@ -378,8 +386,13 @@ All outbound content (prompts, tool results) is passed through the same secret/P
378386
| `--model <name>` | Guide/narrator model name to use |
379387
| `--no-context` | Skip gathering git context (faster but less accurate) |
380388
| `-i, --interactive` | Start an interactive REPL session (one question per line, multi-turn — the agent session is reused across turns) |
389+
| `--byok-http-url <url>` | Request-scoped guide LLM endpoint (bring-your-own-key, Phase 130); bypasses configured/allow-listed models and the DB entirely, never persisted |
390+
| `--byok-api-key <key>` | Bearer token for `--byok-http-url` |
391+
| `--byok-model <name>` | Model id sent to `--byok-http-url` (defaults to the endpoint default) |
392+
| `--byok-max-tokens <n>` | Max tokens per BYOK call |
393+
| `--byok-temperature <n>` | Temperature for BYOK calls |
381394

382-
Configure a guide model with `gitsema models add <name> --guide --http-url <url> [--key <token>] --activate`, or with a local CLI AI tool: `gitsema models add <name> --guide --provider cli --cli-command <tool> [--use-mcp] --activate` (see "CLI-based AI tool backends" below).
395+
Configure a guide model with `gitsema models add <name> --guide --http-url <url> [--key <token>] --activate`, or with a local CLI AI tool: `gitsema models add <name> --guide --provider cli --cli-command <tool> [--use-mcp] --activate` (see "CLI-based AI tool backends" below). Alternatively, pass `--byok-http-url` (and optionally `--byok-api-key`/`--byok-model`) for a one-off, never-persisted request-scoped model (Phase 130).
383396

384397
**Ollama:** `--provider ollama` configures `narrate`/`explain`/`guide` against a local Ollama
385398
server with no API key. It defaults `--http-url` to `http://localhost:11434` (no trailing

docs/PLAN.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4924,16 +4924,26 @@ Deviations from the design doc, discovered during implementation:
49244924
| Phase | Spec section | Title | Deliverable |
49254925
|---|---|---|---|
49264926
| **128** | §5 Phase 1 | Multi-profile embedding serving | Server config shape for N named embedding profiles; `gitsema tools serve` builds a `Map<profileName, {textProvider, codeProvider}>` instead of one global pair; `repos.profileName` column pinned at first index and immutable after; CLI `gitsema index start --profile <name>`, `gitsema repos info` surfacing the pinned profile. ✅ complete |
4927-
| **129** | §5 Phase 2 | Admin-gated enabled sets | Superadmin admin CLI/route enabling/disabling defined profiles and narrator/guide configs server-wide; org-level narrowing (never widening) reusing the Multi-Tenant Auth Track's org/role model; picker UX (pre-selected/disabled when exactly one profile is allowed, real picker otherwise). |
4928-
| **130** | §5 Phase 3 | BYOK for narrator/guide | Request-scoped credential field on `narrate`/`explain`/`guide` CLI/HTTP/MCP entry points; one-off provider construction via `createNarratorProviderFor()` with no persistence path; "lock to none" support (empty allow-list as a valid, tested state). |
4927+
| **129** | §5 Phase 2 | Admin-gated enabled sets | Superadmin admin CLI/route enabling/disabling defined profiles and narrator/guide configs server-wide; org-level narrowing (never widening) reusing the Multi-Tenant Auth Track's org/role model; picker UX (pre-selected/disabled when exactly one profile is allowed, real picker otherwise). ✅ complete |
4928+
| **130** | §5 Phase 3 | BYOK for narrator/guide | Request-scoped credential field on `narrate`/`explain`/`guide` CLI/HTTP/MCP entry points; one-off provider construction via `createNarratorProviderFor()` with no persistence path; "lock to none" support (empty allow-list as a valid, tested state). ✅ complete |
49294929

4930-
**Status:** Phase 128 complete *(completed vX.Y.Z)*; Phases 129–130 not started.
4930+
**Status:** Phases 128–130 complete *(completed vX.Y.Z)*.
49314931

49324932
**Phase 128 deviations from spec:**
49334933
- The `--profile <name>` flag landed on `gitsema remote-index` (not `gitsema index start`) — `remote-index` is the command backed by the persistent repo registry and first-index profile-pinning concept the design doc describes; a local-only `index start` has no registry row to pin a profile to.
49344934
- Query-time profile-aware embedding (search/evolution routes using the *pinned* profile's text provider instead of the process-wide default) is out of scope for this pass — search still embeds queries with the server's default text provider regardless of which profile a result's source repo was indexed with.
49354935
- Testing "a profile disabled but the repo keeps working" is deferred to Phase 129, since the enable/disable admin mechanism doesn't exist yet.
49364936

4937+
**Phase 129 deviations from spec:**
4938+
- Policy storage reuses the existing `settings` key-value table (JSON blobs keyed `model_allowlist:server:<kind>` / `model_allowlist:org:<orgId>:<kind>`) instead of a new schema table — no migration, schema stays at v32.
4939+
- Admin surface is CLI-only (`gitsema admin models list|allow|deny|reset --kind <embedding|narrator|guide> [--org <name>]`); no HTTP/MCP route was added in this pass (no `docs/parity.md` entry to add since the command has no other-interface counterpart to compare against — see parity note below).
4940+
- Org-level narrowing is fully wired for `embedding` profiles (the `gitsema tools serve` HTTP route has org context via `getRepoOrgId`), but **not** for `narrator`/`guide` configs — `gitsema models activate` enforces the server-wide allow-list only, since there is no HTTP entry point for narrator/guide activation to attach org context to. Org narrowing for narrator/guide remains a CLI-side-only allow-list (server-wide) until such a route exists.
4941+
- The Phase 128-deferred regression case — a repo already pinned to an embedding profile keeps working even after that profile is later `denyServer`'d — is implemented as an explicit guard in `src/server/routes/remote.ts`'s profile-resolution block (the new 403 check is skipped whenever `pinnedProfileName` is set) and covered by a dedicated test.
4942+
4943+
**Phase 130 deviations from spec:**
4944+
- BYOK credentials are always resolved to `provider: 'chattydeer'` (HTTP backend) — a CLI-subprocess-backed BYOK variant was judged out of scope; HTTP/OpenAI-compatible is the realistic BYOK case the design doc anticipates.
4945+
- Implemented as an opt-in `byok` field threaded through the *existing* `resolveNarratorProvider`/`resolveGuideConfig`/`resolveGuideProvider` functions (short-circuits before any DB access or allow-list check) rather than a separate code path, so every existing call site (CLI `narrate`/`explain`/`guide`, HTTP `/api/v1/narrate`/`/explain`/`/guide/chat`, MCP `narrate_repo`/`explain_issue_or_error`) gained BYOK support by construction. CLI flags: `--byok-http-url`/`--byok-api-key`/`--byok-model`/`--byok-max-tokens`/`--byok-temperature`; HTTP body: nested `byok: {httpUrl, apiKey, model, maxTokens, temperature}` (narrator routes) or snake_case `byok: {http_url, api_key, model, max_tokens, temperature}` (guide route, matching its existing snake_case body convention); MCP: flattened `byok_http_url`/`byok_api_key`/`byok_model`/`byok_max_tokens`/`byok_temperature` fields.
4946+
49374947
---
49384948

49394949
### Phase 131 — `gitsema index doctor --fix` auto-repair *(completed)*

docs/feature-ideas.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,60 @@ No design committed yet. Two independent, optional follow-ups:
572572

573573
---
574574

575+
## Pinned-Profile Exemption Generalization & Guide HTTP BYOK Mapping Cleanup
576+
577+
### Problem
578+
- Found during the `/simplify` review of Phases 129–130 (admin-gated model
579+
allow-lists, BYOK for narrator/guide).
580+
- `src/server/routes/remote.ts`'s "a pinned profile that was later disabled
581+
keeps working for its own repo" exemption (PLAN.md Phase 128 deviation) is
582+
a flat `if (resolvedProfileName && !pinnedProfileName && ...)` condition
583+
bolted directly onto this one route handler, rather than being a parameter
584+
on `modelPolicy.ts`'s allow-list resolution itself (e.g. an
585+
`isAllowed(..., { pinnedIdentifier })` shape). It works correctly today
586+
but could silently drift out of sync if a second enforcement point for
587+
this exemption is ever added elsewhere.
588+
- `src/server/routes/guide.ts`'s HTTP route does an inline snake_case→camelCase
589+
mapping of the request body's `byok` fields (`byok_http_url`
590+
`httpUrl`, etc.) rather than sharing a helper with the CLI's
591+
`parseByokCliOpts` (`src/cli/lib/byok.ts`) or the MCP tool's equivalent
592+
mapping in `src/mcp/tools/narrator.ts`. All three shapes differ slightly
593+
(string-typed CLI opts vs. typed-number HTTP body vs. flat snake_case MCP
594+
fields), so unifying them isn't a drop-in win.
595+
- Both were judged single-occurrence, low-duplication-cost issues — not
596+
worth restructuring on a cleanup pass over code that just shipped.
597+
598+
### Intended Behavior
599+
No design committed yet. Two independent, optional follow-ups:
600+
- Move the pinned-profile exemption into `modelPolicy.ts`'s effective-set
601+
resolution (or a new `isAllowed()` helper) so any future second
602+
enforcement point inherits the same exemption automatically instead of
603+
re-implementing the `if` check.
604+
- Decide whether a shared `byok` body-shape normalizer (CLI/HTTP/MCP) is
605+
worth introducing, or whether the three call sites should stay
606+
independently shaped since they're parsing genuinely different wire
607+
formats (CLI strings, JSON body, MCP flat fields).
608+
609+
### Design Gaps
610+
- [ ] Whether a second enforcement point for the pinned-profile exemption is
611+
ever likely enough to justify generalizing now vs. waiting until one
612+
actually appears.
613+
- [ ] Whether a shared BYOK body-shape normalizer would reduce real
614+
duplication or just add an abstraction layer over three already-small,
615+
already-distinct mapping functions.
616+
617+
### Effort Estimate
618+
- Pinned-profile exemption push-down: small — one new optional parameter on
619+
an existing `modelPolicy.ts` function, plus updating the single call site
620+
in `remote.ts`.
621+
- BYOK body-shape normalizer: small, but low value given only three
622+
call sites with differing shapes.
623+
624+
### Prerequisites
625+
- None — both are isolated, optional refactors of already-shipped code.
626+
627+
---
628+
575629
## Related Issues & Documents
576630

577631
- **Parity tracking:** See `docs/parity.md` for tool availability across interfaces

docs/features.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,63 @@ codeModel?, httpUrl?, apiKey?}` — and resolved to their own
438438
the server's process-wide text provider, not a per-repo pinned profile's
439439
provider — see `docs/PLAN.md`'s Phase 128 entry for the deviation note.
440440

441+
### Admin-gated enabled sets (Phase 129)
442+
443+
A superadmin/operator can restrict which defined embedding profiles and
444+
narrator/guide model configs are actually selectable, server-wide and
445+
(for embedding profiles) per-org.
446+
447+
- **CLI:** `gitsema admin models list --kind <embedding|narrator|guide>
448+
[--org <name>]` shows the effective allowed set; `gitsema admin models
449+
allow|deny <identifier> --kind <kind> [--org <name>]` and `gitsema admin
450+
models reset --kind <kind> [--org <name>]` manage it.
451+
- **Policy semantics:** no policy set = default-allow-all (Phase 128
452+
behavior unchanged). The first `allow` call seeds an opt-in set
453+
containing only that identifier; the first `deny` call seeds an opt-out
454+
set containing every other currently-defined item. Denying every defined
455+
item reaches "lock to none" — a valid, tested state.
456+
- **Org narrowing.** An org's effective allowed set is always intersected
457+
with the server-wide set — an org can narrow but never widen past what
458+
the server allows. Embedding profiles support org narrowing end-to-end
459+
(`POST /api/v1/remote/index` resolves the requesting repo's org via
460+
`getRepoOrgId` and 403s a disabled profile pick). Narrator/guide
461+
enforcement is server-wide only — `gitsema models activate` rejects
462+
activating a server-disabled narrator/guide config, but there is no
463+
per-org narrowing for them yet (no HTTP entry point carries org context
464+
for narrator/guide activation).
465+
- **Pinned repos are exempt.** A repo already pinned to an embedding
466+
profile (Phase 128) keeps reindexing successfully even after that
467+
profile is later disabled — disabling only blocks *new* profile
468+
selections, never an existing pin.
469+
- Policy is stored in the existing `settings` table as JSON blobs
470+
(`model_allowlist:server:<kind>` / `model_allowlist:org:<orgId>:<kind>`)
471+
— no schema change.
472+
473+
### BYOK (bring-your-own-key) for narrator/guide (Phase 130)
474+
475+
`narrate`, `explain`, and `guide` accept request-scoped LLM credentials
476+
that bypass the DB-backed narrator/guide config system entirely — no
477+
`embed_config`/`settings` write, and no allow-list check (Phase 129),
478+
so BYOK still works even when every defined narrator/guide config is
479+
denied server-wide ("lock to none").
480+
481+
- **CLI:** `--byok-http-url <url>` (required to activate BYOK)
482+
`--byok-api-key <key>` `--byok-model <name>` `--byok-max-tokens <n>`
483+
`--byok-temperature <n>` on `gitsema narrate`, `gitsema explain`, and
484+
`gitsema guide`.
485+
- **HTTP:** `POST /api/v1/narrate` and `POST /api/v1/explain` accept a
486+
`byok: {httpUrl, apiKey?, model?, maxTokens?, temperature?}` body field;
487+
`POST /api/v1/guide/chat` accepts the same shape with its existing
488+
snake_case convention (`byok: {http_url, api_key?, model?, max_tokens?,
489+
temperature?}`).
490+
- **MCP:** `narrate_repo` and `explain_issue_or_error` accept flattened
491+
`byok_http_url`/`byok_api_key`/`byok_model`/`byok_max_tokens`/
492+
`byok_temperature` input fields.
493+
- Resolution order in `resolveNarratorProvider`/`resolveGuideConfig`: BYOK
494+
(if supplied) short-circuits before any other lookup — explicit model
495+
id, model name, active DB selection, disabled. BYOK always resolves to
496+
an HTTP/`chattydeer`-backed provider.
497+
441498
### Persistent server-side repo storage
442499

443500
`POST /api/v1/remote/index` **persists** the clone + index by default (`persist: true`),

docs/parity.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This document tracks the availability of gitsema tools and commands across all interfaces, and the implementation of common flags across the CLI. It serves as the single source of truth for interface parity and helps identify gaps, inconsistencies, and opportunities for unification.
44

5-
**Last updated:** 2026-06-23 (the only date in this document — see §4 for why)
5+
**Last updated:** 2026-06-24 (the only date in this document — see §4 for why)
66
**Maintainer note:** Any tool change, interface change, or flag addition must be reflected in the tables below and in the canonical sections of `CLAUDE.md` / `docs/features.md` / `README.md`.
77

88
---
@@ -139,6 +139,7 @@ This table shows which tools/commands are available in which interface. A checkm
139139
| `repos visibility` |||||||||
140140
| `auth sso link/unlink/list` |||||||||
141141
| `audit log` |||||||||
142+
| `admin models` (list/allow/deny/reset) |||||||||
142143
| `quickstart` / `setup` |||||||||
143144
| **Visualization** |
144145
| `map` |||||||||
@@ -487,6 +488,8 @@ If you find a discrepancy, **update this file first**, then propagate the change
487488
| Phase 117 | MCP Streamable HTTP transport | `tools mcp --http` — MCP's standard network transport, supersedes `--websocket` for MCP clients; no change to tool availability (§0) |
488489
| Phase 119 (review10 close-out) | Network-transport resource bounds | Payload/connection/session caps and non-loopback-without-key warnings added to all `--websocket`/`--http`/`--tcp` modes; no parity impact (security hardening, internal) |
489490
| Phase 120 | `tools lsp --tcp` deprecated | No tool/method availability change — `--tcp` still works identically, now prints a deprecation notice on every invocation steering callers to `--websocket --key` (§0); not yet scheduled for removal |
491+
| Phase 129 | Admin-gated enabled sets | New `gitsema admin models list\|allow\|deny\|reset` CLI command (operator-only, no other interface — see Tool Matrix); no flag changes to existing tools |
492+
| Phase 130 | BYOK for narrator/guide | `--byok-http-url`/`--byok-api-key`/`--byok-model`/`--byok-max-tokens`/`--byok-temperature` flags added to `narrate`/`explain`/`guide` (CLI), nested `byok` body field on the matching HTTP routes, flattened `byok_*` fields on the matching MCP tools; no Tool Matrix changes (additive flags on already-listed tools) |
490493
| Future | CLI Interactive | Full CLI with autocomplete, history, interactive UI |
491494
| Future | Web UI | Browser-based dashboard with visualization |
492495

0 commit comments

Comments
 (0)