# Changelog ## [0.29.0] — 2026-03-17 ### Summary Starlark sandbox + permission model. Server-side extension runtime with pre-completion filter chain, permission-gated modules, and admin review workflow. Six changesets (CS0–CS5) on top of Phase 0 store cleanup (12 changesets, CS0–CS7b). Phase 0 migrated ~242 raw SQL calls into the store interface layer, giving the Starlark sandbox a clean API surface. Phase 1 builds the runtime: sandboxed eval loop, permission model, secrets/notifications modules, task executor integration, and pre-completion filter chain with extension discovery. **DB rebuild required.** Migration 016 rewritten in-place (adds `status` column to `packages`, `extension_permissions` table). ### New - **Pre-completion filter chain** (`server/filters/`) — composable `PreCompletionFilter` interface with ordered execution, error isolation, and logging. `Chain.Register()` / `Chain.Execute()`. Built-in filters use order 0–99; extension filters use 100+. - **KB auto-inject filter** — `KBInjectFilter` (order 10) replaces inline `BuildKBHint()`. Scans persona, project, channel, and personal KBs. Reference implementation for the filter model. - **Starlark sandbox** (`server/sandbox/`) — `go.starlark.net` interpreter with step limits (1M ops default), context timeout, captured print output, disabled `load()`. `MakeModule()` helper for exposing Go functions to Starlark scripts. - **Sandbox runner** — `Runner.ExecPackage()` loads script from manifest `_starlark_script`, assembles module set from granted permissions. `CallEntryPoint()` for event-driven invocation. - **Permission model** — `extension_permissions` table tracks declared capabilities from package manifests. Admin grant/revoke controls which modules the sandbox injects at runtime. Lifecycle: `install → pending_review → grant-all → active`, revoke → `suspended`. Auto-transitions on grant/revoke. - **Extension permission constants** — `secrets.read`, `notifications.send`, `filters.pre_completion`, `db.read`, `db.write`, `api.http`. Validated against manifest declarations. - **Secrets module** — `secrets.get(key)` / `secrets.list()`. Per-package key-value store backed by GlobalConfig (`ext_secrets:{packageID}`). Admin CRUD endpoints. - **Notifications module** — `notifications.send(user_id, title, body?, type?)`. Wraps platform notification service. Extensions cannot send email or bypass user preferences. - **Starlark filter** — `StarlarkFilter` bridges the filter chain to extension scripts. Calls `on_pre_completion(ctx)`, parses return `[{"role": "system", "content": "..."}]`. - **Starlark filter discovery** — `DiscoverStarlarkFilters` scans active packages with `filters.pre_completion` grant at startup. - **`task_type: "starlark"`** — executor `executeStarlark` loads package by `system_function` (package ID), calls `on_run()` entry point. Output to channel/webhook/run record. - **Extension secrets admin** — `GET/PUT/DELETE /admin/extensions/:id/secrets`. GET returns keys only (not values). - **Extension permission admin** — `GET .../permissions`, `GET .../review`, `POST .../grant`, `POST .../revoke`, `POST .../grant-all`. - **ICD runner packaging tier** — 18 tests: permission lifecycle (install → pending_review → grant → active → revoke → suspended), secrets CRUD, invalid permission rejection. ### Changed - **`BuildKBHint` deprecated** — replaced by `KBInjectFilter` in the pre-completion filter chain. Kept for backward compatibility; removal scheduled for v0.30.0. - **`packages` table** — `status` column added (`active`, `pending_review`, `suspended`). `PackageStore.SetStatus()` method. - **`CompletionHandler`** — `filterChain` field + `SetFilterChain()` setter. Filter chain runs between system prompts and message history. - **`Executor`** — `runner` field + `SetRunner()`. Starlark dispatch branch between action and prompt types. - **`AdminInstallExtension`** — calls `SyncManifestPermissions()` after package creation. Parses `"permissions"` from manifest. - **Starlark task validation** — `task.starlark` RBAC gate enforced. `system_function` required (holds package ID). Package must exist and be starlark tier. ### Phase 0 — Store Cleanup - **~242 raw SQL calls eliminated** — every handler, tool, and middleware file now goes through the store interface. - **Constructor signature changes** — `enforcePrivateProviderPolicy(ctx, stores, ...)`, `ResolveProviderConfig(stores, vault, ...)`, `NewChannelHandler(stores)`. - **30+ new store methods** across NoteStore, MessageStore, FileStore, ProjectStore, TeamStore, ProviderStore, CatalogStore, GlobalConfigStore, AuditStore, ChannelStore. ### Fixed - **`API._delete` alias** — `delete` is a JS reserved word; added `_delete` as alias for `_del` on the API client. - **`safeString` nil handling** — returns Go `nil`, not `""`. - **nil JSONMap → PG jsonb** — `ToolCalls`/`Metadata` initialized to `models.JSONMap{}` before store calls. - **`GetByID` vs `GetParentAndRole`** — cursor update uses `GetParentAndRole` (matches original raw SQL deleted_at filter). ## [0.28.8] — 2026-03-15 ### Summary ICD Green Board. Close out all pre-existing ICD test failures before v0.29.0 feature work. Gate: 543/543 (100%) on the ICD runner. Three changesets: provider sync + infrastructure resilience (cs0), CORS lockdown + WebSocket origin validation (cs1), WebSocket ticket exchange (cs2). ### Fixed - **Batched `UpsertFromSync`** — catalog sync rewritten as a single transaction: one SELECT to identify existing models, N prepared `INSERT ON CONFLICT DO UPDATE` statements, one COMMIT. Previously each model was N×2 auto-committed queries (SELECT + INSERT/UPDATE), producing ~300 round-trips and WAL flushes on a typical Venice sync. Both PG and SQLite stores rewritten. Root cause of cascading 502s: the write burst starved the connection pool and caused Traefik to see stale keepalive connections on subsequent requests. - **DB connection lifecycle** — `SetConnMaxLifetime(5m)` and `SetConnMaxIdleTime(1m)` added to the Postgres pool. Previously connections lived forever — stale TCP after PG restart or network blip would fail silently until a query hit the dead connection. - **HTTP transport pool** — `ProviderConfig.Client()` now returns a shared `*http.Client` from a `sync.Map` keyed by `(ProxyMode, ProxyURL)`. Previously every outbound provider call allocated a fresh `http.Transport`, leaking idle connections until GC. - **Traefik retry middleware** — new `Middleware` CRD (2 attempts, 100ms initial interval) applied via ingress annotation. Retries on 502 and connection reset. Both `k8s/` CI manifests and Helm chart templates updated. CI pipeline renders and applies the middleware before the ingress resource. - **Provider sync timeout** — `syncProviderModels` now wraps the outbound provider HTTP call with a configurable timeout (default 30s, `PROVIDER_SYNC_TIMEOUT` env var). Previously used no timeout, causing Gin handler timeouts and cascading 502s from the reverse proxy when upstream providers (especially Venice) were slow. - **504 on upstream timeout** — `POST /admin/models/fetch` and `POST /api-configs/:id/models/fetch` now return 504 with structured `{"error": "upstream timeout: "}` instead of letting the reverse proxy manufacture a 502 from a broken connection. - **ICD runner provider tier resilience** — `models/fetch` calls retry once on 502/504 with 2-second backoff. Eliminates cascading test failures caused by transient upstream timeouts. - **WebSocket CheckOrigin** — `upgrader.CheckOrigin` now validates the `Origin` header against `CORS_ALLOWED_ORIGINS` instead of unconditionally returning `true`. Connections from unlisted origins are rejected at upgrade time. - **CORS startup warning** — if `CORS_ALLOWED_ORIGINS` is explicitly set to `*`, a warning is logged at startup recommending restriction for production deployments. ### New - **WebSocket ticket exchange** — `POST /api/v1/ws/ticket` returns a single-use opaque ticket (128-bit random hex, 30s TTL). Client connects with `?ticket=` instead of `?token=`. Ticket is validated and deleted atomically (single-use). Background reaper removes expired unclaimed tickets every 15 seconds. This eliminates JWT exposure in server logs, proxy logs, and browser history. - **`WsAuth` middleware** — dedicated WebSocket auth middleware with three-path priority: `?ticket=` (preferred), `?token=` (legacy, logs deprecation warning), `Authorization` header (non-browser). - **`TicketStore`** — in-memory ticket store with `Issue()`, `Validate()`, `Stop()`, and automatic TTL reaping. - **`GetAllowedOrigins()`** — exported CORS origin resolver for use by WebSocket hub and other subsystems. - **Traefik retry Middleware CRD** — `k8s/middleware-retry.yaml` and `chart/templates/middleware-retry.yaml`. Configurable via `ingress.retry.attempts` and `ingress.retry.initialInterval` in Helm values. ### Changed - **`events.NewHub(bus, allowedOrigins)`** — Hub constructor now takes the resolved CORS origin string. The upgrader's `CheckOrigin` is configured once at construction time. - **`events.js` `_doConnect()`** — now async. Fetches a ticket via `POST /api/v1/ws/ticket` before building the WebSocket URL. Falls back to legacy `?token=` if ticket exchange fails (pre-v0.28.8 server compatibility). - **WebSocket route** — `GET /ws` now uses `WsAuth` middleware instead of generic `Auth`. Legacy `?token=` still works but logs deprecation. - **`ProviderConfig.Client()`** — returns shared client from pool instead of allocating per call. ### Docs - **`docs/ICD/websocket.md`** — ticket exchange protocol documented (preferred auth, legacy deprecation, origin validation). - **`docs/ARCHITECTURE.md`** — expanded CORS configuration section (environment behavior, production guidance, WebSocket interaction). - **`docs/ROADMAP.md`** — v0.28.x Tier 2 bucket eliminated (items relocated to pinned versions: v0.29.1, v0.30.0, v0.30.1). Pre-1.0 Code Quality Sweeps section deleted (relocated to v0.29.0 Phase 0 with CI-enforced gate). Zero unscheduled items remain outside TBD. ## [0.28.7] — 2026-03-15 ### Summary Unified packaging. The `surface_registry` and `extensions` tables are replaced by a single `packages` table. Surfaces, extensions, and combined "full" packages install through one endpoint (`POST /admin/packages/install`) with one archive format (`.pkg`). Task RBAC pre-positions the `task.starlark` permission gate for v0.29.0. **DB rebuild required.** Migrations 012 and 016 are rewritten in-place. Extension surfaces must be re-installed after upgrade. Builtin extensions and core surfaces re-seed automatically. ### New - **Unified `.pkg` archive format** — zip containing `manifest.json` + static assets (js/, css/, assets/). Manifest `type` field determines behavior: `surface` (routable page), `extension` (hooks/tools/pipes), `full` (both). Missing `type` defaults to `surface` for backward compatibility with `.surface` archives. - **`packages` table** — replaces `surface_registry` + `extensions`. Text slug PKs, unified schema with type/version/tier/scope columns. `package_user_settings` replaces `extension_user_settings`. - **`POST /admin/packages/install`** — unified install endpoint. Validates type-specific requirements (extensions need tools/pipes/hooks, full packages need both route and extension behavior). Accepts `.pkg`, `.surface`, `.zip`. - **Admin packages section** — replaces separate Surfaces + Extensions sections. Type badges, filter by type, install upload area. - **`/admin/surfaces/*` aliases** — all surface admin endpoints retained as aliases pointing to the packages handlers. No URL-breaking change. - **`task.starlark` permission** — pre-positioned in `AllPermissions`. Handler rejects `task_type: "starlark"` with 400 referencing v0.29.0. Not granted to any group, not exposed in admin UI until v0.29.0. ### Changed - **`SeedBuiltinExtensions` → `SeedBuiltinPackages`** — writes to `packages` table with `source='builtin'`, `type='extension'`, `is_system=true`. Same idempotent version-aware logic. - **`Stores.Surfaces` + `Stores.Extensions` → `Stores.Packages`** — single `PackageStore` interface (15 methods). Compiler-enforced migration across all handlers, pages, and completion pipeline. - **Static asset disk path** — `/data/surfaces/` → `/data/packages/`. URL path `/surfaces/:id/*path` retained for stability. - **Extension runtime** — `ListForUser`, `GetUserSettings`, `SetUserSettings`, `ServeExtensionAsset`, `ListBrowserToolSchemas` all read from `packages` table. Same API contract, new backing store. ### Removed - **`extensions` table** (migration 012) — replaced by `packages`. - **`surface_registry` table** (migration 016) — replaced by `packages`. - **`extension_user_settings` table** — replaced by `package_user_settings`. - **`ExtensionStore` interface** — subsumed by `PackageStore`. - **`SurfaceRegistryStore` interface** — subsumed by `PackageStore`. - **`surfaces/` repo directory** — renamed to `packages/`. - **`admin-surfaces.js`** — replaced by `admin-packages.js`. ## [0.28.6] — 2026-03-15 ### Summary Infrastructure release. Virtual scroll for long conversations, Helm chart for Kubernetes deployment, system task type (Go function registry replacing ad-hoc goroutines), admin broadcast notifications, server-side SSH key generation for git credentials, task webhook trigger UI, and model preference dedup fix. KB auto-inject pipe filter deferred to v0.29.0 — will be built as the reference Go implementation of the server-side filter model alongside Starlark, ensuring the architecture is right and performant before user code runs in it. ### New - **Virtual scroll** — viewport-windowed message rendering for long conversations (100+ messages). IntersectionObserver-based sentinel triggers prepend chunks of 40 as user scrolls up. DOM capped at ~80 nodes regardless of conversation length. Streaming messages always rendered. Transparent fallback for short conversations. - **Helm chart** — `chart/` directory with full Kubernetes deployment. `helm install switchboard ./chart` replaces raw manifests. Configures backend + frontend deployments, services, ingress (Traefik), PVC, ConfigMap, Secret. All `config.go` env vars exposed as values. - **System task type** — `task_type: "system"` with Go function registry. Built-in functions: `session_cleanup`, `staleness_check`, `retention_sweep`, `health_prune`. Admin creates task, picks function from dropdown, sets cron schedule. Executor calls registered Go function instead of LLM. Replaces ad-hoc goroutine background jobs with visible, configurable, auditable tasks. Permanent track — not replaced by Starlark. `GET /admin/system-functions` returns available functions. - **Admin broadcast** — `POST /admin/notifications/broadcast` sends `system.announcement` notification to all active users via `NotifyMany`. Emits `system.broadcast` WebSocket event. Admin UI compose form with title, message, level selector (info/warning/critical). - **Git credential key generation** — `POST /git-credentials/generate` creates ED25519 SSH keypair server-side. Private key vault-encrypted, never exposed. Public key + SHA256 fingerprint returned for user to add to git host. Optional `persona_id` for per-persona commit attribution. `GET /git-credentials/:id/public-key` for clipboard copy. Settings UI section with generate, list, copy, delete. - **Task webhook trigger UI** — schedule selector gains "Webhook trigger" option. Trigger URL displayed with copy button after creation. Link button on webhook tasks in list view. `webhook_secret` field for HMAC signing. Task-to-task chaining documented (Task A webhook_url → Task B trigger URL). ### Fixed - **Model preferences NULL-in-UNIQUE** — `provider_config_id` column on `user_model_settings` changed to `NOT NULL`. The `UNIQUE(user_id, model_id, provider_config_id)` constraint silently allowed duplicates when the column was NULL. Handler already enforced `binding:"required"` — schema now matches. - **ICD runner shape** — `S.modelPreference` gains `provider_config_id` field (smoke tier shape assertion). ### Changed - `ListActiveUserIDs` added to `UserStore` interface (PG + SQLite). - `NotifTypeAnnouncement` constant added to notification types. - Task model gains `system_function` field. Task stores updated (taskColumns, scanTask, Create) in both PG and SQLite. - ICD runner version bumped. New tests: model prefs (8), broadcast (3), git credentials (6), system tasks (4). ### Docs - `notifications.md` — broadcast endpoint, `system.broadcast` WS event. - `admin.md` — broadcast cross-reference. - `workspaces.md` — `/generate`, `/public-key` endpoints, new fields. - `tasks.md` — system task type, function registry, chaining pattern, `GET /admin/system-functions`. ## [0.28.5] — 2026-03-14 ### Summary Frontend SDK + pipe/filter pipeline. `switchboard-sdk.js` provides a single-object composition layer (`sw`) over the 15+ platform globals. Three-stage pipe/filter pipeline (pre-send, post-receive stream, post-render) formalizes the ad-hoc extension hook model into composable, priority-ordered, scope-aware transform chains. Backward-compatible compat shim for existing `ctx.renderers.register()` extensions. ICD runner gains `sdk` test tier (36 tests). ### New - **`switchboard-sdk.js`** — SDK core. `Switchboard.init()` returns `sw` singleton with identity (`sw.user`, `sw.isAdmin`), REST client (`sw.api.get/post/put/del/stream`), events (`sw.on/once/off/emit`), theme (`sw.theme.current/mode/set/on`), UI primitives (`sw.toast/confirm/modal`), and component wrappers (`sw.chat()`). - **Pipe/filter pipeline** — three composable stages: - `sw.pipe.pre(priority, fn, opts)` — pre-send: transform user message before LLM request. Runs on both `sendMessage()` and `regenerateMessage()`. Context includes `regenerate` flag and `regenerateMessageId`. - `sw.pipe.stream(priority, fn, opts)` — post-receive: transform each SSE chunk during streaming. Sync-only for performance. - `sw.pipe.render(priority, fn, opts)` — post-render: transform rendered DOM after DOMPurify. Replaces `runExtensionPostRender()` delegation. - **Filter scoping** — filters declare optional `scope: { channelType: [...] }` to restrict execution to specific channel types. Scoped-out filters are skipped with zero overhead. - **Pipeline introspection** — `sw.pipe.list()` returns all registered filters by stage with priority, source, scope, call count, avgMs, errors. - **Extension compat shim** — `ctx.renderers.register(name, { type: 'post' })` auto-shims into `sw.pipe.render()`. Existing block renderers (mermaid, katex, csv, diff) continue working through the unchanged `formatMessage()` path. Shimmed renderers appear in `sw.pipe.list()`. - **ICD runner `sdk` tier** — 36 tests: boot, identity, REST client, events, theme, pipe registration, execution ordering, scoping, halt semantics, error isolation, compat shim, introspection. ### Changed - `base.html` — SDK script tag added after component scripts. Universal init block simplified to `Switchboard.init()` call. UserMenu band-aid (cs15) absorbed into SDK `_hydrateUserMenu()`. - `app.js` — `startApp()` calls `Switchboard.init()` (idempotent). - `chat.js` — `sendMessage()` and `regenerateMessage()` run pre-send pipe. New `_buildPreSendContext()` helper. - `ui-core.js` — `streamResponse()` SSE loop runs stream pipe per chunk. - `ui-format.js` — `runExtensionPostRender()` delegates to render pipe with fallback to `Extensions.runPostRenderers()`. - `extensions.js` — `_registerRenderer()` shims `type: 'post'` renderers into `sw.pipe.render()`. - ICD runner version bumped to 0.28.5.0, `sdk` tier added. ## [0.28.4] — 2026-03-14 ### Summary Security tier red team. 58 adversarial tests added to the ICD runner exposing 5 bugs (2× P0, 2× P1, 1× P2), all fixed in this release. Auth middleware rewritten with a user-status cache so deactivation and role changes take effect within 30 seconds. `surfaces/` directory added to the repo with a build script for packaging extension surfaces. ### Security Fixes - **[P0] Deactivated user JWT accepted.** Auth middleware validated signature + expiry only — never checked `is_active` in the database. A deactivated user's existing JWT continued working until natural expiry (15 min). Fix: `middleware/auth.go` now performs a cached DB lookup (`UserStatusCache`, 30 s TTL) on every request. Deactivated users receive 401 within seconds. - **[P0] Demoted admin JWT retained admin access.** The `role` context value was read from JWT claims, not the database. Demoting an admin via `PUT /admin/users/:id/role` had no effect until the user's token expired. Fix: role is now resolved from the DB on every request via the same cached lookup. `RequireAdmin()` reads the live role. - **[P1] Null byte in path parameter → 500.** Gin passed `%00` through to the database layer, causing a scan error. Fix: new `ValidatePathParams()` middleware rejects null bytes with 400. - **[P1] 4096-char path parameter → 500.** Same root cause — oversized params reached the DB. Fix: `ValidatePathParams()` rejects params longer than 255 characters with 400. - **[P2] CORS `Access-Control-Allow-Origin: *` in all environments.** Fix: production mode now defaults to same-origin only. Override with `CORS_ALLOWED_ORIGINS=https://app.example.com` (comma-separated). Development mode retains `*`. ### Added - **ICD Security Tier** — 58 adversarial tests across 6 domains: auth-boundary (8), cross-tenant (20), input-validation (18), session (6), escalation (7), transport (3). Red "Security" button in the runner UI. Requires fixtures. - **`UserStatusCache`** (`middleware/auth.go`) — per-user `{isActive, role, fetchedAt}` cache with 30 s TTL. Shared by `Auth()`, `AuthOrSession()`, and `AuthOrRedirect()`. Cache eviction on `UpdateUserRole` and `ToggleUserActive` via callback. - **`ValidatePathParams()` middleware** (`middleware/validate.go`) — rejects null bytes and >255-char path params before they reach handlers. - **`surfaces/` directory** — source tree for extension surfaces with `build.sh` that packages each subdirectory into `dist/.surface`. `hello-dashboard` and `icd-test-runner` moved from loose archives into version-controlled source. - **`surfaces/README.md`** — structure docs, build instructions, conventions. - ICD runner: `safeDelete()` now retries 2× with 500 ms backoff on 502/503, swallows cleanup errors with `console.warn` instead of throwing. - ICD runner: `assertDenied()` / `assertRawDenied()` helpers treat 502 as INCONCLUSIVE instead of CRITICAL. ### Changed - `middleware.Auth()` signature: `(cfg)` → `(cfg, users, cache)`. - `middleware.AuthOrSession()` signature: `(cfg, stores)` → `(cfg, stores, cache)`. - `middleware.AuthOrRedirect()` signature: `(cfg)` → `(cfg, users, cache)`. - `middleware.CORS()` signature: `()` → `(cfg)`. - `AdminHandler` gains `OnUserChanged(func(userID string))` — wired to `cache.Evict` in `main.go`. - 10 test files updated for new middleware signatures. - `.gitignore`: added `*.surface` under build artifacts. - ICD runner manifest bumped to `0.28.4.0`. ### Removed - `hello-dashboard.surface` at repo root (replaced by `surfaces/hello-dashboard/` source directory). ### Verified (not bugs) - Cross-tenant isolation: 20/20 — notes, channels, messages, tasks, workspaces, projects, BYOK configs, memories, team providers/ personas/members all properly isolated. - SQL injection: 7 payloads across 3 search endpoints — all parameterized. - XSS storage: 3 round-trip tests — no server crashes (frontend sanitizes via DOMPurify). - Escalation: self-promote, cross-role create, team-admin → platform-admin, user_id substitution — all blocked. - JWT tampering + alg=none: both rejected. - Refresh token rotation + revocation: both working. - Auth rate limiting: 429 after burst. - Unauthenticated access: all 6 endpoints return 401. ### Known - `[P2]` WebSocket `?token=` query parameter exposes JWT in server logs and browser history. Informational — consider ticket exchange pattern post-1.0. - Cross-visitor session isolation: enforced by `session_auth.go` channel binding, but no E2E test yet (requires public_link workflow setup — defer to visitor E2E milestone). ## [0.28.3.5] — 2026-03-14 ### Summary Frontend decomposition Phase 4: ES module conversion. IIFE wrappers removed from all 47 JS files, `