# Changelog ## [0.31.2] — 2026-03-19 ### Summary Team Workflow Self-Service: team admins can now create, edit, publish, and delete workflows scoped to their team without requiring platform admin access. New `/teams/:teamId/workflows` route group with full CRUD, stage management, and publish. New "Workflows" tab in the team admin panel with visual stage builder. Team slugs enable clean public workflow URLs (`/w/engineering/intake` instead of UUID). ### New - **Team-scoped workflow routes** — full CRUD under `/teams/:teamId/workflows` behind `RequireTeamAdmin`, with ownership guards verifying `workflow.team_id` matches the path parameter. Stage CRUD, reorder, publish, and version read all team-scoped. - **Team admin Workflows tab** — 9th tab in the team admin modal. List, create, edit, delete workflows. Visual stage builder with persona picker (team personas), form builder, drag-and-drop reorder. Publish with version display. - **Team slugs** — `slug` column on teams table, auto-generated from name on create/update. Workflow public URLs now use team slug (`/w/engineering/intake`) instead of UUID. `GetBySlug` store method with fallback resolution in workflow entry + page renderer. - **Team-scoped workflow API methods** — `API.listTeamWorkflows()`, `createTeamWorkflow()`, `updateTeamWorkflow()`, etc. in workflow-api.js. - **`force_team_id` injection** — `WorkflowHandler.Create` now checks for `force_team_id` context key (same pattern as `CreateTeamTask`). ### Fixed - **Auth rate limiter** — burst lowered from 8 to 5 to ensure rate limiting triggers within 10 rapid sequential requests. - **Package settings assertions** — ICD runner now checks `response.values` instead of top-level keys (matches handler envelope). - **Provider SSE parser** — ICD runner now captures `reasoning_content` deltas (DeepSeek/reasoning models) in addition to `content` deltas. - **Team workflow version test** — captures `version_number` from publish response instead of hardcoding `1`. ## [0.31.1] — 2026-03-18 ### Summary SDK Exercise Surface: fixes 4 SDK bugs (flyout unification, overflow clipping, menu system split, ChatPane standalone gap), then proves them fixed with a dashboard package exercising every `sw.*` primitive with zero component CSS overrides. ### Fixed - **Flyout unification** — 3 competing CSS systems (layout.css, user-menu.css, primitives.css) consolidated into `.sw-menu-flyout` in primitives.css. UserMenu now shares the same flyout class and `data-position` attribute as `sw.menu()`. - **Flyout positioning** — `position:fixed` escape hatch for flyouts inside `overflow:hidden` extension surface containers. Shared `_positionFlyout` helper wired into both `sw.menu()` and `sw.userMenu()`. - **ChatPane standalone** — `ChatPane.mount()` with `standalone: true` now provides complete chat (textarea, model selector, history, streaming, channel creation). Editor package slimmed ~220 lines. - **Flyout text contrast** — menu items used `--text-2` (#9898a8) against `--bg-elevated` (#42424e), giving ~2.8:1 contrast ratio. Bumped to `--text` (#e8e8ed) for ~5.5:1 (WCAG AA compliant). ### New - **Dashboard `.pkg`** — `packages/dashboard/` exercises all 14 SDK primitives: userMenu, menu, toolbar, tabs, dropdown, chat, notes, modal, confirm, toast, on/emit, api, user/isAdmin, theme. - **E2E tests** — `crud/dashboard-package.js` — install, settings, export/import round-trip (7 tests). ## [0.31.0] — 2026-03-18 ### Summary Editor package: the code editor surface is now an installable `.pkg` file (type `full`, tier `browser`). Zero platform special-casing — validates the entire v0.28.7–v0.30.2 extension stack. The editor package lives in `packages/editor/` and is installed via admin UI upload. Core editor surface, template, CSS, JS, and data loader removed. ### New - **Editor `.pkg`** — `packages/editor/` contains `manifest.json`, `js/main.js`, and `css/main.css`. Type `full`, tier `browser`. JS-only rendering replaces server-rendered Go template partials. Dynamic script loading for CodeMirror bundle and ChatPane. Route: `/s/editor?ws=`. - **Editor settings** — manifest declares `font_size`, `tab_size`, `word_wrap` settings configurable via admin packages UI. - **UI state persistence** — open tabs, active tab stored in localStorage per user per workspace (debounced 2s save). - **E2E tests** — `crud/editor-package.js` — install, type/tier verification, settings CRUD, export/import round-trip, core removal verification. ### Removed - `surface-editor` Go template (`server/pages/templates/surfaces/editor.html`) - `editor-surface.js` and `editor-surface.css` (replaced by package assets) - `EditorPageData` struct and `editorLoader` function - Hardcoded `/editor` sidebar link (now rendered by extension nav loop) - `/editor` nginx location rule (served via `/s/editor`) - Editor entry from `registerCoreSurfaces()` and `extensionNavItems()` skip ### Changed - `SeedSurfaces()` now cleans up the old editor core surface row to prevent broken nav links before the package is installed. ## [0.30.2] — 2026-03-18 ### Summary Workflow packages: workflows can be exported as `.pkg` bundles and imported on other instances. Custom stage surfaces load from installed packages via `surface_pkg_id`. Starlark `workflow.*` module provides sandbox access to workflow state. ICD test suite hardened (auth rate limiter, vault UEK pre-warm). 600 tests, 0 failures. ### New - **Workflow package export/import** — `GET /admin/workflows/:id/export` bundles a workflow definition + stages as a downloadable `.pkg` ZIP. `POST /admin/packages/install` with `type: "workflow"` recreates workflow definition + stages from the manifest. Round-trip tested in ICD suite. - **Custom stage surfaces** — `surface_pkg_id` column on `workflow_stages` links a stage to an installed package's JS surface. Template loads the package's `main.js` via dynamic script import and mounts via `WorkflowSurfaces.mount()`. Falls back to built-in mode rendering when no custom surface is assigned. - **Starlark `workflow` module** — sandbox builtins: `workflow.get_definition`, `workflow.get_stage_data`, `workflow.advance`, `workflow.reject`. Gated by `workflow.access` permission. - **`sw.workflow` SDK namespace** — `mount()`, `registerSurface()`, `getContext()`, `advance()`, `reject()` for frontend surface authors. - **Admin workflow builder** — visual stage editor with DnD reorder, form field builder (8 types), export/import buttons, surface package selector. No JSON editing required. - **ICD test fixes** — auth rate limiter burst 30→8 (transport test expects 429 on 10th rapid login). Vault UEK pre-warm on startup via variadic `ProbeAndRepairVault(*crypto.UEKCache)`. ### Changed - `workflow_stages.surface_pkg_id` column added (migration 018, PG + SQLite) - `packages.source` CHECK widened to include `'registry'` (migration 016) - Auth rate limiter: `(5, 30)` → `(5, 8)` — do not increase burst above 9 - ICD test count: 579 → 600 (with Venice BYOK configured) --- ## [0.29.2] — 2026-03-17 ### Summary DB extensions and server-side tool execution. Starlark packages declare namespaced database tables via `db_tables` in the manifest — created on install, dropped on uninstall. A structured Starlark `db` module (`query/insert/update/delete/view/list_tables`) provides namespaced access to extension-owned tables and column-allowlisted platform views. Server-side tool execution wires the `on_tool_call` entry point into the completion tool loop: extensions declare `tools` in their manifest, the tool loop dispatches matched calls to the sandbox. Three changesets (CS0–CS3) plus docs (CS4). ### New - **`db` Starlark module** — structured API for extension-owned tables. Requires `db.read` permission (query/view/list_tables) or `db.write` (insert/update/delete). Tables namespaced as `ext_{pkg_slug}_{name}`. - `db.query(table, filters=None, order=None, limit=100)` → list of dicts - `db.insert(table, row_dict)` → inserted row dict (auto-generates `id`) - `db.update(table, id, partial_dict)` → True - `db.delete(table, id)` → True - `db.list_tables()` → list of logical table names for this package - `db.view(view_name, filters=None, limit=100)` → list of dicts (allowed views: `"users"`, `"channels"`) - **`db_tables` manifest key** — declare extension-owned tables with typed columns and indexes. Tables created on install, dropped on uninstall. ```json { "db_tables": { "logs": { "columns": {"message": "text", "user_id": "text", "count": "int"}, "indexes": [["user_id"]] } } } ``` Supported column types: `text`, `int`/`integer`, `real`/`float`, `bool`/`boolean`, `timestamp`. Auto-columns: `id TEXT PRIMARY KEY`, `created_at` (dialect-correct default). Dialect-correct DDL for both PG (`TIMESTAMPTZ`, `BOOLEAN`) and SQLite (`TEXT`, `INTEGER`). - **Platform views** — column-allowlisted read-only views over platform tables accessible via `db.view(...)`: - `ext_view_users`: `id`, `display_name`, `email` - `ext_view_channels`: `id`, `name`, `type`, `team_id` - **`ext_data_tables` catalog** — tracks logical→physical table mappings per package. Powers `db.list_tables()` and uninstall cleanup. - **Server-side tool execution** — starlark extensions declare `tools` in the manifest. `BuildToolDefs` includes starlark-tier active package tools alongside server tools. `CoreToolLoop` dispatches matched calls to `executeExtensionTool` which calls the `on_tool_call` entry point. ```json { "tools": [{ "name": "search_logs", "description": "Search extension log entries", "parameters": { "type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"] } }] } ``` Entry point: ```python def on_tool_call(call): # call = {"tool_name": "...", "tool_call_id": "...", "arguments": {...}} if call["tool_name"] == "search_logs": rows = db.query("logs", filters={"user_id": call["arguments"]["user_id"]}) return {"results": rows} return {"error": "unknown tool"} ``` - **`BuildExtToolMap`** — pre-loads `map[toolName]*PackageRegistration` per request to avoid DB lookup on each tool dispatch. - **`ExtDataStore`** interface + implementations (`postgres`, `sqlite`) — `RegisterTable`, `ListTables`, `DeletePackageTables`. - **`db.read` and `db.write` permissions** — active in this release (previously listed as "future"). ### Changed - `LoopConfig` gains `Runner *sandbox.Runner` and `ExtTools map[string]*store.PackageRegistration` for extension tool dispatch. All callers updated: `streamCompletion`, `syncCompletion`, multi-model path, `messages.go` regen, scheduler executor. - `BuildToolDefs` (resolve.go) now includes starlark-tier active package tools in addition to browser-tier tools. - `streamWithToolLoop` and `streamModelResponse` accept runner and extTools parameters. - `CompletionHandler` gains `SetRunner(r *sandbox.Runner)` and `buildExtToolMap` helpers. - `AdminInstallExtension` and `AdminUninstallExtension` call `CreateExtTables`/`DropExtTables` for schema lifecycle. - `InstallPackage` and `DeletePackage` (packages.go) call the same schema lifecycle hooks. - Migration `016_packages.sql` extended with `ext_data_tables` catalog table, `idx_ext_data_tables_pkg` index, and `ext_view_users`/ `ext_view_channels` platform views. - `Runner.SetDB(db, isPostgres)` wires the DB handle for `buildModules`. `main.go` calls `starlarkRunner.SetDB` at startup. - Extension permissions table in docs updated: `db.read` and `db.write` now listed as `v0.29.2` (previously "future"). - Extension tiers in enums.md updated: `starlark` now implemented. --- ## [0.29.1] — 2026-03-17 ### Summary API extensions. Starlark packages serve custom JSON endpoints via `api_routes` manifest key, with SSRF-safe outbound HTTP, LLM provider access via BYOK chain, capability-based routing, and config-file provider types. Five changesets (CS0–CS4). ### New - **Extension API routes** (`/s/:slug/api/*path`) — Starlark packages declare routes in `manifest.api_routes` and implement `on_request(req)` entry point. Supports exact and prefix path matching, wildcard methods, and boolean shorthand. JWT-authenticated. - **HTTP outbound module** (`http`) — `http.get/post/put/delete/request` builtins with SSRF protection (private/loopback IP blocking via `net.Dialer.Control`), manifest-based allowlist/blocklist (`network_access.allow/block`), redirect chain validation, 1 MB response cap, 10 s timeout. Requires `api.http` permission. - **Provider module** (`provider`) — `provider.complete(messages, model?, max_tokens?, temperature?)` makes synchronous LLM calls via BYOK chain. `requires_provider` manifest key with optional `provider_config_id` pin and `model` default. Requires `provider.complete` permission. - **`RunContext`** — per-invocation state (user_id, channel_id) passed through `CallEntryPoint` for user-scoped modules. All callers updated: API routes pass JWT user, task executor passes owner, filters pass nil. - **`ProviderResolver` interface** — decouples sandbox from handlers package (avoids circular dependency). `ProviderResolverAdapter` in handlers bridges to `ResolveProviderConfig` + provider registry. - **`capability_match` routing policy** — filters candidates by model capabilities (`required_caps: ["tool_calling", "vision"]`) with optional `prefer_cheapest` sort by output price. Forward-compatible with unknown capability names. - **Config-file provider types** — `PROVIDER_TYPES_FILE` env var points to JSON array of custom provider type definitions. Registers OpenAI-compatible endpoints (Ollama, LiteLLM, vLLM) at startup without code deploy. Built-in IDs are protected from override. - **`provider.complete` permission** — new extension permission constant for LLM completion access from Starlark scripts. ### Changed - `Runner.ExecPackage` and `Runner.CallEntryPoint` signatures gain `*RunContext` parameter. All callers updated (breaking internal API, no external impact). - `Runner.buildModules` receives manifest for module-specific config (http reads `network_access`, provider reads `requires_provider`). - Routing policy admin handler validation switch includes `capability_match`. - `PolicyConfig` gains `RequiredCaps` and `PreferCheapest` fields. - Routing `Context` gains `Capabilities` map for capability-match evaluation. ### Deferred - **Server-side tool execution in completion handler** → v0.29.2. `provider.complete()` covers bare completions; full tool loop (extensions defining tools that execute server-side during chat completions) requires tool registry integration with the sandbox, which aligns with the DB extensions scope. ## [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, `