# Chat Switchboard — Extension System Specification
**Version:** 0.1 draft
**Status:** Design
**Applies to:** v0.7.x+
**Companion to:** ARCHITECTURE.md (core services + terminology)
---
## 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" Jeff envisions — Editor,
Article, Chat, Cluster Manager — are just extensions that register surfaces,
tools, and event handlers. This project doesn't have to build all of them.
It just 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, LocalStorage, 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.
---
## 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 access to. The loader enforces
these. Undeclared access is blocked (Tier 0 via proxy, Tier 1 via sandbox,
Tier 2 via API scoping).
- **entry**: For browser: JS file path. For starlark: `.star` file. For
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 list 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
- Provide `Extensions.list()`, `Extensions.get()`, `Extensions.settings()`
### 7.3 Backend Support
New tables (migration 006):
```sql
CREATE TABLE extensions (
-- already exists from 001, but needs columns:
tier VARCHAR(20) NOT NULL DEFAULT 'browser', -- browser, starlark, sidecar
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
);
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 — update (enable/disable, config)
```
### 7.4 Tool Registry Integration
The completion handler's tool collection expands:
```go
func (h *CompletionHandler) collectTools(userID string, configID string) []ToolSchema {
var tools []ToolSchema
// 1. Model-native tools (if provider supports them)
caps := h.getModelCapabilities(model, configID)
if !caps.ToolCalling {
return nil // Model doesn't support tools, skip everything
}
// 2. Server-side extension tools (Starlark + Sidecar)
tools = append(tools, h.serverTools(userID)...)
// 3. Browser tools (only if WebSocket connected)
if h.hub.IsConnected(userID) {
tools = append(tools, h.browserTools(userID)...)
}
return tools
}
```
---
## 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. Events with `DirBoth` or `DirFromClient` cross the
WebSocket. `DirLocal` events stay in the browser. This means cluster
manager extensions can coordinate locally at zero latency, and only
publish to the server when something needs persistence or notification.
---
## 9. Implementation Roadmap
### Phase A: Foundation
- [ ] `extensions.js` — loader, registry, scoped context
- [ ] `Extensions.register()` API with permission enforcement
- [ ] Manifest format parser and validator
- [ ] Admin endpoints for extension management
- [ ] Asset serving endpoint
- [ ] Extension settings UI in Settings modal
### Phase B: Browser Tools
- [ ] `ctx.tools.handle()` API for browser tool registration
- [ ] Tool schema collection in completion handler
- [ ] `tool.call.*` / `tool.result.*` WebSocket routing
- [ ] Timeout and error handling for browser tools
- [ ] Tool execution in EventBus `WaitFor` pattern
- [ ] Tool use display in chat messages
### Phase C: Surfaces
- [ ] Surface registration and activation API
- [ ] Mode selector in sidebar
- [ ] `ctx.ui.replace()` / `ctx.ui.restore()` for region management
- [ ] `surface.activated` / `surface.deactivated` events
### Phase D: First Extensions
- [ ] Cost tracker (browser, proof of concept)
- [ ] Slash commands (browser, message transforms)
- [ ] Custom renderers (browser, mermaid/latex/etc)
- [ ] Editor mode (browser, with git provider tools)
### Phase E: Server-Side Tiers
- [ ] Starlark runtime integration
- [ ] Sidecar HTTP tool protocol
- [ ] Server-side tool execution in completion handler
- [ ] Web search as sidecar extension
---
## 10. Design Principles
1. **Extensions are first-class.** The system is designed so that 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 the 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 in another data center. 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."