# Extension Guide

## Package Types

| Type | Description |
|------|-------------|
| `surface` | A routable UI page rendered in the shell viewport |
| `extension` | Starlark hooks, tools, API routes, DB tables |
| `full` | Both surface and extension combined |
| `library` | Shared code imported by other packages via `lib.require()` |
| `workflow` | Bundled workflow definition |

## Tiers

| Tier | Runs | Capabilities |
|------|------|-------------|
| `browser` | Client-side JS only | DOM access, SDK hooks, no server-side logic |
| `starlark` | Sandboxed server-side | DB, HTTP, notifications, secrets, API routes, realtime |
| `sidecar` | Separate container | Full runtime (future) |

## manifest.json

Every package has a `manifest.json` at its root. Example for a surface:

```json
{
  "id": "my-surface",
  "title": "My Surface",
  "type": "full",
  "tier": "starlark",
  "version": "1.0.0",
  "description": "A custom surface with server-side logic.",
  "icon": "🔧",
  "author": "you",
  "route": "/s/my-surface",
  "auth": "authenticated",
  "permissions": ["db.write"],
  "api_routes": [
    {"method": "GET",  "path": "/items"},
    {"method": "POST", "path": "/items"}
  ],
  "db_tables": {
    "items": {
      "columns": {
        "title": "text",
        "done":  "int"
      },
      "indexes": [["title"]]
    }
  },
  "settings": {
    "page_size": {
      "type": "string",
      "label": "Items Per Page",
      "description": "Number of items shown per page",
      "default": "25"
    }
  }
}
```

### Field Reference

| Field | Required | Description |
|-------|----------|-------------|
| `id` | yes | Unique kebab-case identifier |
| `title` | yes | Display name |
| `type` | yes | `surface`, `extension`, `full`, `library`, `workflow` |
| `tier` | yes | `browser`, `starlark`, `sidecar` |
| `version` | yes | Semver string |
| `description` | no | Short description |
| `icon` | no | Emoji icon for sidebar/menu |
| `route` | surfaces | URL path (e.g., `/s/my-surface`) |
| `auth` | no | `authenticated` (default) or `public` |
| `permissions` | no | Sandbox capabilities: `db.write`, `db.read`, `api.http`, `notifications`, `secrets`, `realtime.publish`, `connections.read`, `workflow.access`, `batch.exec`, `files.read`, `files.write`, `workspace.manage` |
| `api_routes` | no | Array of `{method, path}` for extension HTTP endpoints |
| `api_schema` | no | OpenAPI documentation for extension API routes (see below) |
| `db_tables` | no | Table definitions (see below) |
| `settings` | no | User-configurable settings schema |
| `exports` | no | Functions exported for cross-package calls via `lib.require()` |
| `depends` | no | Array of package IDs this package depends on |
| `slots` | no | Named UI injection points for host surfaces |
| `contributes` | no | Slot contributions into other surfaces |
| `hooks` | no | Event bus subscriptions |
| `config_section` | no | Settings/Admin panel injection (see below) |
| `capabilities` | no | Environment requirements (see below) |
| `user_permissions` | no | Permissions this extension registers for users (see below) |
| `gate_permission` | no | Permission checked before `on_request` executes |
| `schema_version` | no | Integer for additive schema migrations |

## db_tables Schema

Tables are automatically namespaced as `ext_{package_id}_{table_name}`.
Every table gets an auto-generated `id` primary key and `created_at` timestamp.

Column types: `text`, `int`, `vector(N)`.

The `vector(N)` type stores N-dimensional float vectors for similarity
search via `db.query_similar()`. Storage adapts to the backend:
Postgres + pgvector uses native `vector(N)` with HNSW indexes,
Postgres without pgvector uses `JSONB`, SQLite uses `TEXT`. N can be
1–4096. See the [Starlark Reference](STARLARK-REFERENCE) for query API.

```json
"db_tables": {
  "notes": {
    "columns": {
      "title":      "text",
      "body":       "text",
      "creator_id": "text",
      "pinned":     "int"
    },
    "indexes": [
      ["creator_id"],
      ["pinned"]
    ]
  }
}
```

## api_schema (OpenAPI Documentation)

Extensions can optionally declare an `api_schema` array in their manifest to provide rich API documentation. Declared routes appear in Swagger UI at `/api/docs` with parameters, request bodies, and response schemas. Routes without `api_schema` entries still appear as auto-generated stubs.

```json
"api_schema": [
  {
    "path": "/items",
    "method": "GET",
    "summary": "List items",
    "description": "Returns paginated items for the current user",
    "params": {
      "limit": {"type": "integer", "default": 50, "description": "Max results"},
      "offset": {"type": "integer", "default": 0}
    },
    "response": {
      "type": "object",
      "example": {"data": [{"id": "string", "title": "string"}]}
    }
  },
  {
    "path": "/items",
    "method": "POST",
    "summary": "Create item",
    "body": {
      "title": {"type": "string", "required": true},
      "description": {"type": "string"}
    }
  }
]
```

Only `path` and `method` are required. All other fields are optional. Malformed entries are logged and skipped without blocking extension loading.

## Capabilities

Extensions can declare environment requirements via the `capabilities`
manifest field. The kernel validates these at install time.

```json
{
  "capabilities": {
    "required": ["postgres"],
    "optional": ["pgvector", "workspace"]
  }
}
```

- **required** — install is rejected (HTTP 422) if any capability is missing.
- **optional** — install succeeds with a logged warning. Query at runtime
  with `settings.has_capability("pgvector")` to adapt behavior.

Detected capabilities: `pgvector`, `workspace`, `object_storage`, `s3`,
`postgres`. The admin can view detected capabilities at
**Admin > System > Capabilities** or via `GET /admin/capabilities`.

## User Permissions

Extensions can register custom permissions that the admin assigns to
user groups. This controls access to extension features beyond the
sandbox permission model.

```json
{
  "user_permissions": ["image-gen.use", "image-gen.admin"],
  "gate_permission": "image-gen.use"
}
```

- **user_permissions** — on install, these are merged into the kernel's
  permission registry. On uninstall, they are removed. The admin assigns
  them to groups in **Admin > Groups**.
- **gate_permission** — if set, the kernel checks this permission before
  calling `on_request`. Unauthorized users get a 403 without the
  extension code executing.

In Starlark, check permissions inline via `req["permissions"]` or call
`permissions.check(user_id, "image-gen.use")`.

## Extension Composability

Extensions compose with each other through three mechanisms: manifest-declared
**slots** (UI injection points), **contributions** (UI components injected into
those slots), and cross-package **function calls** via `lib.require()`.

### Slots — Host Surfaces Declare Injection Points

A surface declares named slots in its manifest where other extensions can
inject UI components:

```json
{
  "id": "notes",
  "type": "surface",
  "slots": {
    "toolbar-actions": {
      "description": "Toolbar action buttons",
      "context": {
        "noteId": "string",
        "getContent": "function — returns note body",
        "setContent": "function — replaces note body"
      }
    }
  }
}
```

The surface renders slot contents using the SDK helper:

```javascript
html`<div class="toolbar">
    ${sw.slots.renderAll('notes:toolbar-actions', {
        noteId: note.id,
        getContent: () => editor.getValue(),
        setContent: (text) => editor.setValue(text),
    })}
</div>`
```

### Contributions — Extensions Inject UI

An extension declares which slots it contributes to:

```json
{
  "id": "note-dictate",
  "type": "extension",
  "contributes": {
    "notes:toolbar-actions": {
      "label": "Dictate",
      "icon": "🎤",
      "description": "Voice-to-text dictation"
    }
  }
}
```

In its JavaScript, it registers the component:

```javascript
sw.slots.register('notes:toolbar-actions', {
    id: 'note-dictate',
    priority: 200,
    component: ({ noteId, setContent, getContent }) => {
        // ... component implementation
        return html`<button class="sw-btn sw-btn--ghost">🎤</button>`;
    }
});
```

Contributions are soft-coupled — install order doesn't matter. The admin
can view all slots and contributors at **Admin > Packages** or via
`GET /api/v1/admin/slots`.

### Cross-Package Function Calls

Any package that declares `exports` in its manifest can be called by other
packages via `lib.require()`. The caller declares the dependency:

```json
{
  "id": "note-ai",
  "depends": ["llm-bridge"],
  "permissions": ["api.http"]
}
```

In Starlark:

```python
llm = lib.require("llm-bridge")
result = llm.complete([{"role": "user", "content": prompt}])
```

The called function runs with the *target* package's permissions, not the
caller's. This is the same security model as library packages.

### Slot Naming Convention

Slot names follow `{host-package-id}:{slot-name}`. The colon separates the
namespace from the slot. Standard slots for first-party packages:

| Slot | Host | Use Case |
|------|------|----------|
| `notes:toolbar-actions` | notes | Dictation, AI tools, formatting |
| `notes:note-footer` | notes | Related items, AI summary |
| `chat:composer-tools` | chat | Image gen, file attach |
| `chat:message-actions` | chat | Reactions, translate, bookmark |
| `chat:image-actions` | chat | Regen, edit, upscale |

## Starlark Sandbox API

Starlark scripts run server-side with a 1M operation budget and no
filesystem access. See the [Starlark Reference](STARLARK-REFERENCE) for
the complete module catalog, function signatures, and permission gates.

## config_section — Settings Panel Injection

Packages can inject configuration panels into the Settings, Admin, or
Team Admin surfaces. Declare `config_section` in `manifest.json`:

```json
{
  "config_section": {
    "label": "My Config",
    "icon": "M12 2L2 7l10 5 10-5-10-5z",
    "component": "js/config.js",
    "surfaces": ["settings", "admin"],
    "category": "system"
  }
}
```

| Field | Required | Description |
|-------|----------|-------------|
| `label` | yes | Navigation label shown in the sidebar or tab list |
| `icon` | no | SVG path data for the nav icon |
| `component` | no | JS asset path (default: `js/config.js`). Must `export default` a Preact component. |
| `surfaces` | yes | Target surfaces: `"settings"`, `"admin"`, `"team-admin"` |
| `category` | no | Admin surface category tab (default: `"system"`). Ignored for settings/team-admin. |

**How it works:**

1. At page load, the backend scans all enabled packages for `config_section`
   entries targeting the current surface.
2. Matching sections are injected into the page as `__CONFIG_SECTIONS__`.
3. The frontend dynamically imports the component module and renders it
   as an additional tab/section.
4. The component receives `{ packageId, teamId }` as props.

**Example component** (`js/config.js`):

```javascript
const { html } = window;
const { useState, useEffect } = hooks;

export default function MyConfig({ packageId }) {
    const [val, setVal] = useState('');

    useEffect(() => {
        sw.api.ext(packageId).get('/settings').then(r => setVal(r.value));
    }, []);

    return html`<div>
        <label>API Key</label>
        <input value=${val} onInput=${e => setVal(e.target.value)} />
        <button onClick=${() => sw.api.ext(packageId).put('/settings', { value: val })}>Save</button>
    </div>`;
}
```

## Permissions Model

Extensions declare required permissions in `manifest.json`. The admin
must grant each permission before the extension can use the corresponding
sandbox module. Permission status is visible in **Admin > Packages > Permissions**.

See [Permissions & Groups](PERMISSIONS-AND-GROUPS) for the full RBAC
model, user permission slugs, and settings cascade.

## File Structure

```
my-package/
  manifest.json       # required
  js/                 # browser-side JavaScript
    index.js
  css/                # stylesheets
    styles.css
  script.star         # Starlark entry point
  star/               # additional Starlark modules
  assets/             # static assets
  migrations/         # schema migration files
```

## Testing Extensions

1. Build the package: `cd packages && bash build.sh my-package`
2. Upload the `.pkg` file via Admin > Packages > Install.
3. Grant permissions in Admin > Packages > Permissions.
4. Enable the package.
5. If it is a surface, navigate to its route (e.g., `/s/my-package`).

Extension API routes are accessible at `/s/{slug}/api/{path}` and require an authenticated Bearer token.
