# Changelog ## [0.35.1] — 2026-03-20 ### Summary Data Portability FE Wiring: connects the v0.34.0 backend (export, import, ChatGPT import, GDPR delete, backup CronJob) to the frontend. New Settings → Data & Privacy section. Admin team export button. ### New - **Settings → Data & Privacy section** — new nav link and full-page section with four features: - **Download My Data** — streams `GET /export/me` as `.switchboard` zip blob download (conversations, notes, memories, projects, files, settings, usage). - **Import Data** — file picker for `.switchboard` / `.zip` archives, uploads to `POST /import/me` with import/skip/error counts in toast. - **Import from ChatGPT** — file picker for ChatGPT export zips, uploads to `POST /import/chatgpt`, maps conversations to direct chats. - **Delete My Account** — danger zone with password confirmation, calls `DELETE /me`, shows anonymized username, clears tokens, and redirects to `/login`. - **Admin team export button** — 📦 icon button on each team row in Admin → Teams. Calls `GET /admin/teams/:id/export` and downloads `.switchboard` archive. ### New files - `src/js/data-portability.js` — Settings section renderer + admin team export helper. ### Changed - `settings.html` — added nav link, section title, content area, script tag, and dynamic section dispatch for `data`. - `ui-admin.js` — added export button to team list action cells. ## [0.35.0] — 2026-03-19 ### Summary Workflow Product: transforms the workflow engine from a linear stage-runner into a product-grade automation tool. Conditional routing, progressive forms, Starlark data pipeline, structured review with comments, and a monitoring dashboard with SLA tracking. ### New - **Conditional routing engine** — `transition_rules.conditions[]` evaluated against accumulated `stage_data`. Supports eq/neq/gt/lt/ gte/lte/in/contains/exists operators. First match wins; no match falls back to ordinal+1. Backward compatible. - **`workflow_route` tool** — persona-callable tool for AI-triggered routing to named stages. Supports forward and backward jumps (loop stages). Records routing decisions in `stage_data._route_history_latest`. - **Starlark `workflow.route()`** — extension-driven routing for confidence-based escalation patterns. - **`on_advance` hook** — Starlark entry point fires synchronously after stage transition. Can enrich/transform `stage_data` or reject the transition. Configured via `transition_rules.on_advance`. - **Progressive forms** — `form_template.fieldsets[]` for multi-step forms with next/back navigation and step indicators. - **Conditional fields** — `FormField.condition` (when/op/value) controls field visibility. Server-side validation skips hidden fields. - **Workflow branding** — `accent_color`, `logo_url`, `tagline` now applied to workflow visitor pages via CSS custom properties. - **Structured review** — side-by-side layout with data card + actions panel. Keyboard shortcuts: Ctrl+Enter approve, Ctrl+Shift+Enter reject. - **Review comments** — `POST /workflow-assignments/:id/comment` appends reviewer notes to the assignment's `review_comments` JSONB array. - **Monitoring dashboard** — admin endpoints for active instances, stage funnels, and stale detection. SLA computed from `stage_entered_at + sla_seconds` vs NOW(). - **SLA tracking** — `sla_seconds` column on workflow_stages, `stage_entered_at` on channels. Dashboard shows remaining time with green/yellow/red indicators. ### Schema - `workflow_stages.sla_seconds INTEGER` (018 modified in-place) - `channels.stage_entered_at TIMESTAMPTZ/TEXT` (005 modified in-place) - `workflow_assignments.review_comments JSONB/TEXT DEFAULT '[]'` (018) - Index `idx_channels_workflow_active` (005) - DB rebuild required (wipe data directory) ### New files - `server/workflow/routing.go` — conditional routing engine - `server/handlers/workflow_hooks.go` — on_advance hook firing - `server/handlers/workflow_monitor.go` — monitoring dashboard - `src/js/workflow-monitor.js` — admin monitoring frontend - `packages/icd-test-runner/js/crud/workflow-product.js` — E2E tests ## [0.33.0] — 2026-03-19 ### Summary Observability: structured logging, Prometheus metrics, OpenAPI docs, Grafana dashboards, alerting rules, and a built-in admin monitoring dashboard. Operate the platform without reading Go source code. ## [0.34.0] — 2026-03-19 ### Summary Data Portability: bulk export/import for user and team data, GDPR "download my data" and "delete my data" with cascade + audit trail, ChatGPT conversation import, and Kubernetes backup/restore CronJob manifests. ### New - **User data export** (`GET /api/v1/export/me`) — streams a `.switchboard` zip archive containing all user-scoped entities: channels, messages, notes, note links, memories, projects, project associations, workspaces, workspace files, file attachments (blobs), folders, user model settings, notification preferences, usage entries, persona groups, and persona group members. Manifest includes entity counts and scope metadata. Sensitive fields (password hashes, vault keys, provider config IDs) stripped via `export.Sanitize*` functions. File blobs capped at 100 MB per file, 500 MB total archive, 10K files. - **User data import** (`POST /api/v1/import/me`) — accepts `.switchboard` zip upload, validates manifest and format version, imports entities in FK-dependency order with UUID dedup (skip if ID exists). User ID remapping for cross-instance portability (source user → current user). Returns imported/skipped counts and error list. - **ChatGPT import** (`POST /api/v1/import/chatgpt`) — accepts ChatGPT export zip containing `conversations.json`. Maps ChatGPT conversation format (alternating author roles, message parts) into Switchboard channels + messages. Handles `system`, `user`, `assistant`, and `tool` roles. Title becomes channel name. - **GDPR account deletion** (`DELETE /api/v1/me`) — requires explicit `"confirm": "DELETE"` + password verification. Prevents deleting the last admin. Cascade: soft-deletes user data (messages anonymized, channels/notes/memories/projects deleted), revokes tokens, deletes personal provider configs, anonymizes user record to `deleted-user-{hash}`. Full audit trail. - **Team data export** (`GET /api/v1/admin/teams/:id/export`) — admin endpoint streaming a `.switchboard` archive of all team-scoped entities: channels, messages, members, personas, persona KBs, knowledge bases, KB documents, workflows, workflow versions/stages, groups, group members, resource grants, and projects. Sanitizes personas (strips provider binding), workflows (strips webhook secrets). - **Team data import** (`POST /api/v1/admin/teams/:id/import`) — admin endpoint accepting `.switchboard` archive for team data restoration. - **Backup/restore CronJob** — Helm chart templates for scheduled `pg_dump` backups. `cronjob-backup.yaml` runs on configurable schedule (default: daily at 02:00), writes gzip-compressed dump to PVC with configurable retention (default: 7 backups). Optional S3 offload. `pvc-backup.yaml` for backup storage. Gated by `backup.enabled` in `values.yaml`. ### New files - `server/export/format.go` — `.switchboard` archive format: zip with `manifest.json`, `data/*.json` entity files, `files/` blob directory. `ArchiveWriter` with size tracking, `ArchiveReader` with manifest validation. - `server/export/entities.go` — per-entity sanitization: `SanitizeUser` (strips password hash, vault fields), `SanitizeChannels` (strips provider config), `SanitizeMessages`, `SanitizeChannelModels`, `SanitizeUserModelSettings`, `SanitizeUsageEntries`, `SanitizeWorkspaces` (strips git creds), `SanitizePersonas`, `SanitizeWorkflows` (strips webhook secrets). - `server/export/chatgpt.go` — ChatGPT `conversations.json` parser. `ParseChatGPTExport` reads zip, finds `conversations.json`, maps to `ChatGPTConversation` structs with `ChatGPTMessage` (author role, content parts, create_time). - `server/handlers/export_data.go` — `DataExportHandler` with `ExportMyData` and `ExportTeam` endpoints. - `server/handlers/import_data.go` — `DataImportHandler` with `ImportMyData`, `ImportTeam`, and `ImportChatGPT` endpoints. FK-dependency-ordered import with dedup. - `server/handlers/gdpr.go` — `GDPRHandler` with `DeleteMyAccount`. - `server/store/postgres/export.go` — `ExportStore` Postgres implementation: 25+ batch-read queries for user/team export, import methods with `ON CONFLICT DO NOTHING`, `SoftDeleteUserData`, `AnonymizeUser`, `DeleteUserTokens`, `CountActiveAdmins`. - `server/store/sqlite/export.go` — `ExportStore` SQLite implementation with identical interface. - `chart/templates/cronjob-backup.yaml` — Kubernetes CronJob for scheduled pg_dump with gzip compression and retention cleanup. - `chart/templates/pvc-backup.yaml` — PersistentVolumeClaim for backup storage (gated by `backup.persistence.enabled`). ### Schema - No new migrations — uses existing tables only. - `ExportStore` added to `store.Stores` struct. ### Changed - `store/interfaces.go` — added `ExportStore` interface with 25+ methods for batch export reads, import writes, and GDPR operations. - `chart/values.yaml` — added `backup` section with `enabled`, `schedule`, `retention`, `persistence`, and `s3Offload` config. - `main.go` — wired `DataExportHandler`, `DataImportHandler`, `GDPRHandler` routes under authenticated and admin groups. ### New - **Structured logging (`slog`)** — `LOG_FORMAT=json|text`, `LOG_LEVEL=debug|info|warn|error`. JSON handler for production, text handler for development. Request ID, method, path, status, latency, client IP, and user ID on every request log line. - **Request ID middleware** — `X-Request-Id` UUID header generated per request (or forwarded from upstream). Propagated through completion chain as correlation ID. - **Prometheus `/metrics` endpoint** — `switchboard_http_requests_total`, `switchboard_http_request_duration_seconds`, `switchboard_websocket_connections`, `switchboard_completion_tokens_total`, `switchboard_completions_total`, `switchboard_completion_duration_seconds`, `switchboard_provider_status`, `switchboard_db_open_connections` (+ `_in_use`, `_idle`, `_wait_count`, `_wait_duration`), `switchboard_task_executions_total`, `switchboard_sandbox_executions_total`. Uses `c.FullPath()` for path patterns (no cardinality explosion). - **OpenAPI 3.0.3 spec** — hand-curated `openapi.yaml` covering auth, channels, completions, health, and admin API groups. Served at `/api/docs/openapi.yaml`. - **Swagger UI** — `/api/docs` renders interactive API browser via CDN. System-respecting light/dark mode with WCAG AA 4.5:1+ contrast ratios. - **Grafana dashboard** — `switchboard-overview.json` with request rate, latency percentiles, provider health, token usage, DB pool, Go runtime. - **PrometheusRule alerting** — OOM recovery, provider down, pool exhaustion, high error rate, task failure spike. - **ServiceMonitor** — Helm template for Prometheus Operator discovery (gated: `monitoring.serviceMonitor.enabled`). - **Admin monitoring dashboard** — `GET /api/v1/admin/dashboard` aggregates provider health, 24h usage totals, DB pool stats, WS connections, Go runtime (goroutines, heap, sys, GC count, Go version), storage status, and recent errors. Auto-refreshes every 30s. - **ICD observability tests** — 6 tests covering `/metrics`, `/api/docs`, `/api/docs/openapi.yaml`, `X-Request-Id` generation, `X-Request-Id` passthrough, and admin dashboard. ### Changed - **`middleware/logging.go`** — rewritten from `gin.Logger` wrapper to `slog.Info` with structured fields. `SkipPaths` still honored. - **`config/config.go`** — added `LogFormat`, `LogLevel` fields with `LOG_FORMAT`, `LOG_LEVEL` env var loading. - **`events/ws.go`** — WebSocket connect/disconnect now updates `switchboard_websocket_connections` gauge. - **`health/accumulator.go`** — flush cycle updates `switchboard_provider_status` gauge per provider config. - **`handlers/completion.go`** — instrumented with token counters, completion latency histogram, and status counter. - **Dockerfile** — Go 1.22 → Go 1.23 (required by `prometheus/client_golang`). - **`nginx.conf`** — added `location = /metrics` proxy rule. - **Admin monitoring tab** — landing page changed from Usage to Dashboard. - **Helm `values.yaml`** — added `monitoring` section (serviceMonitor, grafanaDashboard, prometheusRule — all `enabled: false` by default) and `logging.format`, `logging.level`. ## [0.32.0] — 2026-03-19 ### Summary Multi-Replica HA: run 2+ backend replicas across nodes for node-level availability. All shared mutable state moves from in-process memory to Postgres — task scheduler uses `FOR UPDATE SKIP LOCKED` for atomic claim (no leader election), WebSocket tickets and rate limit counters become PG tables, and `SendToUser` is replaced by bus-routed `PublishToUser` for cross-pod event delivery. New `/healthz/ready` and `/healthz/live` probe endpoints. Helm defaults to 2 replicas with pod anti-affinity. ### New - **`FOR UPDATE SKIP LOCKED` task scheduler** — every replica polls; PG serializes task handoff atomically. `ClaimDueTask` replaces `ListDue`. `CreateRunExclusive` conditional insert prevents double-execution. Startup jitter (0–15s) staggers replicas. - **PG ticket store** — `ws_tickets` table replaces in-memory `sync.Map`. Atomic `DELETE ... RETURNING` for single-use validation. 30s TTL, reaper piggybacked on scheduler poll tick. - **PG rate limiter** — `rate_limit_counters` table with fixed-window `INSERT ON CONFLICT DO UPDATE`. Fail-open policy: DB errors allow the request rather than blocking auth. Cleanup piggybacked on scheduler poll tick. - **Cross-pod WebSocket delivery** — `Hub.PublishToUser` routes events through the bus → `pg_broadcast` → remote pod `publishLocal`. `TargetUserID` field on `Event` for targeted filtering. `tool.result.*` routing changed to `DirBoth` so browser tool results cross pods for `WaitFor`. - **Health probes** — `/healthz/ready` (PG ping, 2s timeout) and `/healthz/live` (process alive, no deps). - **Helm HA** — `backend.replicaCount: 2`, pod anti-affinity (`preferredDuringScheduling`, `kubernetes.io/hostname`), readiness and liveness probes pointed at new healthz endpoints. - **Schema `020_ha.sql`** — `ws_tickets` and `rate_limit_counters` tables (PG + SQLite). ### Changed - **`SendToUser` → `PublishToUser`** — 18 call sites across 10 files migrated to bus-routed delivery for cross-pod reach. - **`ListDue` removed from `TaskStore`** — replaced by `ClaimDueTask` (atomic claim) + `CreateRunExclusive` (belt-and-suspenders guard). - **Rate limiter middleware** — rewritten to accept `store.RateLimitStore` instead of managing in-memory state. Constructor signature changed: `NewRateLimiter(store, rate, burst)`. - **`tool.result.*` routing** — `DirFromClient` → `DirBoth` to enable cross-pod `WaitFor`. WS subscriber explicitly filters `tool.result` to prevent re-sending to clients. ### Removed - **`events/tickets.go`** — in-memory `TicketStore` replaced by `store.TicketStore` (PG/SQLite) + `TicketValidatorAdapter`. - **In-memory rate limiter** — visitor map and cleanup goroutine replaced by PG-backed store. ### Fixed - **Team workflow route registration** — v0.31.2 team-scoped workflow CRUD routes (`/teams/:teamId/workflows`) were missing from `main.go` route registration. 12 routes added to `teamScoped` group. ## [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, `