This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/ICD/extensions.md
2026-03-17 19:32:20 +00:00

6.4 KiB

Extensions

Plugin system with three tiers: Browser JS (client-side), Starlark sandbox (server-side, v0.29.0), Sidecar containers (server-side, future).

User Extensions

Auth: Authenticated user

GET  /extensions                    → {"data": [...UserExtension]}
     ?tier=browser                  (optional filter by tier)
POST /extensions/:id/settings       ← {"is_enabled": bool, "settings": {...}}
     :id = extension UUID           → {"ok": true}
GET  /extensions/:id/manifest       → {"data": <manifest JSON>}
     :id = ext_id (manifest id)
GET  /extensions/tools              → {"data": [...tool schema objects]}

Notes:

  • GET /extensions returns UserExtension objects: the base extension fields plus user_enabled and user_settings overrides.
  • System extensions (is_system: true) cannot be disabled by users. Attempting to set is_enabled: false on a system extension returns 403.
  • GET /extensions/tools returns raw tool schema JSON from all enabled browser extensions' manifest.tools[] arrays.

Extension API Routes (v0.29.1)

Starlark packages serve custom JSON endpoints. Mounted at /s/:slug/api/*path with JWT authentication.

Auth: Authenticated user (JWT — returns 401, not redirect)

ANY  /s/:slug/api/*path             → Starlark on_request(req) response

Route: slug is the package ID. path is matched against the manifest's api_routes declaration.

Manifest api_routes field:

{
  "api_routes": [
    {"method": "GET",  "path": "/status"},
    {"method": "POST", "path": "/webhook"},
    {"method": "*",    "path": "/proxy/*"}
  ]
}
  • method: exact match (case-insensitive), or "*" for any method
  • path: exact match, or trailing "*" for prefix match
  • Boolean true shorthand: all routes forwarded
  • Missing or false: no routes served

Request dict passed to on_request(req):

{
    "method":   "POST",
    "path":     "/webhook",
    "headers":  {"content-type": "application/json", ...},
    "query":    {"page": "1", ...},
    "body":     "{...}",
    "user_id":  "uuid"
}

Expected return dict:

{"status": 200, "headers": {"X-Custom": "val"}, "body": "{...}"}
  • status: int (default 200). Return None for 204 No Content.
  • headers: dict (optional). Content-Type auto-detected if not set.
  • body: string (default "").

Validation pipeline:

  1. Package exists and is enabled
  2. Status is active (not pending_review or suspended)
  3. Tier is starlark
  4. api.http permission is granted
  5. Method+path matches api_routes in manifest

Errors:

  • 401 — no/invalid JWT
  • 403 — package suspended or missing api.http permission
  • 404 — package not found, disabled, or route not declared
  • 400 — package is not starlark tier
  • 500 — Starlark execution error

Admin Extension Management

Auth: Admin role

GET    /admin/extensions             → {"data": [...Extension]}
POST   /admin/extensions             ← {ext_id*, name*, version?, tier?,
                                        description?, author?, manifest?,
                                        is_system?, is_enabled?}
                                     → {"data": Extension} (201)
PUT    /admin/extensions/:id         ← {name?, version?, description?,
     :id = extension UUID               author?, is_system?, is_enabled?,
                                        manifest?}
                                     → {"data": Extension}
DELETE /admin/extensions/:id         → {"ok": true}
     :id = extension UUID

Install defaults: version"0.0.0", tier"browser", manifest{}, scope"global".

Tier validation: tier must be one of: browser, starlark, sidecar.

Duplicate rejection: If ext_id is already installed, returns 409.

Asset Serving

Auth: None (public — script tags can't send Authorization headers)

GET /extensions/:id/assets/*path    → application/javascript
    :id = ext_id (manifest id)

Returns the inline _script field from the extension's manifest. The *path segment is accepted but currently ignored (all requests return the same script). Disabled extensions return 404.

Extension Permissions (v0.29.0+)

Starlark packages declare capabilities in manifest.permissions. Admin must grant each before the package activates.

Permission Module Description
secrets.read secrets Read extension secrets via GlobalConfig
notifications.send notifications Send in-app notifications
filters.pre_completion Register pre-completion filter
api.http http Outbound HTTP requests (v0.29.0: module, v0.29.1: also required for API routes)
provider.complete provider LLM completion calls via BYOK chain (v0.29.1)
db.read db Read extension tables (future)
db.write db Write extension tables (future)

Starlark Modules (v0.29.0+)

Modules injected into the script namespace based on granted permissions:

secrets (requires secrets.read):

  • secrets.get(key) → string or None
  • secrets.list() → list of key names

notifications (requires notifications.send):

  • notifications.send(user_id, title, body?, type?) → True

http (requires api.http, v0.29.1):

  • http.get(url, headers?){"status": int, "headers": dict, "body": string}
  • http.post(url, body?, headers?) → response dict
  • http.put(url, body?, headers?) → response dict
  • http.delete(url, headers?) → response dict
  • http.request(method, url, body?, headers?) → response dict
  • SSRF protection: private/loopback/link-local IPs blocked after DNS
  • Manifest network_access: {"allow": [...]} or {"block": [...]}

provider (requires provider.complete, v0.29.1):

  • provider.complete(messages, model?, max_tokens?, temperature?) → response dict
  • Response: {"content", "model", "finish_reason", "input_tokens", "output_tokens"}
  • Provider resolved via BYOK chain; pinnable via requires_provider.provider_config_id

Builtin Seeding

On startup, SeedBuiltinExtensions() scans extensions/builtin/ for subdirectories containing manifest.json + script.js. Each is upserted as a system extension:

  • New ext_idCreate with is_system: true
  • Same version → skip (idempotent)
  • Different versionUpdate manifest, name, description, author