- Rename Go module switchboard-core → armature (155+ files) - Rename Docker image → gobha/armature - Rename K8s resources, secrets, deployments - Rename Prometheus metrics switchboard_* → armature_* - Rename env vars SWITCHBOARD_ADMIN_* → ARMATURE_ADMIN_* - Rename DB names switchboard_core* → armature* - Update all frontend branding, notification templates, docs - Update CI scripts, e2e tests, Keycloak realm, nginx conf - Rename scripts/switchboard-ca.sh → scripts/armature-ca.sh - Rename k8s/switchboard.yaml → k8s/armature.yaml - Rename chart alerting/dashboard files - Fix: DockerHub push uses env: binding for secret injection - Helm chart updated (name, labels, template functions, dashboard, alerting) - Replace favicon/icon assets with Armature brand No functional changes. Pure mechanical rename + CI fix. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
136 lines
4.2 KiB
Go
136 lines
4.2 KiB
Go
// Package sandbox — modules.go
|
|
//
|
|
// 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"
|
|
|
|
"armature/models"
|
|
"armature/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
|
|
}),
|
|
})
|
|
}
|