# Chat Switchboard — Extension System Specification **Version:** 0.3 **Status:** Browser tier (Tier 0) implemented in v0.11.0. Starlark and Sidecar tiers planned. **See also:** [ARCHITECTURE.md](ARCHITECTURE.md), [ROADMAP.md](ROADMAP.md) --- ## 1. Philosophy Chat Switchboard is a substrate, not an application. The core provides: authentication, provider routing, message persistence, an event bus, and a rendering surface. Everything else — editing, writing, cluster management, cost tracking, custom renderers — is an extension. The goal is that someone with a problem and some JS (or Go, or Python) can solve it without forking the project. The "modes" — Editor, Article, Chat, Cluster Manager — are extensions that register surfaces, tools, and event handlers. This project doesn't have to build all of them. It has to make them possible. --- ## 2. Extension Tiers | | Tier 0: Browser | Tier 1: Starlark | Tier 2: Sidecar | |---|---|---|---| | **Runs in** | User's browser | Go server (embedded) | Separate container | | **Language** | JavaScript | Starlark (Python subset) | Any | | **Deployed by** | User or Admin push | Admin | Admin | | **Latency** | Zero (client-side) | Low (in-process) | Network hop | | **Can access** | DOM, EventBus, user context | Message data, DB reads (sandboxed) | Anything (HTTP, filesystem, network) | | **Cannot access** | Server internals, other users' data | Network, filesystem, raw SQL | N/A (full access) | | **Trust model** | Same-origin; user-scoped or admin-pushed | Starlark sandbox (no I/O) | Container isolation | | **Use cases** | UI, rendering, shortcuts, client tools, modes | Routing rules, message transforms, logging | RAG, external APIs, webhooks, heavy compute | All three tiers communicate through the EventBus. A browser extension publishes `tool.result.{callId}` the same way a sidecar does — the bus doesn't care where the event originated. **Admin-pushed vs User-installed (Tier 0):** Admin-pushed extensions load for all users (like a managed browser extension policy). User-installed extensions load from user settings and only affect that user's session. Both use the same runtime, same manifest format. The difference is governance, not execution. --- ## 3. Manifest Format Every extension, regardless of tier, is described by a manifest: ```json { "id": "cost-tracker", "name": "Cost Tracker", "version": "1.0.0", "tier": "browser", "author": "jeff", "description": "Real-time token counting and cost estimation", "permissions": [ "events:chat.message.*", "events:model.selected", "dom:input-area", "storage:local" ], "entry": "cost-tracker.js", "hooks": { "chat.message.send": { "priority": 10, "async": false }, "chat.message.received": { "priority": 50, "async": true } }, "tools": [], "surfaces": [], "settings": { "showInline": { "type": "boolean", "label": "Show cost inline", "default": true } } } ``` ### 3.1 Fields - **id**: Unique identifier. Namespaced by convention (`jeff.cost-tracker`). - **tier**: `browser` | `starlark` | `sidecar` - **permissions**: What the extension needs. The loader enforces these. Undeclared access is blocked (Tier 0 via proxy, Tier 1 via sandbox, Tier 2 via API scoping). - **entry**: Browser: JS file path. Starlark: `.star` file. Sidecar: endpoint URL or Docker image. - **hooks**: EventBus events this extension subscribes to, with priority (lower = runs first) and whether the hook is async. - **tools**: LLM-callable tools this extension provides (see §5). - **surfaces**: UI surfaces this extension registers (see §6). - **settings**: User-configurable options, rendered as a form in the extension settings UI. --- ## 4. Browser Extensions (Tier 0) ### 4.1 Lifecycle ``` Install → Load → Init → Active → Disable → Unload ``` **Install**: Admin pushes manifest + JS to server, or user adds from settings. Stored in `extensions` table with `tier = 'browser'`. **Load**: On page load, after `events.js` but before `app.js`, the extension loader injects ` ``` ### 7.2 extensions.js (Core) The extension loader and registry. ~200 lines. Responsibilities: - Fetch enabled extensions from `/api/v1/extensions?tier=browser` - Inject script tags in dependency order - Provide `Extensions.register()` API - Build scoped `ctx` objects per extension (permission enforcement) - Manage surface activation/deactivation - Collect browser tool schemas for the completion handler - Route `tool.call.*` events to the correct handler ### 7.3 Backend Support New tables (migration): ```sql CREATE TABLE extensions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ext_id VARCHAR(100) NOT NULL UNIQUE, -- manifest id name VARCHAR(200) NOT NULL, tier VARCHAR(20) NOT NULL DEFAULT 'browser', manifest JSONB NOT NULL, assets_path TEXT, -- filesystem path for browser JS endpoint TEXT, -- URL for sidecar script TEXT, -- inline Starlark source installed_by UUID REFERENCES users(id), is_system BOOLEAN DEFAULT false, is_enabled BOOLEAN DEFAULT true, scope VARCHAR(20) DEFAULT 'global', -- global, team, personal team_id UUID REFERENCES teams(id), created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); CREATE TABLE extension_user_settings ( extension_id UUID REFERENCES extensions(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id) ON DELETE CASCADE, settings JSONB DEFAULT '{}', is_enabled BOOLEAN DEFAULT true, PRIMARY KEY (extension_id, user_id) ); ``` New endpoints: ``` GET /api/v1/extensions — list enabled for current user GET /api/v1/extensions/:id/manifest — get manifest GET /api/v1/extensions/:id/assets/*path — serve browser JS POST /api/v1/extensions/:id/settings — update user settings POST /api/v1/admin/extensions — install extension DELETE /api/v1/admin/extensions/:id — uninstall PUT /api/v1/admin/extensions/:id — enable/disable, config ``` --- ## 8. EventBus Integration The routing table from `events/types.go` expands: ```go var Routes = map[string]Direction{ // Core "chat.message.*": DirBoth, "user.presence": DirToClient, // Tool execution "tool.call.*": DirToClient, // Server → specific client "tool.result.*": DirFromClient, // Client → server // Surfaces "surface.activated": DirLocal, // Client-only "surface.deactivated": DirLocal, // Extension lifecycle "extension.loaded": DirLocal, "extension.error": DirLocal, // Cross-extension (cluster manager example) "cluster.node.*": DirLocal, // Between browser extensions "cluster.alert.*": DirBoth, // Could notify server too } ``` Browser extensions use `Events.on()` and `Events.emit()` — the same API the core app uses. `DirLocal` events stay in the browser. `DirBoth` and `DirFromClient` events cross the WebSocket. --- ## 9. Built-in Tools These ship with core because multiple modes and services depend on them: | Tool | Tier | Description | |---|---|---| | `web_search` | Sidecar | Search provider abstraction (SearXNG, Brave, DuckDuckGo) | | `url_fetch` | Server | Retrieve and extract content from a URL | | `note_create` | Server | Create a note with title, content, folder, tags | | `note_search` | Server | Full-text search across user's notes | | `note_update` | Server | Update note content | | `note_list` | Server | List notes with optional folder filter | | `kb_search` | Server | Semantic search across knowledge bases (future) | | `task_create` | Server | Schedule a new task (future) | Extension-provided tools (not core, expected early extensions): - `read_file`, `write_file`, `search_replace` (Editor mode) - `kubectl_get`, `ceph_status`, `node_ssh` (Cluster manager) - `generate_image` (Image gen, browser or sidecar) - `speak_text`, `transcribe_audio` (STT/TTS, browser via Web Speech API) --- ## 10. Design Principles 1. **Extensions are first-class.** A mode or feature implemented as an extension is indistinguishable from one built into core. No second-class citizens. 2. **The EventBus is the spine.** Extensions don't import each other. They publish and subscribe to events. This is how cluster manager extensions cooperate without knowing about each other at build time. 3. **Tools are location-transparent.** The LLM sees a tool schema. It doesn't know if the tool runs in the browser, in a Starlark sandbox, or in a container. The routing is the platform's problem. 4. **Permissions are declared, not discovered.** An extension says what it needs upfront. The loader enforces it. No ambient authority. 5. **The core stays small.** Auth, provider routing, message persistence, event bus, extension loader. That's core. Everything else is an extension — even if it ships with the project. 6. **Progressive capability.** A browser extension with zero tools and zero surfaces is just an EventBus subscriber. Add a tool and the LLM can call it. Add a surface and it becomes a mode. The same manifest format scales from "show token count" to "full IDE." --- ## Appendix A: Custom Renderers Custom renderers are the simplest useful browser extension. They intercept message content rendering to handle specific content types — no tools, no surfaces, just a pattern match and a render function. ```js Extensions.register({ id: 'collapsible-code', init(ctx) { ctx.renderers.register('collapsible-code', { // Match fenced code blocks with >10 lines pattern: /^```(\w+)?\n([\s\S]{10,}?)```$/gm, render(match, container) { const lang = match[1] || 'text'; const code = match[2]; const lines = code.split('\n'); const details = document.createElement('details'); details.innerHTML = ` ${lang} — ${lines.length} lines
${escapeHtml(code)}
`; container.replaceWith(details); } }); } }); ``` Expected first-party renderer extensions (ship with core or as official): | Renderer | Matches | Renders | |---|---|---| | `collapsible-code` | Code blocks > N lines | `
` with expand/collapse | | `html-preview` | ```html blocks | Sandboxed iframe with live preview | | `mermaid` | ```mermaid blocks | SVG diagram via mermaid.js | | `latex-math` | `$...$` and `$$...$$` | Rendered math via KaTeX | | `doc-preview` | Generated HTML/PDF content | Preview panel with download link | These are all ~30-50 line browser extensions. They don't need tool calling or surfaces. They just pattern-match content in rendered messages. --- ## Appendix B: Model Roles Extensions (and core services like compaction) may need to call LLMs for their own purposes — summarization, embedding, classification — independent of the user's selected chat model. **Model roles** are named slots that resolve to a specific provider+model at runtime. ### B.1 Role Definition ``` ┌──────────────────────────────────────────────────────────────┐ │ Role │ Purpose │ Typical Model │ ├──────────────────────────────────────────────────────────────┤ │ utility │ Summarization, routing │ gpt-4o-mini │ │ │ classification, triage │ deepseek-chat │ │ embedding │ Vector generation for │ text-embedding-3 │ │ │ KB search, note search │ nomic-embed-text │ └──────────────────────────────────────────────────────────────┘ ``` > **Note:** The `generation` role (image/media) was removed in v0.10.2. > Image generation will be extension-managed with its own provider config > and API surface, not a named role slot. ### B.2 Configuration Admin configures roles in global settings, with optional fallback chain: ```json { "model_roles": { "utility": { "primary": { "provider_config_id": "abc", "model_id": "gpt-4o-mini" }, "fallback": { "provider_config_id": "def", "model_id": "deepseek-chat" } }, "embedding": { "primary": { "provider_config_id": "abc", "model_id": "text-embedding-3-small" }, "fallback": null } } } ``` Teams can override roles for their scope. Personal overrides TBD. ### B.3 Extension API ```js // Browser extension requests a utility completion const result = await ctx.models.complete('utility', { messages: [{ role: 'user', content: 'Summarize this in 2 sentences: ...' }] }); // Server-side: extension requests an embedding vec, err := models.Embed(ctx, "embedding", "text to embed") ``` The caller doesn't know which model or provider is being used. The role system resolves it, tries fallback if primary fails, and logs usage against the role for cost tracking. ### B.4 Core Consumers | Consumer | Role | Purpose | |---|---|---| | Compaction service | `utility` | Summarize long conversations | | Smart routing | `utility` | Classify intent, pick best model | | Summarize & Continue | `utility` | Conversation compaction (v0.10.2) | | Knowledge bases | `embedding` | Generate vectors for similarity search | | Note search (future) | `embedding` | Semantic search across notes | | Image gen extension | _(extension-managed)_ | Uses own provider config, not a role | ### B.5 Relationship to Smart Routing Model roles and smart routing are complementary: - **Model roles**: "I need *a* cheap model for summarization" → resolves to a specific provider+model based on admin config. - **Smart routing**: "Route *this user's chat* to the best model" → policy engine that picks based on capabilities, cost, latency, team rules. Roles are for background/system tasks. Routing is for user-facing chat. Both use the same provider infrastructure and cost tracking.