diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b4501..a05be32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,61 @@ All notable changes to Chat Switchboard. +## [0.17.0] — 2026-02-27 + +### Added +- **Persona-KB binding.** Personas can now have knowledge bases directly + bound to them. When a user selects a persona with bound KBs, the + `kb_search` tool is automatically scoped to those KBs and the persona's + system prompt includes a KB listing hint. Admin and team admin preset + forms include a KB picker with per-KB auto-search toggles. + Migration adds `persona_knowledge_bases` join table. +- **Enterprise KB mode.** New `discoverable` flag on knowledge bases + controls whether users can see and attach KBs directly. When + `kb_direct_access` platform policy is set to `true`, users cannot + attach KBs to channels directly — they access KBs exclusively through + persona bindings curated by admins. New `ListDiscoverableKBs` and + `SetDiscoverable` endpoints. +- **Role fallback alerts (issue #69).** When a role's primary provider + fails and the fallback activates, the resolver emits a `role.fallback` + event on the EventBus with audit log entry. Admin users see a persistent + dismissable banner. 5-minute per-role cooldown prevents flooding. +- **Chat rename.** Double-click any chat title in the sidebar to edit + inline. Enter saves, Escape cancels. +- **Utility model auto-naming.** After the first assistant response, + a background request to the utility role generates a concise title. + Falls back to truncation when no utility model is configured. + New endpoint: `POST /channels/:id/generate-title`. +- **Chat token count.** Conversation token estimate shown in the model + bar, color-coded against context budget. +- **State restore on refresh.** Active chat ID persisted to + `sessionStorage`, auto-restored on page reload. + +### Fixed +- **ResolvePreset group access bypass.** `ResolvePreset()` used raw SQL + that skipped `resource_grants` checks from v0.16.0. Now uses + `PersonaStore.UserCanAccess()`. +- **KB create scope authorization.** Now enforces admin role for global + KBs and team admin role for team KBs. +- **Completion handler persona ID scoping.** `personaID` variable scoped + inside an `if` block but referenced downstream. Fixed by threading + through function signatures. +- **Nil slice JSON marshaling.** `ListDiscoverableKBs` returns `[]` + instead of `null` when no KBs match. + +### Changed +- `UpdateDocumentStorageKey` moved from raw `ExecContext` to store method. +- EventBus route table: `role.fallback` → `DirToClient`. +- Resolver gains `.WithBus(bus)` builder (nil-safe, backwards compatible). +- Service worker: `/extensions/` excluded from fetch handler. +- Mermaid renderer v2.0 (pan/zoom, export) promoted to builtin. +- Removed DIAG diagnostics from `TestGroupBasedPersonaAccess`. +- Paste-to-file threshold synced from backend `PublicSettings` + (`storage.paste_to_file_chars`, admin-configurable, default 2000). + Resolves hardcoded constant in `attachments.js`. +- Embedding dropdown already had tolerant type filter, manual model ID + fallback, and auto-switch on empty — confirmed complete, checkbox updated. + ## [0.12.0] — 2026-02-25 ### Added diff --git a/VERSION b/VERSION index 8076af5..07feb82 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.15.1 \ No newline at end of file +0.17.0 \ No newline at end of file diff --git a/docs/DESIGN-0.17.0.md b/docs/DESIGN-0.17.0.md new file mode 100644 index 0000000..15a488f --- /dev/null +++ b/docs/DESIGN-0.17.0.md @@ -0,0 +1,184 @@ +# DESIGN-0.17.0 — Persona-KB Binding + Enterprise KB Mode + +## Overview + +Personas become **gateways** to knowledge. In enterprise deployments, +users don't interact with KBs directly — they talk to Personas that +have KBs attached. This is the core differentiator for enterprise use. + +Depends on: knowledge bases (v0.14.0), user groups (v0.16.0). + +**Design principle: Personas carry context, not users.** When a user +selects a Persona with bound KBs, the KB context flows automatically — +no manual KB selection, no toggle management, no confusion about which +KBs are in scope. Admins curate the KB↔Persona relationships; users +just pick a Persona. + +--- + +## 1. Schema + +### `persona_knowledge_bases` (new join table) + +```sql +CREATE TABLE persona_knowledge_bases ( + persona_id UUID NOT NULL REFERENCES personas(id) ON DELETE CASCADE, + kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, + auto_search BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (persona_id, kb_id) +); +``` + +`auto_search` controls whether the KB is searched automatically on every +message (top-K results prepended to context) or only via explicit +`kb_search` tool calls. Default `false` = tool-only. + +### `knowledge_bases.discoverable` (new column) + +```sql +ALTER TABLE knowledge_bases ADD COLUMN discoverable BOOLEAN NOT NULL DEFAULT true; +``` + +When `false`, the KB does not appear in user-facing listings +(`ListDiscoverableKBs`). It remains searchable through Persona bindings. + +### `platform_policies.kb_direct_access` (new row) + +Seeded as `'true'` (permissive default). When set to `'false'`, the +channel KB popup is hidden — users access KBs exclusively through +Persona bindings. + +--- + +## 2. Store Layer + +### `PersonaStore` additions + +- `SetKBs(ctx, personaID, kbIDs, autoSearch)` — UPSERT join table +- `GetKBs(ctx, personaID)` — returns `[]PersonaKB` with KB metadata + +### `KnowledgeBaseStore` additions + +- `ListDiscoverable(ctx, userID, teamIDs)` — KBs where `discoverable=true` + and user has access via ownership, team, or global scope +- `SetDiscoverable(ctx, kbID, discoverable)` — toggle visibility +- `UpdateDocumentStorageKey(ctx, docID, key)` — moved from raw SQL + +--- + +## 3. Completion Pipeline + +### KB scoping in `BuildKBHint` + +When a Persona has bound KBs, `BuildKBHint` merges them with any +channel-attached KBs: + +1. Load channel KBs (existing path) +2. Load Persona KBs via `PersonaStore.GetKBs()` +3. Union the two sets (deduplicated by KB ID) +4. Build the hint string with listing + +### Persona ID threading + +The `personaID` is threaded through the completion handler methods: +`Complete()` → `streamCompletion(…, personaID)` / `syncCompletion(…, personaID)` +→ `loadConversation(…, personaID)`. + +The `ExecutionContext` for tool calls carries the Persona ID so +`kb_search` can auto-include Persona-bound KBs in its search scope. + +--- + +## 4. Role Fallback Alerts (issue #69) + +When a role's primary provider fails: + +1. **Attempt fallback** — existing behavior, unchanged. +2. **Emit event** — `role.fallback` on EventBus (DirToClient). +3. **Cooldown** — 5-minute per-role TTL prevents flooding. +4. **Admin toast** — frontend listens for `role.fallback` events, + shows warning/error toast to admin users only. + +### Resolver changes + +```go +type Resolver struct { + stores store.Stores + vault *crypto.KeyResolver + bus *events.Bus + mu sync.Mutex + cooldown map[string]time.Time // role → last alert time +} +``` + +`WithBus(bus)` builder method. Nil-safe — if no bus is attached, +alerts are suppressed (log only). + +--- + +## 5. Security Fixes + +### ResolvePreset bypass + +`ResolvePreset()` in `presets.go` used raw SQL that skipped the +`resource_grants` table added in v0.16.0. A Persona accessible only +via group grant would fail at completion time. Fixed to use +`PersonaStore.GetByID()` + `UserCanAccess()`. + +### KB create scope authorization + +`POST /api/v1/knowledge-bases` now enforces: +- `scope=global` → requires admin role +- `scope=team` → requires team admin role for the target team +- `scope=personal` → open to all authenticated users + +--- + +## 6. Frontend + +### KB Picker (`persona-kb.js`) + +Standalone component: `renderPersonaKBPicker(container, opts)` → control +object with `getValues()`, `setValues()`, `clear()`, `refresh()`. + +Options: `scope` ('admin'|'team'|'personal'), `teamId`, `prefix`. + +Shows checkboxes for available KBs with doc/chunk counts. Selected KBs +show an "Auto-inject" toggle for the `auto_search` flag. + +### Integration points + +- Admin preset form: KB picker below the form, load/save bindings on + edit/create via `/api/v1/admin/presets/:id/knowledge-bases`. +- Team preset form: same pattern via team API prefix. +- User preset form: shows discoverable KBs only. +- Admin settings: `kb_direct_access` toggle checkbox. + +### Fallback alert + +Admin users receive `UI.toast()` notifications when `role.fallback` +events arrive via WebSocket. + +--- + +## 7. API Endpoints (new) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/presets/:id/knowledge-bases` | List KBs bound to a persona | +| PUT | `/api/v1/presets/:id/knowledge-bases` | Set persona-KB bindings | +| GET | `/api/v1/admin/presets/:id/knowledge-bases` | Admin: list persona KBs | +| PUT | `/api/v1/admin/presets/:id/knowledge-bases` | Admin: set persona-KB bindings | +| GET | `/api/v1/knowledge-bases-discoverable` | List discoverable KBs for user | +| PUT | `/api/v1/knowledge-bases/:id/discoverable` | Toggle KB discoverability | + +--- + +## 8. Migration + +Single migration file: `002_v017_persona_kb.sql` + +- Creates `persona_knowledge_bases` table +- Adds `discoverable` column to `knowledge_bases` +- Seeds `kb_direct_access` platform policy (default `'true'`) diff --git a/docs/DESIGN-CM6.md b/docs/DESIGN-CM6.md new file mode 100644 index 0000000..4bc3390 --- /dev/null +++ b/docs/DESIGN-CM6.md @@ -0,0 +1,498 @@ +# DESIGN: CodeMirror 6 Integration + +**Version:** v0.18.0 (or fits into current cycle) +**Status:** Draft +**Scope:** Add CM6 as a vendored dependency, bundled at Docker build time via esbuild. Three integration surfaces. + +--- + +## Motivation + +Three features converge on the same dependency: + +1. **Chat input** — live markdown rendering (backtick → code block, bold, etc.) +2. **Extension editor** — syntax-highlighted JavaScript/JSON editing in admin panel (replaces bare `