Changeset 0.29.0 (#195)
This commit is contained in:
136
server/sandbox/modules.go
Normal file
136
server/sandbox/modules.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Package sandbox — modules.go
|
||||
//
|
||||
// v0.29.0 CS3: Module factories for Starlark sandbox.
|
||||
// Each factory returns a starlarkstruct.Module that the runner
|
||||
// injects into the script namespace based on granted permissions.
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go.starlark.net/starlark"
|
||||
"go.starlark.net/starlarkstruct"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
|
||||
// ─── Secrets Module ──────────────────────────
|
||||
//
|
||||
// Requires permission: secrets.read
|
||||
// Starlark API:
|
||||
// val = secrets.get("api_key") → string or None
|
||||
// all = secrets.list() → list of key names
|
||||
//
|
||||
// Secrets are stored in GlobalConfig under key "ext_secrets:{packageID}"
|
||||
// as a JSON map: {"api_key": "sk-...", "webhook_token": "tok-..."}
|
||||
// Admin sets them via PUT /admin/extensions/:id/secrets.
|
||||
|
||||
// BuildSecretsModule creates the "secrets" module scoped to a package.
|
||||
func BuildSecretsModule(ctx context.Context, stores store.Stores, packageID string) *starlarkstruct.Module {
|
||||
// Pre-load secrets for this package (single query, not per-call)
|
||||
secretMap := loadExtensionSecrets(ctx, stores, packageID)
|
||||
|
||||
return MakeModule("secrets", starlark.StringDict{
|
||||
"get": starlark.NewBuiltin("secrets.get", func(
|
||||
thread *starlark.Thread, b *starlark.Builtin,
|
||||
args starlark.Tuple, kwargs []starlark.Tuple,
|
||||
) (starlark.Value, error) {
|
||||
var key string
|
||||
if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if val, ok := secretMap[key]; ok {
|
||||
return starlark.String(val), nil
|
||||
}
|
||||
return starlark.None, nil
|
||||
}),
|
||||
"list": starlark.NewBuiltin("secrets.list", func(
|
||||
thread *starlark.Thread, b *starlark.Builtin,
|
||||
args starlark.Tuple, kwargs []starlark.Tuple,
|
||||
) (starlark.Value, error) {
|
||||
keys := make([]starlark.Value, 0, len(secretMap))
|
||||
for k := range secretMap {
|
||||
keys = append(keys, starlark.String(k))
|
||||
}
|
||||
return starlark.NewList(keys), nil
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
func loadExtensionSecrets(ctx context.Context, stores store.Stores, packageID string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
configKey := "ext_secrets:" + packageID
|
||||
configVal, err := stores.GlobalConfig.Get(ctx, configKey)
|
||||
if err != nil || configVal == nil {
|
||||
return result
|
||||
}
|
||||
for k, v := range configVal {
|
||||
if s, ok := v.(string); ok {
|
||||
result[k] = s
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ─── Notifications Module ────────────────────
|
||||
//
|
||||
// Requires permission: notifications.send
|
||||
// Starlark API:
|
||||
// notifications.send(user_id, title, body="", type="extension.notify")
|
||||
//
|
||||
// Creates an in-app notification via the notification store.
|
||||
// The extension cannot send email or bypass user preferences —
|
||||
// that's handled by the notification service layer.
|
||||
|
||||
// NotificationSender is the interface the notifications module needs.
|
||||
// Matches notifications.Service.Notify without importing the package.
|
||||
type NotificationSender interface {
|
||||
Notify(ctx context.Context, n *models.Notification) error
|
||||
}
|
||||
|
||||
// BuildNotificationsModule creates the "notifications" module scoped to a package.
|
||||
func BuildNotificationsModule(ctx context.Context, sender NotificationSender, packageID string) *starlarkstruct.Module {
|
||||
return MakeModule("notifications", starlark.StringDict{
|
||||
"send": starlark.NewBuiltin("notifications.send", func(
|
||||
thread *starlark.Thread, b *starlark.Builtin,
|
||||
args starlark.Tuple, kwargs []starlark.Tuple,
|
||||
) (starlark.Value, error) {
|
||||
var userID, title string
|
||||
var body string
|
||||
var notifType string = "extension.notify"
|
||||
|
||||
if err := starlark.UnpackArgs(b.Name(), args, kwargs,
|
||||
"user_id", &userID,
|
||||
"title", &title,
|
||||
"body?", &body,
|
||||
"type?", ¬ifType,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if userID == "" {
|
||||
return nil, fmt.Errorf("notifications.send: user_id is required")
|
||||
}
|
||||
if title == "" {
|
||||
return nil, fmt.Errorf("notifications.send: title is required")
|
||||
}
|
||||
|
||||
n := &models.Notification{
|
||||
UserID: userID,
|
||||
Type: notifType,
|
||||
Title: title,
|
||||
Body: body,
|
||||
ResourceType: "extension",
|
||||
ResourceID: packageID,
|
||||
}
|
||||
|
||||
if err := sender.Notify(ctx, n); err != nil {
|
||||
return nil, fmt.Errorf("notifications.send failed: %w", err)
|
||||
}
|
||||
|
||||
return starlark.True, nil
|
||||
}),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user