# Chat Switchboard — Extension System Specification
**Version:** 0.4
**Status:** All tiers implemented (Browser v0.11.0, Starlark v0.25.0, Sidecar v0.26.0). v0.30.2 adds workflow packages and SDK.
**See also:** [ARCHITECTURE.md](ARCHITECTURE.md), [ROADMAP.md](ROADMAP.md), [PACKAGES.md](PACKAGES.md), [SDK.md](SDK.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 = `
${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 | `