Changeset 0.28.0.6 (#178)
This commit is contained in:
@@ -1,34 +1,76 @@
|
|||||||
# Extensions
|
# Extensions
|
||||||
|
|
||||||
Plugin system with three tiers: Browser JS (client-side), Starlark
|
Plugin system with three tiers: Browser JS (client-side), Starlark
|
||||||
sandbox (server-side, future), Sidecar containers (server-side, future).
|
sandbox (server-side, v0.29.0), Sidecar containers (server-side, future).
|
||||||
Currently only Browser tier is implemented.
|
Currently only Browser tier is implemented.
|
||||||
|
|
||||||
### User Extensions
|
### User Extensions
|
||||||
|
|
||||||
|
**Auth:** Authenticated user
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /extensions → { "extensions": [...] }
|
GET /extensions → {"data": [...UserExtension]}
|
||||||
POST /extensions/:id/settings ← { "enabled": true, "config": {...} }
|
?tier=browser (optional filter by tier)
|
||||||
GET /extensions/:id/manifest → full manifest.json
|
POST /extensions/:id/settings ← {"is_enabled": bool, "settings": {...}}
|
||||||
GET /extensions/tools → { "tools": [tool schema objects] }
|
: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.
|
||||||
|
|
||||||
### Admin Extension Management
|
### Admin Extension Management
|
||||||
|
|
||||||
|
**Auth:** Admin role
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /admin/extensions → { "extensions": [...] }
|
GET /admin/extensions → {"data": [...Extension]}
|
||||||
POST /admin/extensions ← { manifest + script content }
|
POST /admin/extensions ← {ext_id*, name*, version?, tier?,
|
||||||
PUT /admin/extensions/:id ← updated manifest/script
|
description?, author?, manifest?,
|
||||||
DELETE /admin/extensions/:id
|
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
|
### Asset Serving
|
||||||
|
|
||||||
|
**Auth:** None (public — script tags can't send Authorization headers)
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /extensions/:id/assets/*path
|
GET /extensions/:id/assets/*path → application/javascript
|
||||||
|
:id = ext_id (manifest id)
|
||||||
```
|
```
|
||||||
|
|
||||||
Public (no auth required). Serves static assets (icons, CSS, JS)
|
Returns the inline `_script` field from the extension's manifest.
|
||||||
from extension bundles.
|
The `*path` segment is accepted but currently ignored (all requests
|
||||||
|
return the same script). Disabled extensions return 404.
|
||||||
|
|
||||||
|
### Builtin Seeding
|
||||||
|
|
||||||
|
On startup, `SeedBuiltinExtensions()` scans `extensions/builtin/`
|
||||||
|
for subdirectories containing `manifest.json` + `script.js`. Each
|
||||||
|
is upserted as a system extension:
|
||||||
|
- **New ext_id** → `Create` with `is_system: true`
|
||||||
|
- **Same version** → skip (idempotent)
|
||||||
|
- **Different version** → `Update` manifest, name, description, author
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -181,6 +181,11 @@ v0.27.5 Team Tasks ✅
|
|||||||
v0.28.0 Platform Polish
|
v0.28.0 Platform Polish
|
||||||
(virtual scroll, KB auto-inject,
|
(virtual scroll, KB auto-inject,
|
||||||
Helm chart, provider model prefs)
|
Helm chart, provider model prefs)
|
||||||
|
│
|
||||||
|
v0.29.0 Starlark Extensions
|
||||||
|
(permissioned server-side plugins,
|
||||||
|
capability-gated module access,
|
||||||
|
admin grant/revoke per-extension)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1025,6 +1030,51 @@ TBD pull-forward: high-value items whose dependencies are met post-tasks.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v0.29.0 — Starlark Extensions: Permissioned Server-Side Plugins
|
||||||
|
|
||||||
|
Server-side extension runtime with capability-gated module access.
|
||||||
|
Depends on: v0.28.0 (platform polish), extension infrastructure (v0.11.0).
|
||||||
|
|
||||||
|
Manifest declares requested permissions (flatpak/Android model):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"permissions": ["http", "store.read", "files.read", "notifications"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Admin reviews and grants/revokes per-permission. Runtime enforces:
|
||||||
|
only approved modules are injected into the Starlark sandbox.
|
||||||
|
|
||||||
|
- [ ] `go.starlark.net` integration (eval loop, timeout, memory ceiling)
|
||||||
|
- [ ] Permission model: manifest declarations, admin grant/revoke, DB schema
|
||||||
|
(`extension_permissions` table: extension_id, permission, granted, granted_by)
|
||||||
|
- [ ] Privileged module library:
|
||||||
|
- `http` — outbound HTTP requests (allowlist/blocklist per extension)
|
||||||
|
- `store` — key/value read/write (extension-namespaced, quota-limited)
|
||||||
|
- `files` — project-scoped file read/write
|
||||||
|
- `notifications` — emit notifications to users
|
||||||
|
- `secrets` — vault-backed per-extension secret storage
|
||||||
|
- [ ] Runtime enforcement: sandbox loader checks granted permissions, injects
|
||||||
|
only approved modules. Denied permissions → clean error, not silent no-op.
|
||||||
|
- [ ] Admin UI: permission review on install, per-extension grant/revoke toggle,
|
||||||
|
audit log of permission changes
|
||||||
|
- [ ] Server-side tool execution in completion handler (Starlark tools run
|
||||||
|
server-side, browser tools run client-side — unified tool schema)
|
||||||
|
- [ ] Extension lifecycle: `install → pending_review → approved → active`
|
||||||
|
- [ ] Migration: `extension_permissions` table
|
||||||
|
- [ ] ICD: update `extensions.md` with Starlark-specific endpoints
|
||||||
|
- [ ] Multi-file asset routing: `ServeExtensionAsset` currently ignores `*path` param
|
||||||
|
and returns inline `_script` for all requests. Starlark extensions need multi-file
|
||||||
|
bundles (source + manifest + static assets). Implement path-based lookup or
|
||||||
|
replace inline `_script` with filesystem/blob serving.
|
||||||
|
- [ ] Deduplicate tool schema extraction: `ListBrowserToolSchemas` (extensions.go) and
|
||||||
|
`ListTools` (completion.go) both independently iterate user extensions, parse
|
||||||
|
manifests, and extract tool schemas. Unify into a shared helper that handles
|
||||||
|
both browser and server-side tool types. The completion handler also has a
|
||||||
|
`hub.IsConnected` guard that the extension handler lacks — reconcile.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## TBD (unscheduled — real features, no immediate need)
|
## TBD (unscheduled — real features, no immediate need)
|
||||||
|
|
||||||
Items that are real but don't yet have a version assignment. Pull left
|
Items that are real but don't yet have a version assignment. Pull left
|
||||||
@@ -1036,9 +1086,9 @@ based on need.
|
|||||||
- Code execution sandbox (server-side, container isolation)
|
- Code execution sandbox (server-side, container isolation)
|
||||||
|
|
||||||
**Extension System — Server Tiers**
|
**Extension System — Server Tiers**
|
||||||
- Starlark runtime integration (Tier 1 — server sandbox)
|
- ~~Starlark runtime integration (Tier 1 — server sandbox)~~ → scheduled v0.29.0
|
||||||
|
- ~~Server-side tool execution in completion handler~~ → scheduled v0.29.0
|
||||||
- Sidecar HTTP tool protocol (Tier 2 — container isolation)
|
- Sidecar HTTP tool protocol (Tier 2 — container isolation)
|
||||||
- Server-side tool execution in completion handler
|
|
||||||
|
|
||||||
**Desktop + Mobile**
|
**Desktop + Mobile**
|
||||||
- Desktop app (Tauri)
|
- Desktop app (Tauri)
|
||||||
@@ -1096,6 +1146,31 @@ based on need.
|
|||||||
- Surface IDE: built-in surface for building surfaces — Go template editor for server-rendered shells, JS/CSS editor for client behavior, live preview in sandboxed region
|
- Surface IDE: built-in surface for building surfaces — Go template editor for server-rendered shells, JS/CSS editor for client behavior, live preview in sandboxed region
|
||||||
- Surface marketplace: share custom surfaces across instances (presentation mode, kanban, form builder, dashboard, etc.)
|
- Surface marketplace: share custom surfaces across instances (presentation mode, kanban, form builder, dashboard, etc.)
|
||||||
- Project-bound surface/pane defaults: project config specifies which panes are available and default layout
|
- Project-bound surface/pane defaults: project config specifies which panes are available and default layout
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pre-1.0 — Code Quality Sweeps
|
||||||
|
|
||||||
|
Codebase-wide refactors required before 1.0. Not tied to a feature version —
|
||||||
|
pull left when touching the affected code, or schedule a dedicated pass.
|
||||||
|
|
||||||
|
**`SELECT *` → Explicit Column Lists**
|
||||||
|
All store implementations use `SELECT * FROM <table>` with positional `rows.Scan()`.
|
||||||
|
If a migration adds a column, every scan breaks silently at runtime (wrong column
|
||||||
|
mapped to wrong field). Every `scanOne`/`scanMany` helper needs explicit column lists.
|
||||||
|
Scope: every store file in `store/postgres/` and `store/sqlite/`.
|
||||||
|
|
||||||
|
**Raw SQL Hunt**
|
||||||
|
Several handlers execute raw SQL via `database.DB.Exec` / `database.TestDB.Exec`
|
||||||
|
instead of going through the store interface. These bypass the dialect abstraction
|
||||||
|
and are a source of PG/SQLite divergence bugs. Known instances:
|
||||||
|
- `handlers/channels.go` — inline channel queries with `pq.Array`
|
||||||
|
- `handlers/workflows.go` — inline unique constraint string matching
|
||||||
|
- `handlers/participants.go` — `isDuplicateErr` string matching (should use `database.IsUniqueViolation`)
|
||||||
|
- `knowledge/` handlers — direct `database.DB.ExecContext` for storage key updates
|
||||||
|
Full sweep: grep for `database.DB.Exec`, `database.TestDB.Exec`, `DB.QueryRow` outside
|
||||||
|
of `store/` packages. Move to store methods or use existing dialect-safe helpers.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v0.27.5 — Team Tasks ✅
|
## v0.27.5 — Team Tasks ✅
|
||||||
|
|||||||
1027
server/handlers/extension_test.go
Normal file
1027
server/handlers/extension_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,11 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||||
)
|
)
|
||||||
@@ -19,6 +21,13 @@ func NewExtensionHandler(stores store.Stores) *ExtensionHandler {
|
|||||||
return &ExtensionHandler{stores: stores}
|
return &ExtensionHandler{stores: stores}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validTiers is the set of accepted extension tier values.
|
||||||
|
var validTiers = map[string]bool{
|
||||||
|
models.ExtTierBrowser: true,
|
||||||
|
models.ExtTierStarlark: true,
|
||||||
|
models.ExtTierSidecar: true,
|
||||||
|
}
|
||||||
|
|
||||||
// ── User endpoints ──────────────────────────────
|
// ── User endpoints ──────────────────────────────
|
||||||
|
|
||||||
// ListUserExtensions returns all enabled extensions for the current user,
|
// ListUserExtensions returns all enabled extensions for the current user,
|
||||||
@@ -147,6 +156,12 @@ func (h *ExtensionHandler) AdminInstallExtension(c *gin.Context) {
|
|||||||
body.Manifest = json.RawMessage("{}")
|
body.Manifest = json.RawMessage("{}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate tier
|
||||||
|
if !validTiers[body.Tier] {
|
||||||
|
c.JSON(400, gin.H{"error": "invalid tier: must be browser, starlark, or sidecar"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check for duplicate ext_id
|
// Check for duplicate ext_id
|
||||||
existing, err := h.stores.Extensions.GetByExtID(c.Request.Context(), body.ExtID)
|
existing, err := h.stores.Extensions.GetByExtID(c.Request.Context(), body.ExtID)
|
||||||
if err != nil && err != sql.ErrNoRows {
|
if err != nil && err != sql.ErrNoRows {
|
||||||
@@ -173,6 +188,10 @@ func (h *ExtensionHandler) AdminInstallExtension(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := h.stores.Extensions.Create(c.Request.Context(), ext); err != nil {
|
if err := h.stores.Extensions.Create(c.Request.Context(), ext); err != nil {
|
||||||
|
if database.IsUniqueViolation(err) {
|
||||||
|
c.JSON(409, gin.H{"error": "extension with ext_id '" + body.ExtID + "' already installed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(500, gin.H{"error": "failed to install extension"})
|
c.JSON(500, gin.H{"error": "failed to install extension"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -197,7 +216,9 @@ func (h *ExtensionHandler) AdminUpdateExtension(c *gin.Context) {
|
|||||||
|
|
||||||
var body struct {
|
var body struct {
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
|
Version *string `json:"version"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
|
Author *string `json:"author"`
|
||||||
IsSystem *bool `json:"is_system"`
|
IsSystem *bool `json:"is_system"`
|
||||||
IsEnabled *bool `json:"is_enabled"`
|
IsEnabled *bool `json:"is_enabled"`
|
||||||
Manifest *json.RawMessage `json:"manifest"`
|
Manifest *json.RawMessage `json:"manifest"`
|
||||||
@@ -210,9 +231,15 @@ func (h *ExtensionHandler) AdminUpdateExtension(c *gin.Context) {
|
|||||||
if body.Name != nil {
|
if body.Name != nil {
|
||||||
ext.Name = *body.Name
|
ext.Name = *body.Name
|
||||||
}
|
}
|
||||||
|
if body.Version != nil {
|
||||||
|
ext.Version = *body.Version
|
||||||
|
}
|
||||||
if body.Description != nil {
|
if body.Description != nil {
|
||||||
ext.Description = *body.Description
|
ext.Description = *body.Description
|
||||||
}
|
}
|
||||||
|
if body.Author != nil {
|
||||||
|
ext.Author = *body.Author
|
||||||
|
}
|
||||||
if body.IsSystem != nil {
|
if body.IsSystem != nil {
|
||||||
ext.IsSystem = *body.IsSystem
|
ext.IsSystem = *body.IsSystem
|
||||||
}
|
}
|
||||||
@@ -241,10 +268,14 @@ func (h *ExtensionHandler) ServeExtensionAsset(c *gin.Context) {
|
|||||||
// path := c.Param("path") // reserved for future multi-file support
|
// path := c.Param("path") // reserved for future multi-file support
|
||||||
|
|
||||||
ext, err := h.stores.Extensions.GetByExtID(c.Request.Context(), extID)
|
ext, err := h.stores.Extensions.GetByExtID(c.Request.Context(), extID)
|
||||||
if err != nil {
|
if err == sql.ErrNoRows {
|
||||||
c.JSON(404, gin.H{"error": "extension not found"})
|
c.JSON(404, gin.H{"error": "extension not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{"error": "failed to fetch extension"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !ext.IsEnabled {
|
if !ext.IsEnabled {
|
||||||
c.JSON(404, gin.H{"error": "extension not enabled"})
|
c.JSON(404, gin.H{"error": "extension not enabled"})
|
||||||
@@ -282,10 +313,14 @@ func (h *ExtensionHandler) GetExtensionManifest(c *gin.Context) {
|
|||||||
extID := c.Param("id")
|
extID := c.Param("id")
|
||||||
|
|
||||||
ext, err := h.stores.Extensions.GetByExtID(c.Request.Context(), extID)
|
ext, err := h.stores.Extensions.GetByExtID(c.Request.Context(), extID)
|
||||||
if err != nil {
|
if err == sql.ErrNoRows {
|
||||||
c.JSON(404, gin.H{"error": "extension not found"})
|
c.JSON(404, gin.H{"error": "extension not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{"error": "failed to fetch extension"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(200, gin.H{"data": ext.Manifest})
|
c.JSON(200, gin.H{"data": ext.Manifest})
|
||||||
}
|
}
|
||||||
@@ -311,6 +346,7 @@ func (h *ExtensionHandler) ListBrowserToolSchemas(c *gin.Context) {
|
|||||||
Tools []json.RawMessage `json:"tools"`
|
Tools []json.RawMessage `json:"tools"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(ext.Manifest, &manifest); err != nil {
|
if err := json.Unmarshal(ext.Manifest, &manifest); err != nil {
|
||||||
|
log.Printf("[extensions] failed to parse manifest for %s: %v", ext.ExtID, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
toolSchemas = append(toolSchemas, manifest.Tools...)
|
toolSchemas = append(toolSchemas, manifest.Tools...)
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ func (s *ExtensionStore) ListAll(ctx context.Context) ([]models.Extension, error
|
|||||||
return s.scanMany(ctx, `SELECT * FROM extensions ORDER BY name`)
|
return s.scanMany(ctx, `SELECT * FROM extensions ORDER BY name`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListEnabled returns all globally enabled extensions regardless of user.
|
||||||
|
// TODO(v0.29.0): Used by Starlark runtime to load server-side extensions at startup.
|
||||||
func (s *ExtensionStore) ListEnabled(ctx context.Context) ([]models.Extension, error) {
|
func (s *ExtensionStore) ListEnabled(ctx context.Context) ([]models.Extension, error) {
|
||||||
return s.scanMany(ctx, `SELECT * FROM extensions WHERE is_enabled = true ORDER BY name`)
|
return s.scanMany(ctx, `SELECT * FROM extensions WHERE is_enabled = true ORDER BY name`)
|
||||||
}
|
}
|
||||||
@@ -112,6 +114,8 @@ func (s *ExtensionStore) ListForUser(ctx context.Context, userID string) ([]mode
|
|||||||
return result, rows.Err()
|
return result, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserSettings returns per-user settings for a specific extension.
|
||||||
|
// TODO(v0.29.0): Used by Starlark runtime to load per-user config for server-side extensions.
|
||||||
func (s *ExtensionStore) GetUserSettings(ctx context.Context, extID, userID string) (*models.ExtensionUserSettings, error) {
|
func (s *ExtensionStore) GetUserSettings(ctx context.Context, extID, userID string) (*models.ExtensionUserSettings, error) {
|
||||||
var eus models.ExtensionUserSettings
|
var eus models.ExtensionUserSettings
|
||||||
err := DB.QueryRowContext(ctx, `
|
err := DB.QueryRowContext(ctx, `
|
||||||
@@ -137,6 +141,8 @@ func (s *ExtensionStore) SetUserSettings(ctx context.Context, eus *models.Extens
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteUserSettings removes per-user settings, reverting to defaults.
|
||||||
|
// TODO(v0.29.0): Exposed via user settings UI when Starlark extensions have per-user config.
|
||||||
func (s *ExtensionStore) DeleteUserSettings(ctx context.Context, extID, userID string) error {
|
func (s *ExtensionStore) DeleteUserSettings(ctx context.Context, extID, userID string) error {
|
||||||
_, err := DB.ExecContext(ctx, `
|
_, err := DB.ExecContext(ctx, `
|
||||||
DELETE FROM extension_user_settings WHERE extension_id = $1 AND user_id = $2`,
|
DELETE FROM extension_user_settings WHERE extension_id = $1 AND user_id = $2`,
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ func (s *ExtensionStore) ListAll(ctx context.Context) ([]models.Extension, error
|
|||||||
return s.scanMany(ctx, `SELECT * FROM extensions ORDER BY name`)
|
return s.scanMany(ctx, `SELECT * FROM extensions ORDER BY name`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListEnabled returns all globally enabled extensions regardless of user.
|
||||||
|
// TODO(v0.29.0): Used by Starlark runtime to load server-side extensions at startup.
|
||||||
func (s *ExtensionStore) ListEnabled(ctx context.Context) ([]models.Extension, error) {
|
func (s *ExtensionStore) ListEnabled(ctx context.Context) ([]models.Extension, error) {
|
||||||
return s.scanMany(ctx, `SELECT * FROM extensions WHERE is_enabled = 1 ORDER BY name`)
|
return s.scanMany(ctx, `SELECT * FROM extensions WHERE is_enabled = 1 ORDER BY name`)
|
||||||
}
|
}
|
||||||
@@ -119,6 +121,8 @@ func (s *ExtensionStore) ListForUser(ctx context.Context, userID string) ([]mode
|
|||||||
return result, rows.Err()
|
return result, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserSettings returns per-user settings for a specific extension.
|
||||||
|
// TODO(v0.29.0): Used by Starlark runtime to load per-user config for server-side extensions.
|
||||||
func (s *ExtensionStore) GetUserSettings(ctx context.Context, extID, userID string) (*models.ExtensionUserSettings, error) {
|
func (s *ExtensionStore) GetUserSettings(ctx context.Context, extID, userID string) (*models.ExtensionUserSettings, error) {
|
||||||
var eus models.ExtensionUserSettings
|
var eus models.ExtensionUserSettings
|
||||||
err := DB.QueryRowContext(ctx, `
|
err := DB.QueryRowContext(ctx, `
|
||||||
@@ -144,6 +148,8 @@ func (s *ExtensionStore) SetUserSettings(ctx context.Context, eus *models.Extens
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteUserSettings removes per-user settings, reverting to defaults.
|
||||||
|
// TODO(v0.29.0): Exposed via user settings UI when Starlark extensions have per-user config.
|
||||||
func (s *ExtensionStore) DeleteUserSettings(ctx context.Context, extID, userID string) error {
|
func (s *ExtensionStore) DeleteUserSettings(ctx context.Context, extID, userID string) error {
|
||||||
_, err := DB.ExecContext(ctx, `
|
_, err := DB.ExecContext(ctx, `
|
||||||
DELETE FROM extension_user_settings WHERE extension_id = ? AND user_id = ?`,
|
DELETE FROM extension_user_settings WHERE extension_id = ? AND user_id = ?`,
|
||||||
|
|||||||
Reference in New Issue
Block a user