Feat v0.6.3 dead code sweep (#38)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-sqlite (push) Successful in 2m46s
CI/CD / test-go-pg (push) Successful in 2m47s
CI/CD / build-and-deploy (push) Successful in 26s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #38.
This commit is contained in:
2026-03-31 12:37:47 +00:00
committed by xcaliber
parent a887b4c78b
commit 3d4228f868
130 changed files with 522 additions and 1215 deletions

View File

@@ -2,6 +2,47 @@
All notable changes to Switchboard Core are documented here.
## v0.6.3 — Dead Code Sweep + Registry Fix
Pre-fork hardening: fix broken registry install, add registry settings UI,
clean up dead code, narrow default bundle.
### Fixed
- **Registry install**: SDK was sending `{ url }` but the Go handler expects
`{ download_url }`, causing every registry Install click to return 400.
### Added
- **Package Registry settings**: New "Package Registry" section in Admin >
Settings with a URL input field for configuring the external registry.
- **`scripts/generate-registry.sh`**: Shell script that reads `.pkg` ZIPs,
extracts manifests, and emits a `registry.json` for self-hosted discovery.
- **`docs/PACKAGE-REGISTRY.md`**: Documents registry JSON format, admin
configuration, script usage, and self-hosting setup.
### Removed
- **Dead Go code**: Orphaned channel/message type comments from
`interfaces.go`, unused `roleFilterType()` from `pages.go`, stale version
comment from `main.go`.
- **Dead vendor JS**: `marked.min.js` (40KB) and `purify.min.js` (22KB) —
unused vendor copies with zero production imports. Removed cache entries
from `sw.js`.
- **`dev.html`**: 676-line standalone dev gallery, not referenced by anything.
- **Pre-fork version comments**: Stripped all chat-switchboard version
annotations across ~140 files — standalone comments, inline suffixes,
section headers, and config field docs. Reworded to describe features
rather than reference pre-fork version numbers.
### Changed
- **Default bundle narrowed** from 10 packages to 5: `notes`, `chat`,
`chat-core`, `mermaid-renderer`, `schedules`. All other packages remain
available via `BUNDLED_PACKAGES=*` or explicit list.
---
## v0.6.2 — Docs Polish + Dynamic OpenAPI
Docs surface polish and dynamic OpenAPI spec with extension route merging.

View File

@@ -1,6 +1,6 @@
# Switchboard Core — Roadmap
## Current: v0.6.2 — Docs Polish + Dynamic OpenAPI
## Current: v0.6.3 — Dead Code Sweep + Registry Fix
Self-hosted extensible platform. Auth, identity, packages, Starlark sandbox,
storage, realtime, and ops are kernel primitives. Everything else is an extension.
@@ -81,16 +81,16 @@ Pure cleanup. No behavior changes except fixing the broken registry install flow
| Step | Status | Description |
|------|--------|-------------|
| Fix registry install | | SDK sends `{ url }`, handler expects `{ download_url }`. Fix `api-domains.js` to send `{ download_url: url }`. Every Install click currently returns 400. |
| Registry settings UI | | Add "Package Registry" section to admin settings with URL input field. Only way to configure registry today is a raw `PUT /api/v1/admin/settings/package_registry` — no user will find it. |
| Registry tooling + docs | | `scripts/generate-registry.sh` scans a directory of `.pkg` files and emits registry JSON. `docs/PACKAGE-REGISTRY.md` documents the format. |
| Delete dead kernel Go | | `store/interfaces.go:178183` — orphaned ChannelListFilter comments. `pages/pages.go:922930``roleFilterType()` + template registration (chat vestige, maps nonexistent roles). `main.go:67` — orphaned provider-type comment. |
| Delete dead vendor JS | | `vendor/marked.min.js` and `vendor/purify.min.js` — 62KB, zero production imports. Only referenced in test helpers. |
| Delete `dev.html` | | 676 lines, not imported by anything. Dead. |
| Remove `dashboard` from default bundle | | Requires `legacy-sdk` (doesn't exist), auto-installs as dormant on fresh installs. Confusing. Remove from `defaultBundledPackages`. |
| Remove `hello-dashboard` | | Proof-of-concept from early development. Move to `examples/` or delete. |
| Strip stale version comments | | 60+ files have `// v0.29.x:`, `// v0.33.x:` pre-fork comments. Single sed pass. |
| Narrow default bundle | | Default: `notes`, `chat`, `chat-core`, `mermaid-renderer`, `schedules`. Everything else available via registry or `BUNDLED_PACKAGES=*`. |
| Fix registry install | | SDK sends `{ url }`, handler expects `{ download_url }`. Fix `api-domains.js` to send `{ download_url: url }`. Every Install click currently returns 400. |
| Registry settings UI | | Add "Package Registry" section to admin settings with URL input field. Only way to configure registry today is a raw `PUT /api/v1/admin/settings/package_registry` — no user will find it. |
| Registry tooling + docs | | `scripts/generate-registry.sh` scans a directory of `.pkg` files and emits registry JSON. `docs/PACKAGE-REGISTRY.md` documents the format. |
| Delete dead kernel Go | | `store/interfaces.go:178183` — orphaned ChannelListFilter comments. `pages/pages.go:922930``roleFilterType()` + template registration (chat vestige, maps nonexistent roles). `main.go:67` — orphaned provider-type comment. |
| Delete dead vendor JS | | `vendor/marked.min.js` and `vendor/purify.min.js` — 62KB, zero production imports. Only referenced in test helpers. |
| Delete `dev.html` | | 676 lines, not imported by anything. Dead. |
| Remove `dashboard` from default bundle | | Requires `legacy-sdk` (doesn't exist), auto-installs as dormant on fresh installs. Confusing. Remove from `defaultBundledPackages`. |
| Remove `hello-dashboard` | | Proof-of-concept from early development. Move to `examples/` or delete. |
| Strip stale version comments | | 60+ files have `// v0.29.x:`, `// v0.33.x:` pre-fork comments. Single sed pass. |
| Narrow default bundle | | Default: `notes`, `chat`, `chat-core`, `mermaid-renderer`, `schedules`. Everything else available via registry or `BUNDLED_PACKAGES=*`. |
### v0.6.4 — Admin Health/Metrics Tab + Cluster Merge

View File

@@ -1 +1 @@
0.6.2
0.6.3

View File

@@ -21,20 +21,22 @@ The production Docker image ships with pre-built packages. A **curated default s
| Package | Type | Description |
|---------|------|-------------|
| notes | surface | Markdown notes with graph view |
| chat | surface | Real-time chat surface |
| chat-core | library | Conversations, messages, read cursors |
| workflow-chat | extension | Chat integration for workflow stages |
| dashboard | surface | Dashboard surface |
| workflow-demo | surface | Interactive walkthrough with diagrams |
| bug-report-triage | workflow | Public entry, severity routing, SLA timers |
| content-approval | workflow | Multi-party signoff example |
| employee-onboarding | workflow | Automated provisioning + manager signoff |
| mermaid-renderer | extension | Diagram rendering |
| schedules | surface | Cron task management UI |
#### Opt-in (ship in image, require `BUNDLED_PACKAGES` to enable)
| Package | Type | Description |
|---------|------|-------------|
| tasks | full | Kanban/list task manager with webhooks |
| schedules | surface | Cron task management UI |
| workflow-chat | extension | Chat integration for workflow stages |
| dashboard | surface | Dashboard surface |
| workflow-demo | surface | Interactive walkthrough with diagrams |
| bug-report-triage | workflow | Public entry, severity routing, SLA timers |
| content-approval | workflow | Multi-party signoff example |
| employee-onboarding | workflow | Automated provisioning + manager signoff |
| editor | surface | Rich markdown editor |
| hello-dashboard | surface | Welcome/getting started surface |
| team-activity-log | surface | Team activity feed |
@@ -45,7 +47,6 @@ The production Docker image ships with pre-built packages. A **curated default s
| gitea-client | library | Gitea API integration library |
| js-sandbox | extension | JavaScript sandbox |
| katex-renderer | extension | LaTeX rendering |
| mermaid-renderer | extension | Diagram rendering |
| regex-tester | extension | Regex testing tool |
| icd-test-runner | surface | E2E API test suite |
| sdk-test-runner | surface | SDK feature test suite |

94
docs/PACKAGE-REGISTRY.md Normal file
View File

@@ -0,0 +1,94 @@
# Package Registry
The package registry lets admins browse and install packages from an external
JSON index — a lightweight alternative to manually uploading `.pkg` files.
## Registry JSON Format
The registry is a static JSON file matching the `RegistryResponse` struct:
```json
{
"packages": [
{
"id": "notes",
"title": "Notes",
"version": "0.8.0",
"description": "Markdown notes with backlinks and graph view",
"author": "switchboard",
"type": "extension",
"tier": "core",
"download_url": "https://cdn.example.com/pkg/notes.pkg",
"size": 48200,
"updated_at": "2026-03-20T00:00:00Z"
}
]
}
```
### Required Fields
| Field | Description |
|----------------|------------------------------------------------|
| `id` | Package identifier (matches `manifest.json`) |
| `title` | Display name |
| `version` | Semver version string |
| `description` | Short description |
| `download_url` | HTTPS URL to the `.pkg` file (must be HTTPS) |
### Optional Fields
| Field | Description |
|--------------|------------------------------------------|
| `author` | Package author |
| `type` | `extension` or `library` |
| `tier` | `core`, `official`, or `community` |
| `size` | File size in bytes |
| `updated_at` | ISO 8601 timestamp of last update |
## Configuring the Registry URL
### Via Admin UI
Navigate to **Admin > Settings > Package Registry** and enter the registry URL.
### Via API
```bash
curl -X PUT /api/v1/admin/settings/package_registry \
-H "Content-Type: application/json" \
-d '{"value": {"url": "https://cdn.example.com/pkg/registry.json"}}'
```
## Generating a Registry
Use `scripts/generate-registry.sh` to build a `registry.json` from a
directory of `.pkg` files:
```bash
# Default: reads dist/, uses placeholder base URL
./scripts/generate-registry.sh
# Custom directory and base URL
./scripts/generate-registry.sh ./my-packages https://cdn.example.com/pkg > registry.json
```
Requirements: `jq`, `unzip`, and `stat` (GNU or BSD).
## Self-Hosting
Any HTTPS-capable file server works. Upload your `.pkg` files and the
generated `registry.json` to the same directory, then point the admin
setting to the `registry.json` URL.
Example with a static file server:
```
/var/www/packages/
registry.json
notes.pkg
chat.pkg
chat-core.pkg
```
The registry is fetched and cached for 5 minutes on each browse request.

81
scripts/generate-registry.sh Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# generate-registry.sh — Build a registry.json from a directory of .pkg files.
#
# Usage:
# ./scripts/generate-registry.sh [PKG_DIR] [BASE_URL]
#
# Arguments:
# PKG_DIR Directory containing .pkg files (default: dist/)
# BASE_URL HTTPS base URL for download links (default: https://example.com/packages)
#
# Output:
# Writes registry.json to stdout. Redirect to a file:
# ./scripts/generate-registry.sh dist/ https://cdn.example.com/pkg > registry.json
set -euo pipefail
PKG_DIR="${1:-dist}"
BASE_URL="${2:-https://example.com/packages}"
# Strip trailing slash
BASE_URL="${BASE_URL%/}"
if [ ! -d "$PKG_DIR" ]; then
echo "Error: directory '$PKG_DIR' not found" >&2
exit 1
fi
# Collect entries
entries=()
for pkg in "$PKG_DIR"/*.pkg; do
[ -f "$pkg" ] || continue
# Extract manifest.json from the ZIP archive
manifest=$(unzip -p "$pkg" manifest.json 2>/dev/null) || {
echo "Warning: skipping $pkg (no manifest.json)" >&2
continue
}
# Read fields from manifest
id=$(echo "$manifest" | jq -r '.id // empty')
title=$(echo "$manifest" | jq -r '.title // .id')
version=$(echo "$manifest" | jq -r '.version // "0.0.0"')
description=$(echo "$manifest" | jq -r '.description // ""')
author=$(echo "$manifest" | jq -r '.author // ""')
type=$(echo "$manifest" | jq -r '.type // "extension"')
tier=$(echo "$manifest" | jq -r '.tier // "community"')
if [ -z "$id" ]; then
echo "Warning: skipping $pkg (no id in manifest)" >&2
continue
fi
filename=$(basename "$pkg")
size=$(stat -c%s "$pkg" 2>/dev/null || stat -f%z "$pkg" 2>/dev/null || echo 0)
updated_at=$(date -r "$pkg" -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")
entry=$(jq -n \
--arg id "$id" \
--arg title "$title" \
--arg version "$version" \
--arg description "$description" \
--arg author "$author" \
--arg type "$type" \
--arg tier "$tier" \
--arg download_url "$BASE_URL/$filename" \
--argjson size "$size" \
--arg updated_at "$updated_at" \
'{id: $id, title: $title, version: $version, description: $description,
author: $author, type: $type, tier: $tier, download_url: $download_url,
size: $size, updated_at: $updated_at}')
entries+=("$entry")
done
# Build final JSON
if [ ${#entries[@]} -eq 0 ]; then
echo '{"packages": []}' | jq .
else
printf '%s\n' "${entries[@]}" | jq -s '{packages: .}'
fi

View File

@@ -26,18 +26,18 @@ type Config struct {
// Seed users (dev/test only) — CSV: "user:pass:role,user2:pass2:role2"
SeedUsers string
// API key encryption (required for v0.9.4+)
// API key encryption (required+)
// Used to derive AES-256 key for global/team provider API keys.
// Personal keys use per-user UEK (derived from password).
EncryptionKey string
// File storage (v0.12.0+)
// File storage
// STORAGE_BACKEND: "pvc" or "s3". Empty = auto-detect (pvc if path writable).
// STORAGE_PATH: mount point for PVC backend (default /data/storage).
StorageBackend string
StoragePath string
// S3-compatible storage (v0.12.0+)
// S3-compatible storage
// Works with AWS S3, MinIO, Ceph RGW, or any S3-compatible API.
S3Endpoint string // custom endpoint URL (required for MinIO/Ceph, optional for AWS)
S3Bucket string // bucket name (required when STORAGE_BACKEND=s3)
@@ -47,16 +47,16 @@ type Config struct {
S3Prefix string // optional key prefix within bucket (e.g. "switchboard/")
S3ForcePathStyle bool // use path-style URLs (required for MinIO, most self-hosted)
// Structured logging (v0.33.0)
// Structured logging
// LOG_FORMAT: "text" (default, backward-compatible) or "json" (structured).
// LOG_LEVEL: "debug", "info" (default), "warn", "error".
LogFormat string
LogLevel string
// Auth mode (v0.24.0): "builtin" (default) | "mtls" | "oidc"
// Auth mode: "builtin" (default) | "mtls" | "oidc"
AuthMode string
// mTLS (v0.24.1)
// mTLS
// Headers injected by TLS-terminating reverse proxy.
MTLSHeaderDN string // default "X-SSL-Client-DN"
MTLSHeaderVerify string // default "X-SSL-Client-Verify"
@@ -64,7 +64,7 @@ type Config struct {
MTLSAutoActivate bool // auto-activate new users (default true)
MTLSDefaultTeam string // team ID for auto-provisioned users (optional)
// Bundled packages (v0.3.8, curated v0.5.4)
// Bundled packages
// SKIP_BUNDLED_PACKAGES: set true to disable auto-install of bundled packages on first run.
// BUNDLED_PACKAGES_DIR: directory containing pre-built .pkg archives (default /app/bundled-packages).
// BUNDLED_PACKAGES: controls which bundled packages are auto-installed:
@@ -75,7 +75,7 @@ type Config struct {
BundledPackagesDir string
BundledPackages string
// OIDC (v0.24.1)
// OIDC
OIDCIssuerURL string // e.g. "https://keycloak.corp/realms/switchboard"
OIDCExternalIssuerURL string // OIDC_EXTERNAL_ISSUER_URL
OIDCClientID string
@@ -87,7 +87,7 @@ type Config struct {
OIDCGroupsClaim string // JWT claim for group sync (default "groups")
OIDCAdminRole string // IdP role value that maps to admin (default "admin")
// Cluster registry (v0.6.0)
// Cluster registry
// PG-backed node registry for horizontal scaling. No-op on SQLite.
// CLUSTER_NODE_ID: override for deterministic identity (default: hostname-PID).
// CLUSTER_HEARTBEAT_INTERVAL: tick frequency (default "10s").

View File

@@ -10,7 +10,7 @@ import (
// provider_configs.api_key_plain using the env-derived key, writing the
// result to api_key_enc (BYTEA) + key_nonce. Clears api_key_plain after.
//
// This runs once on upgrade from v0.9.3 → v0.9.4. Subsequent starts are
// This runs once on upgrade when ENCRYPTION_KEY is first set. Subsequent starts are
// a no-op (api_key_plain will be NULL for all rows).
//
// Personal-scope keys are temporarily encrypted with the env key. They get
@@ -94,7 +94,7 @@ func EnforceEncryptionKey(db *sql.DB, encryptionKey string) error {
// Check if any plaintext keys exist (pre-backfill)
var hasPlaintext bool
// api_key_plain column may not exist on fresh v0.9.4+ installs
// api_key_plain column may not exist on fresh installs
var colExists bool
db.QueryRow(`
SELECT EXISTS (
@@ -121,7 +121,7 @@ func EnforceEncryptionKey(db *sql.DB, encryptionKey string) error {
if hasPlaintext {
return fmt.Errorf(
"ENCRYPTION_KEY is not set but plaintext API keys need encryption. " +
"Set ENCRYPTION_KEY before starting (required for v0.9.4+)",
"Set ENCRYPTION_KEY before starting (required+)",
)
}
log.Println(" ⚠ ENCRYPTION_KEY not set — API key encryption disabled (OK for fresh install with no providers)")

View File

@@ -19,7 +19,7 @@ func SchemaVersion() string { return schemaVersion }
// Migrate runs all pending migrations for the current dialect.
// Migration files live in migrations/<dialect>/ subdirectories.
// Falls back to the root migrations/ directory if no subdirectory exists
// (backward compatible with pre-v0.17.1 postgres-only layouts).
// (backward compatible with postgres-only layouts).
func Migrate() error {
if DB == nil {
return fmt.Errorf("database not connected")

View File

@@ -113,14 +113,14 @@ func TestRouteFor(t *testing.T) {
{"pong", DirToClient},
// Tool bridge routes
{"tool.call.abc123", DirToClient},
{"tool.result.abc123", DirBoth}, // v0.32.0: DirBoth for cross-pod WaitFor
{"tool.result.abc123", DirBoth},
// Extension lifecycle
{"extension.loaded", DirLocal},
{"extension.error", DirLocal},
// Realtime (v0.5.0)
// Realtime
{"realtime.chat.message", DirToClient},
{"realtime.custom.event", DirToClient},
// Room management (v0.5.0)
// Room management
{"room.subscribe", DirFromClient},
{"room.unsubscribe", DirFromClient},
}
@@ -227,7 +227,6 @@ func TestToolCallRouteToClient(t *testing.T) {
}
func TestToolResultRouteBoth(t *testing.T) {
// v0.32.0: tool.result is DirBoth so results cross pods for WaitFor.
// The WS subscriber explicitly filters out tool.result events to
// prevent re-sending to clients (see subscribeToBus in ws.go).
if !ShouldAcceptFromClient("tool.result.abc123") {

View File

@@ -1,6 +1,5 @@
package events
// v0.32.0: TicketValidatorAdapter bridges the context-aware store.TicketStore
// to the middleware.TicketValidator interface (which has no context parameter).
// Replaces the in-memory TicketStore that lived in this file previously.

View File

@@ -9,7 +9,6 @@ type Event struct {
Payload json.RawMessage `json:"payload"`
Ts int64 `json:"ts"`
// v0.32.0: Cross-pod targeted delivery. When set, only connections
// belonging to this user receive the event. Serialized for pg_broadcast
// (harmless if clients see it — they ignore unknown fields).
TargetUserID string `json:"target_user_id,omitempty"`
@@ -38,8 +37,8 @@ var routeTable = map[string]Direction{
// User/presence
"user.presence": DirToClient,
"user.status": DirToClient,
"user.mentioned": DirToClient, // v0.23.2: targeted @mention notification
"typing.user": DirToClient, // v0.23.2: human typing in DM/channel
"user.mentioned": DirToClient,
"typing.user": DirToClient,
// System
"system.notify": DirToClient,
@@ -48,16 +47,16 @@ var routeTable = map[string]Direction{
// Model/provider status
"model.status": DirToClient,
// Role alerts (v0.17.0)
// Role alerts
"role.fallback": DirToClient,
// Notifications (v0.20.0)
// Notifications
"notification.new": DirToClient, // targeted via Hub.PublishToUser, not room-based
"notification.read": DirToClient, // badge sync across tabs
// Workspace (v0.21.5)
// Workspace
// Workflow (v0.27.0, v0.3.2, v0.3.3)
// Workflow
"workflow.started": DirToClient, // instance started
"workflow.assigned": DirToClient, // new assignment → team members
"workflow.claimed": DirToClient, // assignment claimed → team + claimer
@@ -65,10 +64,10 @@ var routeTable = map[string]Direction{
"workflow.completed": DirToClient, // workflow finished → instance participants
"workflow.cancelled": DirToClient, // instance cancelled
"workflow.error": DirToClient, // engine/hook error
"workflow.sla_breach": DirToClient, // SLA exceeded → team + admins (v0.3.3)
"workflow.stale": DirToClient, // instance marked stale (v0.3.3)
"workflow.signoff": DirToClient, // signoff submitted (v0.3.4)
"workflow.rejected": DirToClient, // rejection triggered cancel/reroute (v0.3.4)
"workflow.sla_breach": DirToClient, // SLA exceeded → team + admins
"workflow.stale": DirToClient, // instance marked stale
"workflow.signoff": DirToClient, // signoff submitted
"workflow.rejected": DirToClient, // rejection triggered cancel/reroute
// Plugin hooks — never cross the wire
"plugin.hook.": DirLocal,
@@ -77,20 +76,20 @@ var routeTable = map[string]Direction{
// Tool execution (browser tools bridge)
"tool.call.": DirToClient, // Server → specific client (browser tool invocation)
"tool.result.": DirBoth, // v0.32.0: DirBoth — result must cross pods for WaitFor
"tool.result.": DirBoth,
// Extension lifecycle
"extension.loaded": DirLocal, // Client-only
"extension.error": DirLocal,
// Trigger system (v0.2.2)
// Trigger system
"trigger.fired": DirLocal, // Trigger invocation event (observability)
"trigger.error": DirLocal, // Trigger execution error
// Realtime (v0.5.0): extension-published events, room-scoped
// Realtime: extension-published events, room-scoped
"realtime.": DirToClient,
// Room management (v0.5.0): client room join/leave
// Room management: client room join/leave
"room.subscribe": DirFromClient,
"room.unsubscribe": DirFromClient,

View File

@@ -174,7 +174,6 @@ func (h *Hub) IsConnected(userID string) bool {
}
// PublishToUser sends an event to a specific user via the bus.
// v0.32.0: Cross-pod safe — the bus broadcast hook fans out via pg_notify.
// All replicas receive the event; only the one with the user's connection delivers it.
func (h *Hub) PublishToUser(userID string, event Event) {
event.TargetUserID = userID
@@ -238,13 +237,11 @@ func (c *Conn) subscribeToBus() {
return
}
// v0.32.0: Targeted delivery — only deliver to the intended user.
// Targeted events skip room filtering (user-scoped, not room-scoped).
if e.TargetUserID != "" && e.TargetUserID != c.userID {
return
}
// v0.32.0: tool.result travels cross-pod for WaitFor (DirBoth) but
// must not be forwarded to WebSocket clients — they sent it.
if strings.HasPrefix(e.Label, "tool.result.") {
return
@@ -316,7 +313,6 @@ func (c *Conn) readPump() {
continue
}
// v0.5.0: Room management — intercept before bus publish (like ping)
if event.Label == "room.subscribe" {
var req struct {
Room string `json:"room"`

View File

@@ -1,6 +1,5 @@
package handlers
// v0.38.1: Extension connection handlers — admin/global scope.
// Methods on AdminHandler, mirrors admin provider config pattern.
import (

View File

@@ -345,7 +345,6 @@ func hashToken(token string) string {
// - AdminHandler.destroyVault (admin-initiated reset)
//
// Does NOT evict from UEK cache or write audit logs — callers handle that.
// v0.29.0: accepts stores instead of using database.DB directly.
func DestroyVaultDB(ctx context.Context, stores store.Stores, userID string) (deleted int64) {
if err := stores.Users.ClearVaultKeys(ctx, userID); err != nil {
log.Printf("⚠ DestroyVaultDB: failed to clear vault columns for user %s: %v", userID, err)
@@ -366,8 +365,6 @@ func DestroyVaultDB(ctx context.Context, stores store.Stores, userID string) (de
// sessions survive server restarts.
//
// Used by BootstrapAdmin and SeedUsers where the password is known at startup.
// v0.29.0: accepts stores instead of using database.DB directly.
// v0.30.2: accepts optional uekCache to pre-warm vault on restart.
func ProbeAndRepairVault(ctx context.Context, stores store.Stores, userID, password string, uekCache ...*crypto.UEKCache) {
vaultSet, encryptedUEK, salt, nonce, err := stores.Users.GetVaultKeys(ctx, userID)
if err != nil || !vaultSet {
@@ -493,7 +490,7 @@ func BootstrapAdmin(cfg *config.Config, s store.Stores, uekCache ...*crypto.UEKC
})
// If the actual password changed, the vault seal is stale. Probe it
// with the current password and destroy only if unwrap fails.
// Ensure handle exists (backfill for pre-v0.24.0 users)
// Ensure handle exists (backfill for users)
if existing.Handle == "" {
handle := auth.UniqueHandle(ctx, s.Users, models.HandleFromName(cfg.AdminUsername))
s.Users.Update(ctx, existing.ID, map[string]interface{}{"handle": handle})

View File

@@ -1,6 +1,5 @@
package handlers
// v0.38.1: ConnectionResolver bridges the sandbox connections module
// to the store + vault layers. Implements sandbox.ConnectionResolver.
import (

View File

@@ -1,6 +1,5 @@
package handlers
// v0.38.4: Connection type discovery endpoint.
// Returns merged connection types from all active packages.
// Library declarations take precedence over non-library declarations.

View File

@@ -1,6 +1,5 @@
package handlers
// v0.38.1: Extension connection handlers — personal scope + resolution.
// Pattern follows apiconfigs.go (ProviderConfigHandler).
import (
@@ -28,7 +27,6 @@ func NewConnectionHandler(s store.Stores, vault *crypto.KeyResolver) *Connection
// ListConnections returns the user's personal connections.
func (h *ConnectionHandler) ListConnections(c *gin.Context) {
userID := getUserID(c)
// v0.38.5: Show all accessible connections (personal + team + global)
// so users can see which connections are available to them.
conns, err := h.stores.Connections.ListAccessible(c.Request.Context(), userID)
if err != nil {

View File

@@ -8,7 +8,7 @@ import (
"switchboard-core/models"
)
// ── Extension Dependencies (v0.38.2) ──────────────────
// ── Extension Dependencies ──────────────────
// Admin-only read endpoints for dependency graph visibility.
// Dependencies are created/deleted implicitly by InstallPackage/DeletePackage.

View File

@@ -1,6 +1,5 @@
// Package handlers — ext_api.go
//
// v0.29.1 CS1: Extension API routes. Starlark packages serve custom
// JSON endpoints via the existing Gin router. Mounted at
// /s/:slug/api/*path with JWT auth.
//
@@ -332,7 +331,7 @@ func hasPermission(granted []string, perm string) bool {
return false
}
// ── api_schema support (v0.6.2) ─────────────────────────────────────
// ── api_schema support ─────────────────────────────────────
// APISchemaEntry represents one route's OpenAPI-level documentation
// declared via the optional manifest "api_schema" array.

View File

@@ -1,6 +1,6 @@
package handlers
// ext_db_schema.go — v0.29.2
// ext_db_schema.go
//
// DDL generation and lifecycle management for extension-owned database tables.
// Tables are namespaced as ext_{pkg_slug}_{logical_name} and tracked in the

View File

@@ -10,7 +10,6 @@ import (
)
// ExtPermHandler serves extension permission management endpoints.
// v0.29.0 CS2: Admin reviews and grants/revokes declared permissions.
type ExtPermHandler struct {
stores store.Stores
}

View File

@@ -10,7 +10,6 @@ import (
)
// ExtSecretsHandler serves extension secret management endpoints.
// v0.29.0 CS3: Admin sets key-value secrets that Starlark extensions
// can read via the secrets.get() module.
//
// Secrets are stored in GlobalConfig under key "ext_secrets:{packageID}"

View File

@@ -16,7 +16,6 @@ import (
)
// ExtensionHandler serves extension management endpoints.
// v0.28.7: Backed by PackageStore (packages table) instead of ExtensionStore.
type ExtensionHandler struct {
stores store.Stores
}
@@ -94,7 +93,6 @@ func (h *ExtensionHandler) UpdateUserExtensionSettings(c *gin.Context) {
settings := json.RawMessage("{}")
if body.Settings != nil {
// v0.2.0: enforce user_overridable — strip locked keys before saving
schema := store.ParseSettingsSchema(pkg.Manifest)
if len(schema) > 0 {
var incoming map[string]any
@@ -229,18 +227,15 @@ func (h *ExtensionHandler) AdminInstallExtension(c *gin.Context) {
return
}
// v0.29.0: Parse manifest permissions and declare them.
// If permissions are declared, package moves to pending_review.
SyncManifestPermissions(c, h.stores, pkg.ID, manifestMap)
// v0.29.2: Create namespaced DB tables declared in the manifest.
if tables, ok := ParseDBTables(manifestMap); ok {
if err := CreateExtTables(c.Request.Context(), database.DB, database.IsPostgres(), h.stores, pkg.ID, tables); err != nil {
log.Printf("[ext-db] schema create failed for %s: %v", pkg.ID, err)
}
}
// v0.2.2: Sync triggers from manifest
SyncManifestTriggers(c.Request.Context(), h.stores, triggers.GlobalEngine(), pkg.ID, manifestMap)
c.JSON(201, gin.H{"data": pkg})
@@ -403,7 +398,6 @@ func (h *ExtensionHandler) AdminUninstallExtension(c *gin.Context) {
return
}
// v0.29.2: Drop namespaced DB tables before removing the package record.
if err := DropExtTables(c.Request.Context(), database.DB, h.stores, id); err != nil {
log.Printf("[ext-db] schema drop failed for %s: %v", id, err)
}

View File

@@ -243,7 +243,7 @@ func (h *GroupHandler) AddMember(c *gin.Context) {
return
}
// Notify the added user (v0.20.0)
// Notify the added user
if svc := notifications.Default(); svc != nil {
notifications.NotifyGroupMemberAdded(svc, req.UserID, groupID, group.Name)
}
@@ -273,7 +273,7 @@ func (h *GroupHandler) RemoveMember(c *gin.Context) {
return
}
// Notify the removed user (v0.20.0)
// Notify the removed user
if svc := notifications.Default(); svc != nil && group != nil {
notifications.NotifyGroupMemberRemoved(svc, userID, groupID, group.Name)
}

View File

@@ -62,7 +62,7 @@ func setupNotifHarness(t *testing.T) *notifHarness {
protected.PUT("/notifications/preferences/:type", notifH.SetPreference)
protected.DELETE("/notifications/preferences/:type", notifH.DeletePreference)
// Admin broadcast route (v0.28.6)
// Admin broadcast route
admin := api.Group("/admin")
admin.Use(middleware.Auth(cfg, stores.Users, userCache))
admin.Use(middleware.RequireAdmin(stores))
@@ -560,7 +560,7 @@ func TestNotifications_Unauthenticated(t *testing.T) {
}
}
// ── Admin Broadcast (v0.28.6) ────────────
// ── Admin Broadcast ────────────
func TestBroadcast_Success(t *testing.T) {
h := setupNotifHarness(t)

View File

@@ -175,7 +175,7 @@ func (h *NotificationHandler) Delete(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
}
// ── Notification Preferences (v0.20.0 Phase 3) ──
// ── Notification Preferences ──
// GET /api/v1/notifications/preferences
func (h *NotificationHandler) ListPreferences(c *gin.Context) {
@@ -282,7 +282,7 @@ func (h *NotificationHandler) DeletePreference(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
}
// ── Admin Broadcast (v0.28.6) ───────────────
// ── Admin Broadcast ───────────────
// POST /api/v1/admin/notifications/broadcast
// Admin-only: sends a system.announcement notification to all active users.

View File

@@ -1,6 +1,5 @@
// Package handlers — openapi.go
//
// v0.6.2: Dynamic OpenAPI spec generation. Merges the static kernel spec
// with extension-declared API routes. Extensions with api_schema get rich
// path items; others get auto-generated stubs.
//

View File

@@ -1,6 +1,6 @@
package handlers
// package_registry.go — v0.30.0 CS4
// package_registry.go
//
// Package registry (marketplace discovery). Admin browses available
// packages from an external JSON registry and installs them by URL.

View File

@@ -27,12 +27,12 @@ import (
var validPackageID = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$`)
// PackageHandler manages unified package lifecycle (admin-only).
// Replaces SurfaceHandler (v0.28.7).
// Replaces SurfaceHandler.
type PackageHandler struct {
stores store.Stores
packagesDir string // e.g. /data/packages — where archives are extracted
sandbox *sandbox.Sandbox // v0.30.0: for schema migration scripts
runner *sandbox.Runner // v0.38.2: for test-tool execution
sandbox *sandbox.Sandbox
runner *sandbox.Runner
}
func NewPackageHandler(s store.Stores, packagesDir ...string) *PackageHandler {
@@ -43,12 +43,12 @@ func NewPackageHandler(s store.Stores, packagesDir ...string) *PackageHandler {
return &PackageHandler{stores: s, packagesDir: dir}
}
// SetSandbox attaches a Starlark sandbox for schema migrations (v0.30.0).
// SetSandbox attaches a Starlark sandbox for schema migrations.
func (h *PackageHandler) SetSandbox(sb *sandbox.Sandbox) {
h.sandbox = sb
}
// SetRunner attaches the Starlark runner for test-tool execution (v0.38.2).
// SetRunner attaches the Starlark runner for test-tool execution.
func (h *PackageHandler) SetRunner(r *sandbox.Runner) {
h.runner = r
}
@@ -95,7 +95,6 @@ func (h *PackageHandler) GetPackage(c *gin.Context) {
func (h *PackageHandler) EnablePackage(c *gin.Context) {
id := c.Param("id")
// v0.3.7: Block enabling dormant packages (unmet requires).
pkg, err := h.stores.Packages.Get(c.Request.Context(), id)
if err != nil || pkg == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "package not found"})
@@ -149,7 +148,6 @@ func (h *PackageHandler) DeletePackage(c *gin.Context) {
return
}
// v0.38.2: Libraries with active consumers cannot be uninstalled.
if pkg.Type == "library" && h.stores.Dependencies != nil {
hasConsumers, _ := h.stores.Dependencies.HasConsumers(c.Request.Context(), id)
if hasConsumers {
@@ -158,12 +156,10 @@ func (h *PackageHandler) DeletePackage(c *gin.Context) {
}
}
// v0.38.2: Clean up dependency records when uninstalling a consumer.
if h.stores.Dependencies != nil {
h.stores.Dependencies.DeleteAllForConsumer(c.Request.Context(), id)
}
// v0.29.2: Drop namespaced DB tables before removing the package record.
if err := DropExtTables(c.Request.Context(), database.DB, h.stores, id); err != nil {
log.Printf("[ext-db] schema drop failed for %s: %v", id, err)
}
@@ -195,7 +191,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
var tmpPath string
var cleanupTmp bool
// v0.30.0: Check for pre-downloaded file from registry install
if regFile, ok := c.Get("_registry_file"); ok {
tmpPath = regFile.(string)
// Don't remove — caller manages lifecycle
@@ -278,7 +273,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
return
}
// v0.38.0: Entry point validation for starlark-tier packages.
// Scripts are loaded from disk at runtime — no _starlark_script injection.
if manifestTier, _ := manifest["tier"].(string); manifestTier == "starlark" {
entryPoint := "script.star"
@@ -380,7 +374,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "full packages require a route"})
return
}
// v0.31.0: full packages need either server-side behavior (tools/pipes/hooks)
// or a settings schema. A surface-with-settings is a valid "full" package
// (e.g. editor: browser-tier surface + admin-configurable settings).
if !hasExtBehavior && !hasSettings {
@@ -393,7 +386,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
return
}
case "library":
// v0.38.2: Libraries must declare exports, cannot have tools/pipes/route.
exports, _ := manifest["exports"].([]any)
if len(exports) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "library packages require an 'exports' array"})
@@ -471,7 +463,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
userID := c.GetString("user_id")
// v0.30.0: use registry source if set by registry install handler
pkgSource := "extension"
if src, ok := c.Get("_registry_source"); ok {
pkgSource = src.(string)
@@ -509,22 +500,18 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
log.Printf("[packages] Failed to update package metadata for %s: %v", pkgID, err)
}
// v0.29.2: Create namespaced DB tables declared in the manifest.
if tables, ok := ParseDBTables(manifest); ok {
if err := CreateExtTables(c.Request.Context(), database.DB, database.IsPostgres(), h.stores, pkgID, tables); err != nil {
log.Printf("[ext-db] schema create failed for %s: %v", pkgID, err)
}
}
// v0.38.5: Declare manifest permissions (same as AdminInstallExtension).
// This creates the permission rows and sets status to pending_review
// if the package declares permissions.
SyncManifestPermissions(c, h.stores, pkgID, manifest)
// v0.2.2: Sync triggers from manifest
SyncManifestTriggers(c.Request.Context(), h.stores, triggers.GlobalEngine(), pkgID, manifest)
// v0.30.0: Run schema migrations if declared.
newSchemaVersion := ParseSchemaVersion(manifest)
if newSchemaVersion > 0 && h.sandbox != nil {
oldSchemaVersion := 0
@@ -553,7 +540,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
}
}
// v0.30.2: Install workflow definition from package manifest.
if pkgType == "workflow" {
if err := InstallWorkflowFromManifest(c, h.stores, pkgID, manifest); err != nil {
log.Printf("[packages] workflow install failed for %s: %v", pkgID, err)
@@ -562,7 +548,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
}
}
// v0.38.2: Create dependency records from manifest "dependencies" map.
// Libraries must be installed first; consumers declare them.
if deps, ok := manifest["dependencies"].(map[string]any); ok && len(deps) > 0 {
// Clear stale dependency records from a previous install of the same consumer.
@@ -603,7 +588,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
log.Printf("[packages] %s: %d dependencies recorded", pkgID, len(deps))
}
// v0.2.1: Auto-set default_surface when the first extension surface is installed.
if (pkgType == "surface" || pkgType == "full") && pkgSource != "core" {
if raw, err := h.stores.GlobalConfig.Get(c.Request.Context(), "default_surface"); err != nil || raw == nil {
dflt := models.JSONMap{"id": pkgID}
@@ -615,7 +599,6 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
}
}
// v0.3.7: Auto-set dormant for packages with unmet requires.
// Known capabilities: (none yet — chat and legacy-sdk are future/removed).
knownCaps := map[string]bool{}
var unmetReqs []string
@@ -689,7 +672,7 @@ func extractableRelPath(name string) string {
return ""
}
// ── Package settings (v0.30.0 CS2) ──────────────
// ── Package settings ──────────────
// GetPackageSettings returns the settings schema from the manifest merged
// with current admin-configured values.
@@ -823,7 +806,7 @@ func (h *PackageHandler) ListEnabledSurfaces(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": enabled})
}
// ── Test Tool (v0.38.2) ─────────────────────────
// ── Test Tool ─────────────────────────
// POST /admin/packages/:id/test-tool
// Invokes a starlark extension's on_tool_call entry point directly,
// without going through the AI chat loop. Admin-only test harness.
@@ -898,7 +881,7 @@ func (h *PackageHandler) TestTool(c *gin.Context) {
})
}
// ── Package update (v0.5.4) ──────────────────────
// ── Package update ──────────────────────
// UpdatePackage applies an in-place update to an existing package.
// POST /api/v1/admin/packages/:id/update
@@ -1175,7 +1158,7 @@ func mergePackageSettings(existing *store.PackageRegistration, manifest map[stri
return data
}
// ── Package export (v0.5.4) ──────────────────────
// ── Package export ──────────────────────
// ExportPackage exports an installed package as a downloadable .pkg archive.
// GET /api/v1/admin/packages/:id/export

View File

@@ -23,23 +23,16 @@ import (
)
// defaultBundledPackages is the curated set of packages installed by default.
// This is the dev/test bundle — includes demos and workflow examples.
// Other packages still ship in the Docker image but require BUNDLED_PACKAGES
// to be set explicitly (or "*" for all).
//
// Recommended production override (lean):
// BUNDLED_PACKAGES=notes,chat,chat-core,cluster-dashboard
// Recommended production override: BUNDLED_PACKAGES=notes,chat,chat-core,mermaid-renderer,schedules
var defaultBundledPackages = map[string]bool{
"notes": true,
"chat": true,
"chat-core": true,
"workflow-chat": true,
"dashboard": true,
"cluster-dashboard": true,
"workflow-demo": true,
"bug-report-triage": true,
"content-approval": true,
"employee-onboarding": true,
"notes": true,
"chat": true,
"chat-core": true,
"mermaid-renderer": true,
"schedules": true,
}
// InstallBundledPackages scans bundledDir for .pkg archives and installs

View File

@@ -1,12 +1,11 @@
package handlers
// presence.go — Heartbeat upsert and status query (v0.23.1)
// presence.go — Heartbeat upsert and status query
//
// Clients POST /api/v1/presence/heartbeat every 30s while active.
// GET /api/v1/presence?users=id1,id2 returns current status.
// Online = last_seen within 90s.
//
// v0.29.0: Raw SQL replaced with PresenceStore + UserStore methods.
import (
"net/http"

View File

@@ -8,7 +8,6 @@ package handlers
// (profile, permissions, teams/mine, settings) into one call.
// The SDK calls this at startup and on token refresh.
//
// v0.37.15
import (
"net/http"

View File

@@ -51,7 +51,7 @@ func (s *jsonScanner) Scan(src interface{}) error {
// memory on the next rows.Scan() call, corrupting our data after the
// fact. This caused intermittent SafeJSON 500s on paginated list
// endpoints where row N's Settings pointed to row N+1's overwritten
// buffer. Fixed in v0.21.7.
// buffer. Fixed.
cp := make([]byte, len(v))
copy(cp, v)
*s.dest = json.RawMessage(cp)

View File

@@ -185,7 +185,7 @@ func TestSafeJSON_PaginatedWithCorrupt(t *testing.T) {
}
// ── Driver buffer aliasing tests ────────────
// These verify the fix for the v0.21.7 corruption bug: scanJSON must
// These verify the fix for the buffer-aliasing corruption bug: scanJSON must
// copy []byte from the database driver, not alias it. Without the copy,
// rows.Next() can overwrite the buffer and corrupt previously-scanned
// json.RawMessage values in a paginated list.

View File

@@ -2,7 +2,6 @@ package handlers
// settings.go — User profile, preferences, and password management.
//
// v0.29.0: Raw SQL replaced with UserStore methods.
import (
"context"

View File

@@ -1,6 +1,6 @@
package handlers
// team_package_settings.go — v0.2.0
// team_package_settings.go
//
// Team-admin endpoints for managing team-scoped package settings.
// These sit in the settings cascade between global (admin) and user.

View File

@@ -419,7 +419,7 @@ func (h *TeamHandler) ListTeamAuditActions(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"actions": actions})
}
// ── Team Roles API (v0.3.4) ─────────────────
// ── Team Roles API ─────────────────
// builtinRoles are always present in every team's role list.
var builtinRoles = []string{"admin", "member"}

View File

@@ -14,7 +14,7 @@ import (
)
// ═══════════════════════════════════════════════
// Upgrade Tests — v0.5.5
// Upgrade Tests
//
// Schema edge cases, settings migration, and
// package compatibility across kernel upgrades.

View File

@@ -1,6 +1,6 @@
package handlers
// user_packages.go — v0.30.0 CS5
// user_packages.go
//
// Non-admin package management. Users install personal-scoped packages;
// team admins install team-scoped packages.

View File

@@ -1,6 +1,6 @@
package handlers
// workflow_packages.go — v0.30.2 CS1
// workflow_packages.go
//
// Handles workflow-specific package operations:
// - ExportWorkflowPackage: bundles a workflow definition + stages into a .pkg

View File

@@ -11,7 +11,7 @@ import (
"switchboard-core/workflow"
)
// ── Public Workflow Handlers (v0.3.3) ───────
// ── Public Workflow Handlers ───────
// WorkflowPublicHandler manages unauthenticated workflow entry endpoints.
type WorkflowPublicHandler struct {

View File

@@ -10,7 +10,7 @@ import (
"switchboard-core/workflow"
)
// ── Signoff Handlers (v0.3.4) ─────────────────
// ── Signoff Handlers ─────────────────
// WorkflowSignoffHandler manages signoff HTTP endpoints.
type WorkflowSignoffHandler struct {

View File

@@ -603,7 +603,7 @@ func TestWorkflowAssignment_Cancel(t *testing.T) {
}
}
// ── v0.3.3 Store Tests ──────────────────────
// ── Instance Lifecycle Store Tests ──────────────────────
func TestWorkflowInstance_MarkStale(t *testing.T) {
database.RequireTestDB(t)
@@ -779,7 +779,7 @@ func TestWorkflowAssignment_ListByTeam(t *testing.T) {
}
}
// ── Signoff store tests (v0.3.4) ──────────────
// ── Signoff store tests ──────────────
func TestWorkflowSignoff_CreateAndList(t *testing.T) {
database.RequireTestDB(t)
@@ -862,7 +862,7 @@ func TestWorkflowSignoff_ListEmpty(t *testing.T) {
}
}
// ── Clone tests (v0.3.5) ──────────────────
// ── Clone tests ──────────────────
func TestWorkflow_Clone(t *testing.T) {
database.RequireTestDB(t)

View File

@@ -9,7 +9,7 @@ import (
"switchboard-core/models"
)
// ── Team-Scoped Workflow Wrappers (v0.31.2) ────────────────
// ── Team-Scoped Workflow Wrappers ────────────────
//
// Thin wrappers that enforce team ownership before delegating
// to the existing WorkflowHandler methods. Follows the same

View File

@@ -106,7 +106,6 @@ func (h *WorkflowHandler) Create(c *gin.Context) {
w.CreatedBy = c.GetString("user_id")
// v0.31.2: team-scoped route injects force_team_id via CreateTeamWorkflow
if ftid, ok := c.Get("force_team_id"); ok {
tid := ftid.(string)
w.TeamID = &tid
@@ -417,7 +416,6 @@ func (h *WorkflowHandler) Clone(c *gin.Context) {
CreatedBy: c.GetString("user_id"),
}
// v0.31.2: team-scoped route injects force_team_id
if ftid, ok := c.Get("force_team_id"); ok {
tid := ftid.(string)
clone.TeamID = &tid

View File

@@ -36,7 +36,6 @@ import (
sqliteStore "switchboard-core/store/sqlite"
)
// v0.33.0: Embedded OpenAPI spec and Swagger UI for /api/docs.
//
//go:embed static/openapi.yaml
var openapiSpec []byte
@@ -59,13 +58,10 @@ func main() {
// ── Server startup ──────────────────────
cfg := config.Load()
// v0.33.0: Structured logging — must be first so all subsequent
// Structured logging — must be first so all subsequent
// log output goes through slog.
logging.Init(cfg.LogFormat, cfg.LogLevel)
// v0.29.1: Config-file provider types (Ollama, LiteLLM, vLLM, etc.)
var stores store.Stores
uekCache := crypto.NewUEKCache()
var keyResolver *crypto.KeyResolver
@@ -109,7 +105,6 @@ func main() {
stores = postgres.NewStores(database.DB)
}
// v0.33.0: Start Prometheus DB pool collector
metrics.StartDBCollector(database.DB, 15*time.Second)
// Bootstrap admin from env (K8s secret) — upserts on every restart
@@ -161,23 +156,18 @@ func main() {
sandbox.New(sandbox.DefaultConfig()),
stores,
)
// v0.38.1: connections module — extension connection resolution
starlarkRunner.SetConnectionResolver(handlers.NewConnectionResolverAdapter(stores, keyResolver))
// v0.29.2: db module — extension namespaced table access
starlarkRunner.SetDB(database.DB, database.IsPostgres())
// v0.38.0: disk-based script loading + load() support
if cfg.StoragePath != "" {
starlarkRunner.SetPackagesDir(cfg.StoragePath + "/packages")
}
// v0.5.0: realtime module — extension publish to WebSocket channels
starlarkRunner.SetBus(bus)
// v0.38.5: allow extensions to reach private IPs (self-hosted Gitea, etc.)
if os.Getenv("EXT_ALLOW_PRIVATE_IPS") == "true" {
starlarkRunner.SetAllowPrivateIPs(true)
log.Printf(" ⚠️ Extension SSRF protection relaxed: private IPs allowed")
}
// ── Bundled Packages (v0.3.8) ───────────────
// ── Bundled Packages ───────────────
// Auto-install bundled .pkg archives on first run.
// Skipped if SKIP_BUNDLED_PACKAGES=true or packages already exist.
if !cfg.SkipBundledPackages && stores.Packages != nil {
@@ -188,7 +178,7 @@ func main() {
handlers.InstallBundledPackages(cfg.BundledPackagesDir, bundledPkgDir, cfg.BundledPackages, stores, starlarkRunner)
}
// ── Trigger Engine (v0.2.2) ─────────────────
// ── Trigger Engine ─────────────────
triggerEngine := triggers.New(stores, starlarkRunner, bus)
if err := triggerEngine.Start(context.Background()); err != nil {
log.Printf(" ⚠️ Trigger engine start: %v", err)
@@ -210,7 +200,7 @@ func main() {
// ── WebSocket Hub ─────────────────────────
hub := events.NewHub(bus, middleware.GetAllowedOrigins(cfg))
// ── Cluster Registry (v0.6.0) ────────────
// ── Cluster Registry ────────────
// PG-backed node self-registration + heartbeat. No-op on SQLite.
var clusterReg *cluster.Registry
if database.IsPostgres() && stores.Cluster != nil {
@@ -231,10 +221,10 @@ func main() {
}
}
// ── WebSocket Ticket Store (v0.32.0: PG-backed for cross-pod) ─────
// ── WebSocket Ticket Store ─────
ticketAdapter := &events.TicketValidatorAdapter{Store: stores.Tickets}
// ── Notification Service (v0.20.0) ───────
// ── Notification Service ───────
var notifSvc *notifications.Service
if stores.Notifications != nil {
notifSvc = notifications.NewService(stores.Notifications, hub).
@@ -277,7 +267,6 @@ func main() {
c.JSON(200, info)
})
// v0.32.0: Kubernetes probe endpoints
// Liveness: process is alive and serving (no dependency checks).
base.GET("/healthz/live", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
@@ -299,10 +288,8 @@ func main() {
c.JSON(200, gin.H{"status": "ok"})
})
// v0.33.0: Prometheus metrics endpoint (no auth — Prometheus scrapes directly)
base.GET("/metrics", gin.WrapH(promhttp.Handler()))
// v0.33.0: OpenAPI spec + Swagger UI (no auth — documentation)
base.GET("/api/docs", func(c *gin.Context) {
c.Data(http.StatusOK, "text/html; charset=utf-8", swaggerHTML)
})
@@ -311,7 +298,6 @@ func main() {
patched := bytes.Replace(openapiSpec, []byte("${VERSION}"), []byte(Version), 1)
c.Data(http.StatusOK, "application/yaml", patched)
})
// v0.6.2: Dynamic OpenAPI spec with extension routes merged in
base.GET("/api/docs/openapi.json", func(c *gin.Context) {
spec, err := handlers.BuildOpenAPISpec(stores, openapiSpec, Version)
if err != nil {
@@ -401,7 +387,7 @@ func main() {
extH := handlers.NewExtensionHandler(stores)
api.GET("/extensions/:id/assets/*path", extH.ServeExtensionAsset)
// ── Webhook Inbound (v0.2.2) ──────────────
// ── Webhook Inbound ──────────────
// Public routes — HMAC-verified in handler, no auth middleware.
api.POST("/hooks/:package_id/:slug", triggerEngine.HandleWebhook)
api.GET("/hooks/:package_id/:slug", triggerEngine.HandleWebhook)
@@ -409,7 +395,7 @@ func main() {
// ── Workflow Engine (shared by public + protected routes) ──
wfEngine := workflow.NewEngine(stores, bus, starlarkRunner)
// ── Public Workflow Entry (v0.3.3) ─────
// ── Public Workflow Entry ─────
publicWfH := handlers.NewWorkflowPublicHandler(wfEngine, stores)
publicWf := api.Group("/public/workflows")
publicWf.Use(authLimiter.Limit())
@@ -419,7 +405,7 @@ func main() {
publicWf.POST("/advance/:token", publicWfH.AdvancePublic)
}
// ── Workflow Scanner (v0.3.3) ──────────
// ── Workflow Scanner ──────────
wfScanner := workflow.NewScanner(stores, bus)
wfScanner.Start()
defer wfScanner.Stop()
@@ -429,7 +415,7 @@ func main() {
protected.Use(middleware.Auth(cfg, stores.Users, userCache))
protected.Use(middleware.ValidatePathParams())
{
// ── WebSocket Ticket (v0.28.8) ───────────
// ── WebSocket Ticket ───────────
// Issue a single-use ticket for WebSocket auth.
// Client fetches this, then connects with ?ticket=<opaque>.
protected.POST("/ws/ticket", func(c *gin.Context) {
@@ -442,15 +428,15 @@ func main() {
c.JSON(http.StatusOK, gin.H{"ticket": ticket})
})
// Presence (v0.23.1)
// Presence
presence := handlers.NewPresenceHandler(stores)
protected.POST("/presence/heartbeat", presence.Heartbeat)
protected.GET("/presence", presence.Query)
// User search (v0.23.2 — DM user picker)
// User search
protected.GET("/users/search", presence.SearchUsers)
// Workflows (v0.26.1 — team-owned staged processes)
// Workflows
wfH := handlers.NewWorkflowHandler(stores)
protected.GET("/workflows", wfH.List)
protected.POST("/workflows", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfH.Create)
@@ -466,7 +452,7 @@ func main() {
protected.POST("/workflows/:id/clone", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfH.Clone)
protected.GET("/workflows/:id/versions/:version", wfH.GetVersion)
// Workflow instances + assignments (v0.3.2)
// Workflow instances + assignments
wfInstH := handlers.NewWorkflowInstanceHandler(wfEngine, stores)
protected.POST("/workflows/:id/instances", middleware.RequirePermission(auth.PermWorkflowSubmit, stores), wfInstH.Start)
protected.GET("/workflows/:id/instances", wfInstH.ListInstances)
@@ -481,16 +467,16 @@ func main() {
protected.POST("/assignments/:id/cancel", middleware.RequirePermission(auth.PermWorkflowCreate, stores), wfAssignH.Cancel)
protected.GET("/assignments/mine", wfAssignH.ListMine)
// Workflow signoffs (v0.3.4)
// Workflow signoffs
wfSignoffH := handlers.NewWorkflowSignoffHandler(wfEngine, stores)
protected.POST("/instances/:iid/signoffs", wfSignoffH.Submit)
protected.GET("/instances/:iid/signoffs", wfSignoffH.List)
// Surface discovery (v0.25.0, v0.28.7: unified packages)
// Surface discovery
pkgH := handlers.NewPackageHandler(stores)
protected.GET("/surfaces", pkgH.ListEnabledSurfaces)
// User package management (v0.30.0)
// User package management
userPkgDir := ""
if cfg.StoragePath != "" {
userPkgDir = cfg.StoragePath + "/packages"
@@ -500,11 +486,11 @@ func main() {
protected.POST("/packages/install", userPkgH.InstallPersonalPackage)
protected.DELETE("/packages/:id", userPkgH.DeletePersonalPackage)
// Connection Type Discovery (v0.38.4)
// Connection Type Discovery
connTypeH := handlers.NewConnectionTypeHandler(stores)
protected.GET("/connection-types", connTypeH.ListConnectionTypes)
// Extension Connections (personal scope — v0.38.1)
// Extension Connections (personal scope
connH := handlers.NewConnectionHandler(stores, keyResolver)
protected.GET("/connections", connH.ListConnections)
protected.POST("/connections", connH.CreateConnection)
@@ -521,21 +507,21 @@ func main() {
protected.GET("/settings", settings.GetSettings)
protected.PUT("/settings", settings.UpdateSettings)
// Documentation (v0.6.1)
// Documentation
docsDir := findDocsDir()
docsH := handlers.NewDocsHandler(docsDir)
protected.GET("/docs", docsH.ListDocs)
protected.GET("/docs/:name", docsH.GetDoc)
// Permission bootstrap (v0.37.1) — self-service resolved permissions
// Permission bootstrap — self-service resolved permissions
permH := handlers.NewProfilePermissionsHandler(stores)
protected.GET("/profile/permissions", permH.GetMyPermissions)
// Boot payload (v0.37.15) — single-call SDK bootstrap
// Boot payload — single-call SDK bootstrap
bootH := handlers.NewProfileBootstrapHandler(stores)
protected.GET("/profile/bootstrap", bootH.GetBootstrap)
// Notifications (v0.20.0)
// Notifications
notifH := handlers.NewNotificationHandler(stores, hub)
protected.GET("/notifications", notifH.List)
protected.GET("/notifications/unread-count", notifH.UnreadCount)
@@ -543,12 +529,12 @@ func main() {
protected.POST("/notifications/mark-all-read", notifH.MarkAllRead)
protected.DELETE("/notifications/:id", notifH.Delete)
// Notification preferences (v0.20.0 Phase 3)
// Notification preferences
protected.GET("/notifications/preferences", notifH.ListPreferences)
protected.PUT("/notifications/preferences/:type", notifH.SetPreference)
protected.DELETE("/notifications/preferences/:type", notifH.DeletePreference)
// ── Scheduled Tasks — User (v0.2.2) ────
// ── Scheduled Tasks — User ────
userSchedH := handlers.NewScheduleHandler(stores, triggerEngine)
protected.GET("/schedules", userSchedH.ListMySchedules)
protected.POST("/schedules", userSchedH.CreateSchedule)
@@ -562,7 +548,7 @@ func main() {
teams := handlers.NewTeamHandler(stores, keyResolver)
protected.GET("/teams/mine", teams.MyTeams)
// Groups (user: my groups — v0.16.0)
// Groups (user: my groups
groupH := handlers.NewGroupHandler(stores)
protected.GET("/groups/mine", groupH.MyGroups)
@@ -580,18 +566,18 @@ func main() {
// Team groups (team admins manage team-scoped groups)
teamScoped.GET("/groups", groupH.ListTeamGroups)
// Team connections (v0.38.1)
// Team connections
teamScoped.GET("/connections", teams.ListTeamConnections)
teamScoped.POST("/connections", teams.CreateTeamConnection)
teamScoped.PUT("/connections/:id", teams.UpdateTeamConnection)
teamScoped.DELETE("/connections/:id", teams.DeleteTeamConnection)
// Team package management (v0.30.0)
// Team package management
teamPkgH := handlers.NewUserPackageHandler(stores, userPkgDir)
teamScoped.POST("/packages/install", teamPkgH.InstallTeamPackage)
teamScoped.DELETE("/packages/:id", teamPkgH.DeleteTeamPackage)
// Team package settings — cascade overrides (v0.2.0)
// Team package settings — cascade overrides
teamPkgSettingsH := handlers.NewTeamPackageSettingsHandler(stores)
teamScoped.GET("/packages/:id/settings", teamPkgSettingsH.GetTeamPackageSettings)
teamScoped.PUT("/packages/:id/settings", teamPkgSettingsH.UpdateTeamPackageSettings)
@@ -601,7 +587,7 @@ func main() {
teamScoped.GET("/audit", teams.ListTeamAuditLog)
teamScoped.GET("/audit/actions", teams.ListTeamAuditActions)
// Team workflows — self-service (v0.31.2)
// Team workflows — self-service
teamWfH := handlers.NewWorkflowHandler(stores)
teamScoped.GET("/workflows", teamWfH.ListTeamWorkflows)
teamScoped.POST("/workflows", teamWfH.CreateTeamWorkflow)
@@ -619,7 +605,7 @@ func main() {
teamScoped.GET("/workflows/available", teamWfH.ListGlobalWorkflows)
teamScoped.GET("/workflows/:id/versions/:version", teamWfH.GetTeamWorkflowVersion)
// Team workflow instances + assignments (v0.3.2)
// Team workflow instances + assignments
teamWfInstH := handlers.NewWorkflowInstanceHandler(wfEngine, stores)
teamScoped.POST("/workflows/:id/instances", teamWfInstH.Start)
teamScoped.GET("/workflows/:id/instances", teamWfInstH.ListInstances)
@@ -630,7 +616,7 @@ func main() {
teamWfAssignH := handlers.NewWorkflowAssignmentHandler(wfEngine, stores)
teamScoped.GET("/assignments", teamWfAssignH.ListByTeam)
// Team workflow signoffs (v0.3.4)
// Team workflow signoffs
teamWfSignoffH := handlers.NewWorkflowSignoffHandler(wfEngine, stores)
teamScoped.POST("/instances/:iid/signoffs", teamWfSignoffH.Submit)
teamScoped.GET("/instances/:iid/signoffs", teamWfSignoffH.List)
@@ -674,7 +660,7 @@ func main() {
// Stats
admin.GET("/stats", adm.GetStats)
// Global Connections (v0.38.1)
// Global Connections
admin.GET("/connections", adm.ListGlobalConnections)
admin.POST("/connections", adm.CreateGlobalConnection)
admin.PUT("/connections/:id", adm.UpdateGlobalConnection)
@@ -692,7 +678,7 @@ func main() {
admin.PUT("/teams/:id/members/:memberId", teamAdm.UpdateMember)
admin.DELETE("/teams/:id/members/:memberId", teamAdm.RemoveMember)
// Admin broadcast (v0.28.6)
// Admin broadcast
adminNotifH := handlers.NewNotificationHandler(stores, hub)
admin.POST("/notifications/broadcast", adminNotifH.Broadcast)
@@ -700,7 +686,7 @@ func main() {
admin.GET("/audit", adm.ListAuditLog)
admin.GET("/audit/actions", adm.ListAuditActions)
// Groups (admin — v0.16.0)
// Groups (admin
groupAdm := handlers.NewGroupHandler(stores)
admin.GET("/groups", groupAdm.ListGroups)
admin.POST("/groups", groupAdm.CreateGroup)
@@ -711,11 +697,11 @@ func main() {
admin.POST("/groups/:id/members", groupAdm.AddMember)
admin.DELETE("/groups/:id/members/:userId", groupAdm.RemoveMember)
// Permissions (v0.24.2)
// Permissions
admin.GET("/permissions", groupAdm.ListPermissions)
admin.GET("/users/:id/permissions", groupAdm.GetUserPermissions)
// Resource Grants (admin — v0.16.0)
// Resource Grants (admin
admin.GET("/grants/:type/:id", groupAdm.GetResourceGrant)
admin.PUT("/grants/:type/:id", groupAdm.SetResourceGrant)
@@ -728,7 +714,7 @@ func main() {
// Vault
admin.GET("/vault/status", adm.VaultStatus)
// Email / SMTP test (v0.20.0 Phase 3)
// Email / SMTP test
emailAdm := handlers.NewAdminEmailHandler(stores)
admin.POST("/notifications/test-email", emailAdm.TestEmail)
@@ -739,7 +725,7 @@ func main() {
admin.PUT("/extensions/:id", extAdm.AdminUpdateExtension)
admin.DELETE("/extensions/:id", extAdm.AdminUninstallExtension)
// Extension permissions (admin — v0.29.0)
// Extension permissions (admin
extPermH := handlers.NewExtPermHandler(stores)
admin.GET("/extensions/:id/permissions", extPermH.ListPackagePermissions)
admin.GET("/extensions/:id/review", extPermH.ReviewPackage)
@@ -747,21 +733,21 @@ func main() {
admin.POST("/extensions/:id/permissions/:perm/revoke", extPermH.RevokePermission)
admin.POST("/extensions/:id/permissions/grant-all", extPermH.GrantAllPermissions)
// Extension secrets (admin — v0.29.0 CS3)
// Extension secrets (admin
extSecH := handlers.NewExtSecretsHandler(stores)
admin.GET("/extensions/:id/secrets", extSecH.GetSecrets)
admin.PUT("/extensions/:id/secrets", extSecH.SetSecrets)
admin.DELETE("/extensions/:id/secrets", extSecH.DeleteSecrets)
// Package lifecycle management (v0.28.7 — replaces surfaces + extensions admin)
// Package lifecycle management
packagesDir := ""
if cfg.StoragePath != "" {
packagesDir = cfg.StoragePath + "/packages"
}
pkgAdm := handlers.NewPackageHandler(stores, packagesDir)
pkgAdm.SetSandbox(sandbox.New(sandbox.DefaultConfig())) // v0.30.0: schema migrations
pkgAdm.SetSandbox(sandbox.New(sandbox.DefaultConfig()))
// Package registry — must be registered before /packages/:id (v0.30.0)
// Package registry — must be registered before /packages/:id
registryH := handlers.NewRegistryHandler(stores, packagesDir, pkgAdm)
admin.GET("/packages/registry", registryH.BrowseRegistry)
admin.POST("/packages/registry/install", registryH.InstallFromRegistry)
@@ -772,20 +758,20 @@ func main() {
admin.PUT("/packages/:id/enable", pkgAdm.EnablePackage)
admin.PUT("/packages/:id/disable", pkgAdm.DisablePackage)
admin.DELETE("/packages/:id", pkgAdm.DeletePackage)
admin.GET("/packages/:id/settings", pkgAdm.GetPackageSettings) // v0.30.0
admin.PUT("/packages/:id/settings", pkgAdm.UpdatePackageSettings) // v0.30.0
admin.GET("/packages/:id/dependencies", pkgAdm.ListDependencies) // v0.38.2
admin.GET("/packages/:id/consumers", pkgAdm.ListConsumers) // v0.38.2
admin.POST("/packages/:id/test-tool", pkgAdm.TestTool) // v0.38.2
admin.POST("/packages/:id/update", pkgAdm.UpdatePackage) // v0.5.4
admin.GET("/packages/:id/export", pkgAdm.ExportPackage) // v0.5.4
admin.GET("/dependencies", pkgAdm.ListAllDependencies) // v0.38.2
admin.GET("/packages/:id/settings", pkgAdm.GetPackageSettings)
admin.PUT("/packages/:id/settings", pkgAdm.UpdatePackageSettings)
admin.GET("/packages/:id/dependencies", pkgAdm.ListDependencies)
admin.GET("/packages/:id/consumers", pkgAdm.ListConsumers)
admin.POST("/packages/:id/test-tool", pkgAdm.TestTool)
admin.POST("/packages/:id/update", pkgAdm.UpdatePackage)
admin.GET("/packages/:id/export", pkgAdm.ExportPackage)
admin.GET("/dependencies", pkgAdm.ListAllDependencies)
// Workflow package export (v0.30.2)
// Workflow package export
wfPkgH := handlers.NewWorkflowPackageHandler(stores)
admin.GET("/workflows/:id/export", wfPkgH.ExportWorkflowPackage)
// ── Triggers (v0.2.2) ─────────────────
// ── Triggers ─────────────────
trigH := handlers.NewTriggerHandler(stores, triggerEngine)
admin.GET("/triggers", trigH.ListTriggers)
admin.GET("/triggers/:id", trigH.GetTrigger)
@@ -795,20 +781,20 @@ func main() {
admin.GET("/triggers/:id/logs", trigH.ListTriggerLogs)
admin.GET("/packages/:id/triggers", trigH.ListPackageTriggers)
// ── Scheduled Tasks — Admin (v0.2.2) ──
// ── Scheduled Tasks — Admin ──
schedH := handlers.NewScheduleHandler(stores, triggerEngine)
admin.GET("/schedules", schedH.AdminListSchedules)
admin.PUT("/schedules/:id/enable", schedH.AdminEnableSchedule)
admin.PUT("/schedules/:id/disable", schedH.AdminDisableSchedule)
admin.DELETE("/schedules/:id", schedH.AdminDeleteSchedule)
// ── Cluster (v0.6.0) ─────────────────
// ── Cluster ─────────────────
if stores.Cluster != nil {
clusterH := handlers.NewClusterHandler(stores)
admin.GET("/cluster", clusterH.ListNodes)
}
// ── Backup/Restore (v0.6.1) ─────────
// ── Backup/Restore ─────────
backupH := handlers.NewBackupHandler(stores, packagesDir, cfg.StoragePath)
admin.POST("/backup", backupH.CreateBackup)
admin.GET("/backups", backupH.ListBackups)
@@ -828,9 +814,7 @@ func main() {
}
// ── Page Routes ──────────────────────────
// v0.27.0: Extension surface static assets (JS, CSS, images).
// Served without auth — same rationale as extension assets (script tags can't send headers).
// v0.28.7: on-disk path moved to /data/packages/, URL stays /surfaces/:id/ for stability.
// In split deployment, nginx serves these from /data/packages/ directly.
if cfg.StoragePath != "" {
packagesStaticDir := cfg.StoragePath + "/packages"
@@ -850,13 +834,12 @@ func main() {
pages.SetVersion(Version)
pageEngine := pages.New(cfg, stores)
// Root redirect → configurable default surface (v0.2.1)
// Root redirect → configurable default surface
base.GET("/", pageEngine.DefaultSurfaceRedirect())
// Login page — no auth required
base.GET("/login", pageEngine.RenderLogin())
// v0.25.0: Surface routes generated from manifest registry.
// Replaces manual per-surface route blocks.
pageEngine.RegisterPageRoutes(base, pages.PageRouteMiddleware{
Authenticated: middleware.AuthOrRedirect(cfg, stores.Users, userCache),
@@ -864,10 +847,9 @@ func main() {
middleware.AuthOrRedirect(cfg, stores.Users, userCache),
middleware.RequireAdminPage(stores),
},
Session: middleware.AuthOrRedirect(cfg, stores.Users, userCache), // TODO: session middleware for workflow visitors (v0.2.0)
Session: middleware.AuthOrRedirect(cfg, stores.Users, userCache), // TODO: session middleware for workflow visitors
})
// v0.29.1: Extension API routes — Starlark packages serve JSON endpoints.
// Mounted at /s/:slug/api/* with JWT auth (returns 401, not redirect).
{
extAPIH := handlers.NewExtAPIHandler(stores, starlarkRunner)

View File

@@ -290,7 +290,7 @@ type TicketValidator interface {
// WsAuth returns a Gin middleware for the WebSocket endpoint.
// It authenticates via three methods in priority order:
//
// 1. ?ticket=<opaque> — single-use ticket (preferred, v0.28.8+)
// 1. ?ticket=<opaque> — single-use ticket (preferred)
// 2. ?token=<jwt> — legacy JWT in query param (deprecated)
// 3. Authorization header — standard Bearer JWT
//

View File

@@ -10,7 +10,6 @@ import (
)
// RateLimiter implements per-IP rate limiting backed by a shared store.
// v0.32.0: replaces in-memory token bucket for cross-pod consistency.
//
// Fail-open: if the store is unavailable (PG down), requests are allowed.
// Auth endpoints have their own protections (bcrypt, lockout); blocking

View File

@@ -10,7 +10,6 @@ import (
// RequireTeamAdmin returns middleware that restricts access to team admins.
// System admins are always allowed through.
// v0.29.0: accepts TeamStore instead of using database.DB directly.
func RequireTeamAdmin(teams store.TeamStore) gin.HandlerFunc {
return func(c *gin.Context) {
role, _ := c.Get("role")
@@ -43,7 +42,6 @@ func RequireTeamAdmin(teams store.TeamStore) gin.HandlerFunc {
// RequireTeamMember returns middleware that restricts access to users who
// belong to the team identified by :teamId (any role).
// System admins are always allowed through.
// v0.29.0: accepts TeamStore instead of using database.DB directly.
func RequireTeamMember(teams store.TeamStore) gin.HandlerFunc {
return func(c *gin.Context) {
role, _ := c.Get("role")

View File

@@ -1,6 +1,6 @@
package models
// ── Extension Connections (v0.38.1) ──────────────────
// ── Extension Connections ──────────────────
// ExtConnection represents a scoped credential/endpoint configuration
// for extensions that integrate with external services. Same scope

View File

@@ -1,6 +1,6 @@
package models
// ── Extension Dependencies (v0.38.2) ──────────────────
// ── Extension Dependencies ──────────────────
// ExtDependency records that a consumer package depends on a library package.
// Created at consumer install time from the manifest's "dependencies" map.

View File

@@ -218,7 +218,7 @@ func IntPtr(i int) *int { return &i }
func BoolPtr(b bool) *bool { return &b }
// =========================================
// GROUPS (v0.16.0)
// GROUPS
// =========================================
// Group scope constants (reuses ScopeGlobal, ScopeTeam from above)
@@ -286,7 +286,7 @@ type ResourceGrant struct {
// KBChunk is a text segment from a document with its embedding vector.
// KBSearchResult is a single result from similarity search.
// ChannelKB represents a knowledge base linked to a channel.
// PROVIDER HEALTH (v0.22.0)
// PROVIDER HEALTH
// ProviderStatus represents the derived health state.
@@ -294,10 +294,10 @@ type ResourceGrant struct {
// ErrorRate returns the error fraction, or 0 if no requests.
// CAPABILITY OVERRIDES (v0.22.0)
// CAPABILITY OVERRIDES
// CapabilityOverride is an admin correction for a model's capabilities.
// ROUTING POLICIES (v0.22.2)
// ROUTING POLICIES
// RoutingPolicy is one routing rule that controls how requests are
// dispatched to provider configs. Stored in the routing_policies table.

View File

@@ -31,11 +31,11 @@ const (
ExtPermDBRead = "db.read"
ExtPermDBWrite = "db.write"
ExtPermAPIHTTP = "api.http"
ExtPermFormValidate = "forms.validate" // v0.29.3: form validation hooks
ExtPermWorkflowAccess = "workflow.access" // v0.30.2: workflow definition + stage data access
ExtPermConnectionsRead = "connections.read" // v0.38.1: extension connection resolution
ExtPermTriggersRegister = "triggers.register" // v0.2.2: event/webhook trigger registration
ExtPermRealtimePublish = "realtime.publish" // v0.5.0: publish events to realtime channels
ExtPermFormValidate = "forms.validate"
ExtPermWorkflowAccess = "workflow.access"
ExtPermConnectionsRead = "connections.read"
ExtPermTriggersRegister = "triggers.register"
ExtPermRealtimePublish = "realtime.publish"
)
// ValidExtensionPermissions is the set of recognized permission keys.

View File

@@ -3,7 +3,7 @@ package models
import "time"
// =========================================
// NOTIFICATIONS (v0.20.0)
// NOTIFICATIONS
// =========================================
// Notification represents an in-app notification for a user.

View File

@@ -120,22 +120,21 @@ var ValidAudiences = map[string]bool{
// ── Typed Form Template ─────────────────────
// TypedFormTemplate is the structured form_template schema (v0.29.3).
// v0.35.0: added Fieldsets for progressive multi-step forms.
// TypedFormTemplate is the structured form_template schema.
type TypedFormTemplate struct {
Fields []FormField `json:"fields"`
Fieldsets []FormFieldset `json:"fieldsets,omitempty"` // v0.35.0: multi-step form groups
Fieldsets []FormFieldset `json:"fieldsets,omitempty"`
Hooks *FormHooks `json:"hooks,omitempty"`
}
// FormFieldset groups fields into a labelled step for progressive forms (v0.35.0).
// FormFieldset groups fields into a labelled step for progressive forms.
// When fieldsets is present, top-level fields is ignored.
type FormFieldset struct {
Label string `json:"label"`
Fields []FormField `json:"fields"`
}
// FieldCondition controls conditional visibility of a form field (v0.35.0).
// FieldCondition controls conditional visibility of a form field.
// When set, the field is shown/validated only if the condition is met.
type FieldCondition struct {
When string `json:"when"` // key of the field to check
@@ -153,7 +152,7 @@ type FormField struct {
Default interface{} `json:"default,omitempty"`
Options []FormOption `json:"options,omitempty"`
Validation *FormValidation `json:"validation,omitempty"`
Condition *FieldCondition `json:"condition,omitempty"` // v0.35.0: conditional visibility
Condition *FieldCondition `json:"condition,omitempty"`
}
// FormOption is a choice for select fields.
@@ -200,7 +199,6 @@ func ParseTypedFormTemplate(raw json.RawMessage) *TypedFormTemplate {
return nil
}
// v0.35.0: If fieldsets present, flatten into fields for backward compat
if len(tpl.Fieldsets) > 0 {
var allFields []FormField
for _, fs := range tpl.Fieldsets {
@@ -237,7 +235,6 @@ type FieldError struct {
func ValidateFormData(tpl *TypedFormTemplate, data map[string]interface{}) []FieldError {
var errs []FieldError
for _, f := range tpl.Fields {
// v0.35.0: Skip fields whose condition is not met
if f.Condition != nil && !evaluateFieldCondition(f.Condition, data) {
continue
}
@@ -392,7 +389,7 @@ func validateSelect(f FormField, val interface{}) []FieldError {
return []FieldError{{Key: f.Key, Message: f.Label + " is not a valid option"}}
}
// ── Field Condition Evaluator (v0.35.0) ─────
// ── Field Condition Evaluator ─────
// evaluateFieldCondition checks if a conditional field should be visible/validated.
func evaluateFieldCondition(cond *FieldCondition, data map[string]interface{}) bool {
@@ -505,7 +502,7 @@ type WorkflowAssignment struct {
CreatedAt time.Time `json:"created_at"`
}
// ── Workflow Signoff (v0.3.4) ───────────────
// ── Workflow Signoff ───────────────
// WorkflowSignoff records a user's approval or rejection at a stage boundary.
type WorkflowSignoff struct {

View File

@@ -14,7 +14,7 @@ import (
// ── Role Fallback ───────────────────────────
// Subscribes to the existing "role.fallback" EventBus event (emitted by
// capabilities/resolver.go since v0.17.0) and creates notifications for
// capabilities/resolver.go) and creates notifications for
// all admin users.
// roleFallbackPayload matches the payload emitted by the role resolver.

View File

@@ -28,10 +28,10 @@ type AdminPageData struct {
}
// SettingsPageData is what the settings surface templates receive.
// Feature gates (BYOK, personas) moved to sw.auth.policies in v0.37.19.
// Feature gates (BYOK, personas) moved to sw.auth.policies.
type SettingsPageData struct {
Section string `json:"section"`
ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"` // v0.38.3
ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"`
}
// ── Loader registration ──────────────────────
@@ -39,7 +39,7 @@ type SettingsPageData struct {
// TeamAdminPageData is what the team-admin surface receives.
type TeamAdminPageData struct {
Section string `json:"section"`
ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"` // v0.38.3
ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"`
}
func (e *Engine) registerLoaders() {
@@ -91,7 +91,6 @@ func sectionCategory(section string) string {
}
// ── Settings loader ──────────────────────────
// v0.22.7: Reads feature gates from GlobalConfig to control
// which nav links/tabs are visible (BYOK, User Personas).
func (e *Engine) teamAdminLoader(c *gin.Context, s store.Stores) (any, error) {
@@ -117,7 +116,7 @@ func (e *Engine) settingsLoader(c *gin.Context, s store.Stores) (any, error) {
}, nil
}
// ── Config section discovery (v0.38.3) ───────
// ── Config section discovery ───────
//
// Packages declare a config_section in their manifest. This helper scans
// enabled packages and returns entries whose surfaces array includes the

View File

@@ -38,7 +38,7 @@ type Engine struct {
cfg *config.Config
stores store.Stores
loaders map[string]DataLoaderFunc
surfaces []SurfaceManifest // v0.25.0: registered surface definitions
surfaces []SurfaceManifest
devMode bool
}
@@ -95,25 +95,20 @@ type PageData struct {
User *UserContext
Data any // surface-specific data from loader
// v0.22.7: Theme + settings injection
Theme string // "dark", "light", or "" (= use default)
SurfaceSettings any // JSON-serialized to window.__SETTINGS__
// v0.25.0: Surface manifest — layout preset, components, etc.
Manifest *SurfaceManifest `json:"manifest,omitempty"`
// v0.25.0: Enabled surface IDs for conditional nav rendering.
EnabledSurfaces []string `json:"-"`
// v0.27.0: Extension surfaces for sidebar nav rendering.
ExtensionSurfaces []ExtensionNavItem `json:"-"`
// v0.22.7: Login/splash page fields
InstanceName string // branding: instance display name
LogoURL string // branding: custom logo URL
Tagline string // branding: tagline under instance name
RegistrationOpen bool // whether self-registration is enabled
AuthMode string // v0.24.1: "builtin", "mtls", "oidc"
AuthMode string // "builtin", "mtls", "oidc"
}
// SurfaceEnabled returns true if the given surface ID is in the EnabledSurfaces list.
@@ -137,7 +132,6 @@ type ExtensionNavItem struct {
// ConfigSectionEntry describes a package-declared config section for
// lazy-loading in Settings, Admin, or Team Admin surfaces.
// v0.38.3: manifest-driven discovery — no new tables or endpoints.
type ConfigSectionEntry struct {
PackageID string `json:"package_id"` // section key & asset path prefix
Label string `json:"label"` // nav link text
@@ -207,7 +201,7 @@ func New(cfg *config.Config, stores store.Stores) *Engine {
e.parseTemplates()
e.registerLoaders()
e.registerCoreSurfaces()
e.SeedSurfaces() // v0.25.0: Write core manifests to DB (preserves admin toggle state)
e.SeedSurfaces()
return e
}
@@ -278,7 +272,6 @@ func (e *Engine) parseTemplates() {
"toJSON": toJSON,
"eq": func(a, b string) bool { return a == b },
"contains": stringContains,
"roleFilterType": roleFilterType,
"esc": template.HTMLEscapeString,
"safeHTML": func(s string) template.HTML { return template.HTML(s) },
"join": strings.Join,
@@ -323,7 +316,6 @@ func (e *Engine) Render(c *gin.Context, name string, data PageData) {
data.Message = e.loadMessage()
data.Footer = e.loadFooter()
// v0.22.7: Default theme if not set
if data.Theme == "" {
data.Theme = "dark"
}
@@ -478,7 +470,6 @@ func (e *Engine) RegisterPageRoutes(base *gin.RouterGroup, mw PageRouteMiddlewar
registerRoutes(group, s, handler)
}
// v0.27.0: Extension surface catch-all — DB lookup at request time.
// No restart needed after installing new surfaces via admin API.
group.GET("/s/:slug", e.RenderExtensionSurface())
}
@@ -593,7 +584,6 @@ func stripPrefix(route, prefix string) string {
// RenderLogin serves the standalone login page.
func (e *Engine) RenderLogin() gin.HandlerFunc {
return func(c *gin.Context) {
// v0.22.7: Populate splash/login fields from global config
instanceName, logoURL, tagline := e.loadBranding()
regOpen := e.isRegistrationOpen()
@@ -620,8 +610,8 @@ type WorkflowPageData struct {
FormTemplateJSON string // typed form template JSON (empty if custom)
TotalStages int
CurrentStage int
SurfacePkgID string // v0.30.2: custom package surface override (empty = use StageMode)
BrandingJSON string // v0.35.0: workflow branding JSON (accent_color, logo_url, tagline)
SurfacePkgID string
BrandingJSON string
}
// WorkflowLandingPageData is passed to workflow-landing.html.
@@ -639,7 +629,7 @@ type WorkflowLandingPageData struct {
PersonaName string
PersonaIcon string
StageCount int
FirstStageMode string // custom | form_only | form_chat (v0.29.3)
FirstStageMode string // custom | form_only | form_chat
ResumeURL string // non-empty if visitor has an active session
}
@@ -659,7 +649,7 @@ func (e *Engine) RenderWorkflow() gin.HandlerFunc {
// Load session display name
sessionName := "Visitor"
// Load workflow stage info for form rendering (v0.29.3)
// Load workflow stage info for form rendering
var stageMode, stageName, formTplJSON, surfacePkgID, brandingJSON string
var totalStages, currentStage int
stageMode = "custom" // default
@@ -919,18 +909,6 @@ func stringContains(haystack []string, needle string) bool {
return false
}
// roleFilterType returns the model_type filter for a given role name.
func roleFilterType(role string) string {
switch role {
case "embedding":
return "embedding"
case "utility":
return "chat"
default:
return ""
}
}
// dict creates a map from alternating key/value pairs for template use.
func dict(pairs ...any) map[string]any {
m := make(map[string]any, len(pairs)/2)

View File

@@ -35,7 +35,6 @@ func (e *Engine) SeedSurfaces() {
}
log.Printf("[pages] Seeded %d core surfaces into registry", len(e.surfaces))
// v0.31.0: Clean up old "editor" core surface row.
// Editor is now an installable package — the old seed row would show a
// broken nav link until the package .pkg is uploaded via admin UI.
if sr, err := e.stores.Packages.Get(ctx, "editor"); err == nil && sr != nil && sr.Source == "extension" {

View File

@@ -1,6 +1,5 @@
// Package sandbox — connections_module.go
//
// v0.38.1: Connections module for Starlark extensions.
// Requires permission: connections.read
//
// Starlark API:

View File

@@ -1,6 +1,5 @@
// Package sandbox — db_module.go
//
// v0.29.2 CS1: Structured db module for Starlark extensions.
//
// Permissions:
// db.read → db.query(), db.view(), db.list_tables()

View File

@@ -1,6 +1,5 @@
// Package sandbox — http_module.go
//
// v0.29.1 CS0: HTTP outbound module for Starlark extensions.
// Requires permission: api.http
//
// Starlark API:

View File

@@ -1,6 +1,5 @@
// Package sandbox — lib_module.go
//
// v0.38.2: Library loading module for Starlark extensions.
// No permission required — any starlark package can use lib.require().
//
// Starlark API:

View File

@@ -1,6 +1,5 @@
// 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

View File

@@ -1,6 +1,5 @@
// Package sandbox — realtime_module.go
//
// v0.5.0: Starlark realtime.publish() module.
//
// Allows extensions to publish events to WebSocket channels.
// Events are scoped to rooms — only clients subscribed to the

View File

@@ -1,19 +1,14 @@
// Package sandbox — runner.go
//
// v0.29.0 CS3: Runner loads a package's Starlark script, assembles
// the module set based on granted permissions, and executes it.
//
// v0.29.1 CS0: Wires api.http permission to BuildHTTPModule with
// network_access config from package manifest.
//
// v0.29.1 CS2: Adds RunContext for per-invocation state (user_id),
// vault for provider key decryption, and provider.complete module.
//
// v0.29.2 CS1: Adds db *sql.DB for the db module. SetDB() wires it
// at startup. db.read grants query/view/list_tables; db.write adds
// insert/update/delete. If both are granted, db.write wins (superset).
//
// v0.38.2: Adds lib.load() for library packages. Libraries run with
// their own permission context. Per-invocation lib cache + cycle detection.
//
// The runner is the bridge between the package/permission system
@@ -48,7 +43,7 @@ type RunContext struct {
// ChannelID is the context identifier, if any.
ChannelID string
// TeamID is the team context for settings cascade resolution (v0.2.0).
// TeamID is the team context for settings cascade resolution.
// When set, team-scoped package settings are included in the cascade.
TeamID string
}
@@ -57,13 +52,13 @@ type RunContext struct {
type Runner struct {
sandbox *Sandbox
stores store.Stores
packagesDir string // v0.38.0: disk path for load() support
packagesDir string
notifier NotificationSender // nil = notifications module unavailable
connResolver ConnectionResolver // nil = connections module unavailable (v0.38.1)
connResolver ConnectionResolver // nil = connections module unavailable
db *sql.DB // nil = db module unavailable
dbPostgres bool // true = use $N placeholders; false = use ?
allowPrivateIPs bool // v0.38.5: disable SSRF private IP check
bus *events.Bus // v0.5.0: event bus for realtime module
allowPrivateIPs bool
bus *events.Bus
}
// NewRunner creates a runner with the given sandbox and dependencies.
@@ -79,7 +74,7 @@ func (r *Runner) SetNotifier(n NotificationSender) {
r.notifier = n
}
// SetConnectionResolver attaches the connection resolver for the connections module (v0.38.1).
// SetConnectionResolver attaches the connection resolver for the connections module.
func (r *Runner) SetConnectionResolver(cr ConnectionResolver) {
r.connResolver = cr
}
@@ -94,12 +89,11 @@ func (r *Runner) SetDB(db *sql.DB, isPostgres bool) {
// SetPackagesDir sets the disk path where package archives are extracted.
// Required for load() support — scripts are read from disk at runtime.
// v0.38.0.
func (r *Runner) SetPackagesDir(dir string) {
r.packagesDir = dir
}
// SetBus attaches the event bus for the realtime module (v0.5.0).
// SetBus attaches the event bus for the realtime module.
func (r *Runner) SetBus(bus *events.Bus) {
r.bus = bus
}
@@ -114,7 +108,6 @@ func (r *Runner) SetAllowPrivateIPs(allow bool) {
// ExecPackage loads a package's script from disk (primary) or manifest
// (legacy) and executes it with modules gated by granted permissions.
//
// v0.38.0: Disk-based loading + package-scoped load() support.
//
// The RunContext carries per-invocation state (user_id for provider
// resolution). Pass nil if no per-invocation context is needed.
@@ -136,7 +129,6 @@ func (r *Runner) ExecPackage(ctx context.Context, pkg *store.PackageRegistration
return nil, err
}
// v0.38.2: per-invocation lib context for lib.load() caching + cycle detection
lc := newLibContext()
// Build modules based on granted permissions
@@ -288,7 +280,7 @@ func (r *Runner) buildModules(ctx context.Context, packageID string, manifest ma
// The manifest is passed through so modules can read their config
// (e.g., http module reads network_access, provider reads requires_provider).
// The RunContext carries per-invocation state for user-scoped modules.
// The libContext enables lib.load() caching and cycle detection (v0.38.2).
// The libContext enables lib.load() caching and cycle detection.
func (r *Runner) buildModulesWithLibCtx(ctx context.Context, packageID string, manifest map[string]any, rc *RunContext, lc *libContext) (map[string]starlark.Value, error) {
if r.stores.ExtPermissions == nil {
return nil, nil
@@ -352,8 +344,7 @@ func (r *Runner) buildModulesWithLibCtx(ctx context.Context, packageID string, m
})
}
// v0.30.0: settings module — always injected, no permission required.
// A package reads its own admin + team + user settings (v0.2.0 cascade).
// A package reads its own admin + team + user settings.
userID := ""
teamID := ""
if rc != nil {
@@ -362,7 +353,6 @@ func (r *Runner) buildModulesWithLibCtx(ctx context.Context, packageID string, m
}
modules["settings"] = BuildSettingsModule(ctx, r.stores, packageID, userID, teamID)
// v0.38.2: lib module — always injected, no permission required.
// Allows any starlark package to load declared library dependencies.
if lc != nil {
modules["lib"] = BuildLibModule(ctx, r, packageID, rc, lc)

View File

@@ -1,6 +1,5 @@
// Package sandbox — sandboxed Starlark interpreter.
//
// v0.29.0 CS1: Provides execution with timeout, step limits,
// captured output, and injectable predeclared modules.
//
// The sandbox is intentionally restrictive:

View File

@@ -1,6 +1,6 @@
package sandbox
// settings_module.go — v0.2.0
// settings_module.go
//
// The settings module lets extensions read their resolved settings via
// the three-tier cascade: global → team → user, respecting the

View File

@@ -1,6 +1,5 @@
// Package sandbox — unicode_scan.go
//
// v0.5.1: Invisible Unicode scanning gate.
//
// Defends against GlassWorm (variation selector payloads), Trojan Source
// (bidi override attacks / CVE-2021-42574), and other invisible Unicode

View File

@@ -1,6 +1,6 @@
package sandbox
// workflow_module.go — v0.30.2 CS1, v0.3.2 instance API
// workflow_module.go
//
// The workflow module lets extensions read workflow definitions,
// manage instances, and programmatically advance workflow stages.
@@ -27,7 +27,6 @@ import (
// BuildWorkflowModule creates the "workflow" Starlark module for a package.
// Requires the workflow.access permission.
//
// v0.3.2: added get_instance and list_instances for read-only instance access.
// Mutating operations (start/advance/cancel) are available via HTTP API;
// Starlark builtins for them will be added when the engine interface is
// extracted to a shared package to avoid circular imports.
@@ -87,7 +86,7 @@ func workflowGetDef(ctx context.Context, stores store.Stores) func(*starlark.Thr
}
}
// ── Instance Read API (v0.3.2) ──────────────
// ── Instance Read API ──────────────
func workflowGetInstance(ctx context.Context, stores store.Stores) func(*starlark.Thread, *starlark.Builtin, starlark.Tuple, []starlark.Tuple) (starlark.Value, error) {
return func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
@@ -187,5 +186,5 @@ func goValToStarlark(v interface{}) starlark.Value {
}
}
// workflowRoute routes the workflow to a named stage (v0.35.0).
// workflowRoute routes the workflow to a named stage.
// Starlark: workflow.route(channel_id, target_stage, reason)

View File

@@ -42,19 +42,19 @@ type Stores struct {
ResourceGrants ResourceGrantStore
Notifications NotificationStore
NotifPrefs NotificationPreferenceStore
Presence PresenceStore // v0.29.0: User online/offline status
Health HealthStore // v0.29.0: Provider health window management
Connections ConnectionStore // v0.38.1: Extension connection credentials
Dependencies DependencyStore // v0.38.2: Library package dependency graph
Packages PackageStore // v0.28.7: Unified package registry (surfaces + extensions)
Workflows WorkflowStore // v0.26.1: Workflow definitions + stages
ExtPermissions ExtensionPermissionStore // v0.29.0: Extension declared/granted capabilities
ExtData ExtDataStore // v0.29.2: Extension namespaced table catalog
Tickets TicketStore // v0.32.0: WS auth tickets (PG-backed for cross-pod)
RateLimits RateLimitStore // v0.32.0: Distributed rate limiting
Triggers TriggerStore // v0.2.2: Extension event/webhook triggers
ScheduledTasks ScheduledTaskStore // v0.2.2: User-created cron tasks
Cluster ClusterStore // v0.6.0: PG-backed cluster node registry (nil on SQLite)
Presence PresenceStore
Health HealthStore
Connections ConnectionStore
Dependencies DependencyStore
Packages PackageStore
Workflows WorkflowStore
ExtPermissions ExtensionPermissionStore
ExtData ExtDataStore
Tickets TicketStore
RateLimits RateLimitStore
Triggers TriggerStore
ScheduledTasks ScheduledTaskStore
Cluster ClusterStore
}
// TeamAvailableModel is returned by CatalogStore.ListTeamAvailable.
@@ -85,7 +85,7 @@ type UserStore interface {
RevokeAllRefreshTokens(ctx context.Context, userID string) error
CleanExpiredTokens(ctx context.Context) error
// ── CS1 additions (v0.29.0) ──
// ── CS1 additions ──
// Exists returns true if a user with the given ID exists and is active.
Exists(ctx context.Context, userID string) (bool, error)
@@ -94,7 +94,7 @@ type UserStore interface {
// Excludes the calling user. Max 20 results.
SearchActive(ctx context.Context, excludeUserID, query string) ([]UserSearchResult, error)
// ── CS2 additions (v0.29.0) ──
// ── CS2 additions ──
// CountAll returns the total number of users.
CountAll(ctx context.Context) (int, error)
@@ -108,7 +108,7 @@ type UserStore interface {
// UpdateVaultKeys re-wraps the user's vault keys (password change).
UpdateVaultKeys(ctx context.Context, userID string, encUEK, salt, nonce []byte) error
// ── CS4 additions (v0.29.0) ──
// ── CS4 additions ──
// ClearVaultKeys nulls out vault columns and sets vault_set = false.
ClearVaultKeys(ctx context.Context, userID string) error
@@ -147,7 +147,7 @@ type TeamStore interface {
IsTeamAdmin(ctx context.Context, teamID, userID string) (bool, error)
IsMember(ctx context.Context, teamID, userID string) (bool, error)
// ── CS1 additions (v0.29.0) ──
// ── CS1 additions ──
// Exists returns true if a team with the given ID exists.
Exists(ctx context.Context, teamID string) (bool, error)
@@ -161,12 +161,12 @@ type TeamStore interface {
// ListTeamAuditActions returns distinct audit actions for a team's members.
ListTeamAuditActions(ctx context.Context, teamID string) ([]string, error)
// ── CS5b additions (v0.29.0) ──
// ── CS5b additions ──
// GetFirstTeamIDForUser returns the first team_id the user belongs to, or "" if none.
GetFirstTeamIDForUser(ctx context.Context, userID string) (string, error)
// ── CS6 additions (v0.29.0) ──
// ── CS6 additions ──
// AddMemberReturningID inserts a team member and returns the row ID.
AddMemberReturningID(ctx context.Context, teamID, userID, role string) (string, error)
@@ -175,11 +175,6 @@ type TeamStore interface {
MergeSettings(ctx context.Context, teamID, settingsJSON string) error
}
// ChannelListFilter holds filter options for ChannelStore.ListFiltered.
// ChannelListItem is returned by ListFiltered and GetForUser.
// ArchivedChannel is a view model for the admin archived channels list.
// MessageWithSender is returned by MessageStore.ListWithSenderInfo.
// ChannelSearchResult is returned by MessageStore.SearchInChannel.
// =========================================
// AUDIT STORE
// =========================================
@@ -212,7 +207,7 @@ type GlobalConfigStore interface {
Set(ctx context.Context, key string, value models.JSONMap, updatedBy string) error
GetAll(ctx context.Context) (map[string]models.JSONMap, error)
// ── OIDC state (v0.29.0-cs4) ──
// ── OIDC state ──
// SaveOIDCState stores a state+nonce pair for OIDC callback verification.
SaveOIDCState(ctx context.Context, state, nonce, redirectTo string) error
@@ -224,7 +219,7 @@ type GlobalConfigStore interface {
// CleanupOIDCState removes stale OIDC states older than 10 minutes.
CleanupOIDCState(ctx context.Context) error
// ── CS6 additions (v0.29.0) ──
// ── CS6 additions ──
// GetString returns the raw value column as a string for simple settings
// (e.g. bare JSON booleans like "true"/"false").
@@ -232,7 +227,7 @@ type GlobalConfigStore interface {
}
// =========================================
// GROUP STORE (v0.16.0)
// GROUP STORE
// =========================================
type GroupStore interface {
@@ -257,7 +252,7 @@ type GroupStore interface {
}
// =========================================
// RESOURCE GRANT STORE (v0.16.0)
// RESOURCE GRANT STORE
// =========================================
type ResourceGrantStore interface {
@@ -270,7 +265,7 @@ type ResourceGrantStore interface {
}
// =========================================
// NOTIFICATION STORE (v0.20.0)
// NOTIFICATION STORE
// =========================================
type NotificationStore interface {
@@ -284,7 +279,7 @@ type NotificationStore interface {
}
// =========================================
// NOTIFICATION PREFERENCES STORE (v0.20.0)
// NOTIFICATION PREFERENCES STORE
// =========================================
type NotificationPreferenceStore interface {
@@ -299,12 +294,12 @@ type NotificationPreferenceStore interface {
}
// =========================================
// SESSION STORE (v0.24.3)
// SESSION STORE
// =========================================
// =========================================
// HEALTH STORE (v0.29.0)
// HEALTH STORE
// =========================================
// HealthStore manages provider health windows. The full interface is used
@@ -315,7 +310,7 @@ type HealthStore interface {
}
// =========================================
// PRESENCE STORE (v0.29.0)
// PRESENCE STORE
// =========================================
// PresenceStore manages user online/offline status.
@@ -334,7 +329,7 @@ type PresenceStore interface {
// =========================================
// SHARED TYPES
// =========================================
// CONNECTION STORE (v0.38.1)
// CONNECTION STORE
// =========================================
type ConnectionStore interface {
@@ -367,7 +362,7 @@ type ConnectionStore interface {
}
// =========================================
// DEPENDENCY STORE (v0.38.2)
// DEPENDENCY STORE
// =========================================
type DependencyStore interface {

View File

@@ -6,7 +6,7 @@ import (
)
// PackageStore manages the unified package registry (surfaces + extensions).
// Replaces SurfaceRegistryStore and ExtensionStore (v0.28.7).
// Replaces SurfaceRegistryStore and ExtensionStore.
type PackageStore interface {
// ── Lifecycle (from SurfaceRegistryStore) ────────────
@@ -66,13 +66,13 @@ type PackageStore interface {
// DeleteUserSettings removes per-user settings, reverting to defaults.
DeleteUserSettings(ctx context.Context, pkgID, userID string) error
// ── Scoped visibility (v0.30.0) ────────────────
// ── Scoped visibility ────────────────
// ListVisiblePackages returns packages visible to the given user:
// global packages + team packages for user's teams + personal packages.
ListVisiblePackages(ctx context.Context, userID string) ([]PackageRegistration, error)
// ── Package lifecycle (v0.30.0) ─────────────────
// ── Package lifecycle ─────────────────
// SetSchemaVersion updates the current schema version for a package.
SetSchemaVersion(ctx context.Context, id string, version int) error
@@ -83,7 +83,7 @@ type PackageStore interface {
// SetPackageSettings stores admin-configured package-level settings.
SetPackageSettings(ctx context.Context, id string, settings json.RawMessage) error
// ── Team-level settings (v0.2.0) ─────────────────
// ── Team-level settings ─────────────────
// GetTeamSettings returns team-scoped overrides for a package.
GetTeamSettings(ctx context.Context, pkgID, teamID string) (json.RawMessage, error)
@@ -134,7 +134,7 @@ type PackageUserSettings struct {
IsEnabled bool `json:"is_enabled" db:"is_enabled"`
}
// PackageTeamSettings stores team-scoped overrides for a package (v0.2.0).
// PackageTeamSettings stores team-scoped overrides for a package.
type PackageTeamSettings struct {
PackageID string `json:"package_id" db:"package_id"`
TeamID string `json:"team_id" db:"team_id"`

View File

@@ -9,7 +9,7 @@ import (
"switchboard-core/models"
)
// ── ConnectionStore (v0.38.1) ────────────────────────
// ── ConnectionStore ────────────────────────
type ConnectionStore struct{}

View File

@@ -7,7 +7,7 @@ import (
"switchboard-core/models"
)
// ── DependencyStore (v0.38.2) ────────────────────────
// ── DependencyStore ────────────────────────
type DependencyStore struct{}

View File

@@ -58,7 +58,7 @@ func (s *GlobalConfigStore) GetAll(ctx context.Context) (map[string]models.JSONM
return result, rows.Err()
}
// ── OIDC state (v0.29.0-cs4) ────────────────────────────────────────────
// ── OIDC state ────────────────────────────────────────────
func (s *GlobalConfigStore) SaveOIDCState(ctx context.Context, state, nonce, redirectTo string) error {
_, err := DB.ExecContext(ctx, `
@@ -86,7 +86,7 @@ func (s *GlobalConfigStore) CleanupOIDCState(ctx context.Context) error {
return err
}
// ── CS6 additions (v0.29.0) ─────────────────────────────────────────────
// ── CS6 additions ─────────────────────────────────────────────
func (s *GlobalConfigStore) GetString(ctx context.Context, key string) (string, error) {
var val string

View File

@@ -310,7 +310,7 @@ func (s *PackageStore) scanMany(ctx context.Context, query string, args ...inter
return result, rows.Err()
}
// ── Scoped visibility (v0.30.0) ──────────────────
// ── Scoped visibility ──────────────────
func (s *PackageStore) ListVisiblePackages(ctx context.Context, userID string) ([]store.PackageRegistration, error) {
return s.scanMany(ctx, `
@@ -327,7 +327,7 @@ func (s *PackageStore) ListVisiblePackages(ctx context.Context, userID string) (
ORDER BY p.source, p.title`, userID)
}
// ── Package lifecycle (v0.30.0) ──────────────────
// ── Package lifecycle ──────────────────
func (s *PackageStore) SetSchemaVersion(ctx context.Context, id string, version int) error {
result, err := DB.ExecContext(ctx,
@@ -367,7 +367,7 @@ func (s *PackageStore) SetPackageSettings(ctx context.Context, id string, settin
return nil
}
// ── Team-level settings (v0.2.0) ─────────────────
// ── Team-level settings ─────────────────
func (s *PackageStore) GetTeamSettings(ctx context.Context, pkgID, teamID string) (json.RawMessage, error) {
var settings []byte

View File

@@ -7,7 +7,6 @@ import (
)
// RateLimitStore manages distributed rate limit counters in Postgres.
// v0.32.0: fixed-window counter — upsert per-second bucket, check burst.
type RateLimitStore struct{}
func NewRateLimitStore() *RateLimitStore { return &RateLimitStore{} }

View File

@@ -224,7 +224,7 @@ func (s *TeamStore) IsMember(ctx context.Context, teamID, userID string) (bool,
}
// ── CS1 additions (v0.29.0) ─────────────────────────────────────────────
// ── CS1 additions ─────────────────────────────────────────────
func (s *TeamStore) Exists(ctx context.Context, teamID string) (bool, error) {
var exists bool
@@ -279,7 +279,7 @@ func (s *TeamStore) ListTeamAuditActions(ctx context.Context, teamID string) ([]
return actions, rows.Err()
}
// ── CS5b additions (v0.29.0) ────────────────────────────────────────────
// ── CS5b additions ────────────────────────────────────────────
func (s *TeamStore) GetFirstTeamIDForUser(ctx context.Context, userID string) (string, error) {
var teamID string
@@ -291,7 +291,7 @@ func (s *TeamStore) GetFirstTeamIDForUser(ctx context.Context, userID string) (s
return teamID, nil
}
// ── CS6 additions (v0.29.0) ────────────────────────────────────────────
// ── CS6 additions ────────────────────────────────────────────
func (s *TeamStore) AddMemberReturningID(ctx context.Context, teamID, userID, role string) (string, error) {
var id string

View File

@@ -8,7 +8,6 @@ import (
)
// TicketStore manages WS auth tickets in Postgres.
// v0.32.0: replaces in-memory sync.Map for cross-pod validation.
type TicketStore struct{}
func NewTicketStore() *TicketStore { return &TicketStore{} }

View File

@@ -187,7 +187,7 @@ func scanOneUser(ctx context.Context, query string, args ...interface{}) (*model
ScanJSON(sj, &u.Settings)
return &u, nil
}
// ── CS1 additions (v0.29.0) ─────────────────────────────────────────────
// ── CS1 additions ─────────────────────────────────────────────
func (s *UserStore) Exists(ctx context.Context, userID string) (bool, error) {
var exists bool
@@ -231,7 +231,7 @@ func (s *UserStore) SearchActive(ctx context.Context, excludeUserID, query strin
return results, rows.Err()
}
// ── CS2 additions (v0.29.0) ─────────────────────────────────────────────
// ── CS2 additions ─────────────────────────────────────────────
func (s *UserStore) MergeSettings(ctx context.Context, userID string, patch []byte) error {
_, err := DB.ExecContext(ctx, `
@@ -268,7 +268,7 @@ func (s *UserStore) CountAll(ctx context.Context) (int, error) {
return count, err
}
// ── CS4 additions (v0.29.0) ─────────────────────────────────────────────
// ── CS4 additions ─────────────────────────────────────────────
func (s *UserStore) ClearVaultKeys(ctx context.Context, userID string) error {
_, err := DB.ExecContext(ctx, `

View File

@@ -404,7 +404,7 @@ func nullIfEmpty(s string) interface{} {
return s
}
// ── Instances (v0.3.1) ──────────────────────
// ── Instances ──────────────────────
func (s *WorkflowStore) CreateInstance(ctx context.Context, inst *models.WorkflowInstance) error {
stageData := jsonOrEmpty(inst.StageData)
@@ -573,7 +573,7 @@ func (s *WorkflowStore) ListActiveInstances(ctx context.Context) ([]models.Workf
return result, rows.Err()
}
// ── Assignments (v0.3.1) ────────────────────
// ── Assignments ────────────────────
func (s *WorkflowStore) CreateAssignment(ctx context.Context, a *models.WorkflowAssignment) error {
reviewData := jsonOrEmpty(a.ReviewData)
@@ -661,7 +661,7 @@ func (s *WorkflowStore) ListAssignmentsByUser(ctx context.Context, userID string
return s.queryAssignments(ctx, q, args...)
}
// ── Signoffs (v0.3.4) ─────────────────────────
// ── Signoffs ─────────────────────────
func (s *WorkflowStore) CreateSignoff(ctx context.Context, so *models.WorkflowSignoff) error {
return DB.QueryRowContext(ctx, `

View File

@@ -6,7 +6,6 @@ import (
)
// RateLimitStore manages distributed rate limit counters.
// v0.32.0: replaces in-memory token bucket for cross-pod rate limiting.
type RateLimitStore interface {
// Allow checks if a request is within the rate limit.
// key is "{scope}:{identifier}" e.g. "auth:192.168.1.1".

View File

@@ -1,6 +1,6 @@
package store
// settings_cascade.go — v0.2.0
// settings_cascade.go
//
// Pure functions for three-tier settings resolution:
// global (admin) → team → user

View File

@@ -11,7 +11,7 @@ import (
"switchboard-core/store"
)
// ── ConnectionStore (v0.38.1) ────────────────────────
// ── ConnectionStore ────────────────────────
type ConnectionStore struct{}

View File

@@ -7,7 +7,7 @@ import (
"switchboard-core/models"
)
// ── DependencyStore (v0.38.2) ────────────────────────
// ── DependencyStore ────────────────────────
type DependencyStore struct{}

View File

@@ -57,7 +57,7 @@ func (s *GlobalConfigStore) GetAll(ctx context.Context) (map[string]models.JSONM
return result, rows.Err()
}
// ── OIDC state (v0.29.0-cs4) ────────────────────────────────────────────
// ── OIDC state ────────────────────────────────────────────
func (s *GlobalConfigStore) SaveOIDCState(ctx context.Context, state, nonce, redirectTo string) error {
_, err := DB.ExecContext(ctx, `
@@ -84,7 +84,7 @@ func (s *GlobalConfigStore) CleanupOIDCState(ctx context.Context) error {
return err
}
// ── CS6 additions (v0.29.0) ─────────────────────────────────────────────
// ── CS6 additions ─────────────────────────────────────────────
func (s *GlobalConfigStore) GetString(ctx context.Context, key string) (string, error) {
var val string

View File

@@ -331,7 +331,7 @@ func (s *PackageStore) scanMany(ctx context.Context, query string, args ...inter
return result, rows.Err()
}
// ── Scoped visibility (v0.30.0) ──────────────────
// ── Scoped visibility ──────────────────
func (s *PackageStore) ListVisiblePackages(ctx context.Context, userID string) ([]store.PackageRegistration, error) {
return s.scanMany(ctx, `
@@ -348,7 +348,7 @@ func (s *PackageStore) ListVisiblePackages(ctx context.Context, userID string) (
ORDER BY p.source, p.title`, userID, userID)
}
// ── Package lifecycle (v0.30.0) ──────────────────
// ── Package lifecycle ──────────────────
func (s *PackageStore) SetSchemaVersion(ctx context.Context, id string, version int) error {
result, err := DB.ExecContext(ctx,
@@ -388,7 +388,7 @@ func (s *PackageStore) SetPackageSettings(ctx context.Context, id string, settin
return nil
}
// ── Team-level settings (v0.2.0) ─────────────────
// ── Team-level settings ─────────────────
func (s *PackageStore) GetTeamSettings(ctx context.Context, pkgID, teamID string) (json.RawMessage, error) {
var settings string

View File

@@ -7,7 +7,6 @@ import (
)
// RateLimitStore manages rate limit counters in SQLite.
// v0.32.0: functional parity for single-process test coverage.
type RateLimitStore struct{}
func NewRateLimitStore() *RateLimitStore { return &RateLimitStore{} }

View File

@@ -231,7 +231,7 @@ func (s *TeamStore) IsMember(ctx context.Context, teamID, userID string) (bool,
}
// ── CS1 additions (v0.29.0) ─────────────────────────────────────────────
// ── CS1 additions ─────────────────────────────────────────────
func (s *TeamStore) Exists(ctx context.Context, teamID string) (bool, error) {
var count int
@@ -286,7 +286,7 @@ func (s *TeamStore) ListTeamAuditActions(ctx context.Context, teamID string) ([]
return actions, rows.Err()
}
// ── CS5b additions (v0.29.0) ────────────────────────────────────────────
// ── CS5b additions ────────────────────────────────────────────
func (s *TeamStore) GetFirstTeamIDForUser(ctx context.Context, userID string) (string, error) {
var teamID string
@@ -298,7 +298,7 @@ func (s *TeamStore) GetFirstTeamIDForUser(ctx context.Context, userID string) (s
return teamID, nil
}
// ── CS6 additions (v0.29.0) ────────────────────────────────────────────
// ── CS6 additions ────────────────────────────────────────────
func (s *TeamStore) AddMemberReturningID(ctx context.Context, teamID, userID, role string) (string, error) {
id := store.NewID()

View File

@@ -8,7 +8,6 @@ import (
)
// TicketStore manages WS auth tickets in SQLite.
// v0.32.0: functional parity for single-process test coverage.
type TicketStore struct{}
func NewTicketStore() *TicketStore { return &TicketStore{} }

View File

@@ -194,7 +194,7 @@ func (s *UserStore) scanOne(ctx context.Context, query string, args ...interface
ScanJSON(sj, &u.Settings)
return &u, nil
}
// ── CS1 additions (v0.29.0) ─────────────────────────────────────────────
// ── CS1 additions ─────────────────────────────────────────────
func (s *UserStore) Exists(ctx context.Context, userID string) (bool, error) {
var count int
@@ -238,7 +238,7 @@ func (s *UserStore) SearchActive(ctx context.Context, excludeUserID, query strin
return results, rows.Err()
}
// ── CS2 additions (v0.29.0) ─────────────────────────────────────────────
// ── CS2 additions ─────────────────────────────────────────────
func (s *UserStore) MergeSettings(ctx context.Context, userID string, patch []byte) error {
_, err := DB.ExecContext(ctx, `
@@ -275,7 +275,7 @@ func (s *UserStore) CountAll(ctx context.Context) (int, error) {
return count, err
}
// ── CS4 additions (v0.29.0) ─────────────────────────────────────────────
// ── CS4 additions ─────────────────────────────────────────────
func (s *UserStore) ClearVaultKeys(ctx context.Context, userID string) error {
_, err := DB.ExecContext(ctx, `

View File

@@ -418,7 +418,7 @@ func nullIfEmpty(s string) interface{} {
return s
}
// ── Instances (v0.3.1) ──────────────────────
// ── Instances ──────────────────────
func (s *WorkflowStore) CreateInstance(ctx context.Context, inst *models.WorkflowInstance) error {
inst.ID = store.NewID()
@@ -597,7 +597,7 @@ func (s *WorkflowStore) ListActiveInstances(ctx context.Context) ([]models.Workf
return result, rows.Err()
}
// ── Assignments (v0.3.1) ────────────────────
// ── Assignments ────────────────────
func (s *WorkflowStore) CreateAssignment(ctx context.Context, a *models.WorkflowAssignment) error {
a.ID = store.NewID()
@@ -690,7 +690,7 @@ func (s *WorkflowStore) ListAssignmentsByUser(ctx context.Context, userID string
return s.queryAssignments(ctx, q, args...)
}
// ── Signoffs (v0.3.4) ─────────────────────────
// ── Signoffs ─────────────────────────
func (s *WorkflowStore) CreateSignoff(ctx context.Context, so *models.WorkflowSignoff) error {
so.ID = store.NewID()

View File

@@ -3,7 +3,6 @@ package store
import "context"
// TicketStore manages short-lived, single-use WebSocket auth tickets.
// v0.32.0: replaces in-memory sync.Map for cross-pod ticket validation.
type TicketStore interface {
// Issue creates a single-use ticket for the given user.
// Returns the opaque ticket ID (128-bit hex).

View File

@@ -31,7 +31,7 @@ type WorkflowStore interface {
GetVersion(ctx context.Context, workflowID string, versionNumber int) (*models.WorkflowVersion, error)
GetLatestVersion(ctx context.Context, workflowID string) (*models.WorkflowVersion, error)
// Instances (v0.3.1+)
// Instances
CreateInstance(ctx context.Context, inst *models.WorkflowInstance) error
GetInstance(ctx context.Context, id string) (*models.WorkflowInstance, error)
GetInstanceByToken(ctx context.Context, token string) (*models.WorkflowInstance, error)
@@ -43,7 +43,7 @@ type WorkflowStore interface {
MarkInstanceStale(ctx context.Context, id string) error
ListActiveInstances(ctx context.Context) ([]models.WorkflowInstance, error)
// Assignments (v0.3.1)
// Assignments
CreateAssignment(ctx context.Context, a *models.WorkflowAssignment) error
ClaimAssignment(ctx context.Context, id string, userID string) error
UnclaimAssignment(ctx context.Context, id string) error
@@ -53,7 +53,7 @@ type WorkflowStore interface {
ListAssignmentsByInstance(ctx context.Context, instanceID string) ([]models.WorkflowAssignment, error)
ListAssignmentsByUser(ctx context.Context, userID string, status string) ([]models.WorkflowAssignment, error)
// Signoffs (v0.3.4)
// Signoffs
CreateSignoff(ctx context.Context, s *models.WorkflowSignoff) error
ListSignoffs(ctx context.Context, instanceID, stage string) ([]models.WorkflowSignoff, error)
CountSignoffs(ctx context.Context, instanceID, stage, decision string) (int, error)

View File

@@ -1,6 +1,5 @@
// Package triggers — engine.go
//
// v0.2.2: Core trigger engine. Manages extension-declared triggers
// (event bus subscriptions + webhook receivers) and user-created
// scheduled tasks (cron). All three converge to sandbox.Runner.CallEntryPoint.
package triggers

View File

@@ -1,8 +1,6 @@
// Package webhook delivers HTTP POST notifications on task/workflow completion.
//
// v0.27.3: Retry (3 attempts, exponential backoff), HMAC-SHA256 signature,
// 10s timeout per attempt.
// v0.28.0: Added RunID, TokensUsed fields (D1 audit fix).
package webhook
import (

View File

@@ -158,7 +158,7 @@ func (e *Engine) advanceInternal(ctx context.Context, instanceID string, stageDa
}
currentStage := stages[currentOrdinal]
// ── Validation gate (v0.3.4) ──────────────
// ── Validation gate ──────────────
sc := ParseStageConfig(currentStage.StageConfig)
if sc.Validation != nil && sc.Validation.RequiredApprovals > 0 {
// Check for rejections first
@@ -296,7 +296,7 @@ func (e *Engine) Cancel(ctx context.Context, instanceID string, userID string) e
return nil
}
// ── Public Entry (v0.3.3) ───────────────────
// ── Public Entry ───────────────────
// StartPublic creates an instance for a public_link workflow without authentication.
// The caller is identified as "public:<uuid>". Returns the instance including its entry_token.
@@ -358,7 +358,7 @@ func (e *Engine) AdvancePublic(ctx context.Context, entryToken string, stageData
return e.advanceInternal(ctx, inst.ID, stageData, inst.StartedBy, 0)
}
// ── Signoffs (v0.3.4) ─────────────────────────
// ── Signoffs ─────────────────────────
// SubmitSignoff records a user's approval or rejection at the current stage boundary.
func (e *Engine) SubmitSignoff(ctx context.Context, instanceID, userID, decision, comment string) (*models.WorkflowSignoff, error) {

View File

@@ -1,676 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Switchboard Core — Dev Gallery</title>
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/sw-primitives.css">
<link rel="stylesheet" href="css/sw-shell.css">
<style>
body { overflow: auto; padding: 0; }
.dev-root { height: 100vh; display: flex; flex-direction: column; }
.dev-nav { display: flex; gap: 0; border-bottom: 1px solid var(--border); background: var(--bg-surface); flex-shrink: 0; padding: 0 1rem; }
.dev-nav button {
padding: 0.6rem 1.2rem; background: none; border: none; border-bottom: 2px solid transparent;
color: var(--text-2); font-family: var(--font); font-size: 0.85rem; font-weight: 500; cursor: pointer;
}
.dev-nav button:hover { color: var(--text); }
.dev-nav button.active { color: var(--accent); border-bottom-color: var(--accent); }
.dev-page { flex: 1; overflow: auto; }
.gallery { max-width: 800px; margin: 0 auto; padding: 2rem; }
.gallery h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
.gallery .subtitle { color: var(--text-2); margin-bottom: 2rem; font-size: 0.9rem; }
.section { margin-bottom: 2.5rem; }
.section h2 {
font-size: 0.85rem; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.5px; color: var(--text-3); margin-bottom: 0.75rem;
padding-bottom: 0.5rem; border-bottom: 1px solid var(--border);
}
.row { display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap; margin-bottom: 0.75rem; }
.col { display: flex; flex-direction: column; gap: 0.75rem; }
.spacer { height: 0.5rem; }
.shell-demo { height: 100%; }
.shell-demo .sw-shell { height: 100%; }
</style>
</head>
<body>
<div id="app"></div>
<script type="module">
(async () => {
try {
const { h, render } = await import('./js/sw/vendor/preact.module.js');
const hooksModule = await import('./js/sw/vendor/hooks.module.js?v=2');
const { default: htm } = await import('./js/sw/vendor/htm.module.js');
const html = htm.bind(h);
const { useState, useEffect, useRef, useMemo, useCallback } = hooksModule;
window.preact = { h, render };
window.hooks = hooksModule;
window.html = html;
const { Button } = await import('./js/sw/primitives/button.js');
const { Spinner } = await import('./js/sw/primitives/spinner.js');
const { Avatar } = await import('./js/sw/primitives/avatar.js');
const { FormField } = await import('./js/sw/primitives/form-field.js');
const { Tooltip } = await import('./js/sw/primitives/tooltip.js');
const { Banner } = await import('./js/sw/primitives/banner.js');
const { Dialog } = await import('./js/sw/primitives/dialog.js');
const { toast, ToastContainer } = await import('./js/sw/primitives/toast.js');
const { Menu } = await import('./js/sw/primitives/menu.js');
const { Drawer } = await import('./js/sw/primitives/drawer.js');
const { Tabs } = await import('./js/sw/primitives/tabs.js');
const { Dropdown } = await import('./js/sw/primitives/dropdown.js');
const { AppShell } = await import('./js/sw/shell/app-shell.js');
const { UserMenu } = await import('./js/sw/shell/user-menu.js');
const { SurfaceViewport } = await import('./js/sw/shell/surface-viewport.js');
const { DialogStack } = await import('./js/sw/shell/dialog-stack.js');
const { confirm } = await import('./js/sw/primitives/confirm.js');
const { prompt } = await import('./js/sw/primitives/prompt.js');
// ── SDK Boot ────────────────────────────
const { boot } = await import('./js/sw/sdk/index.js');
await boot().catch(e => console.warn('SDK boot (no backend):', e));
const sw = window.sw;
// ── SDK Demo Page ───────────────────────
function SDKPage() {
const [authState, setAuthState] = useState(null);
const [apiResult, setApiResult] = useState(null);
const [permInput, setPermInput] = useState('model.use');
const [canResult, setCanResult] = useState(null);
const [eventLog, setEventLog] = useState([]);
const [emitLabel, setEmitLabel] = useState('test.event');
const logRef = useRef([]);
// Subscribe to all events for live log
useEffect(() => {
const unsub = sw.on('*', (payload, meta) => {
const entry = { ts: new Date().toLocaleTimeString(), label: meta.event, local: meta.local };
logRef.current = [entry, ...logRef.current].slice(0, 20);
setEventLog([...logRef.current]);
});
return unsub;
}, []);
function refreshAuth() {
setAuthState({
isAuthenticated: sw.auth.isAuthenticated,
user: sw.auth.user,
permissions: [...sw.auth.permissions],
teams: sw.auth.teams,
policies: sw.auth.policies,
});
}
useEffect(() => { refreshAuth(); }, []);
async function testApi() {
try {
const res = await sw.api.channels.list();
setApiResult({ ok: true, data: res });
} catch (e) {
setApiResult({ ok: false, error: e.message });
}
}
function testCan() {
setCanResult(sw.can(permInput));
}
const monoStyle = 'font-family: var(--font-mono, monospace); font-size: 0.8rem; background: var(--bg-1); padding: 0.75rem; border-radius: 6px; overflow: auto; max-height: 200px; white-space: pre-wrap; word-break: break-all; color: var(--text-2);';
return html`
<div class="gallery">
<h1>SDK Demo</h1>
<p class="subtitle">Layer 1 — SDK modules (v0.37.3)</p>
<!-- Auth State -->
<div class="section">
<h2>sw.auth</h2>
<div class="row" style="margin-bottom:0.5rem">
<${Button} variant="secondary" size="sm" onClick=${refreshAuth}>Refresh<//>
</div>
<div style=${monoStyle}>${JSON.stringify(authState, null, 2)}</div>
</div>
<!-- API Test -->
<div class="section">
<h2>sw.api</h2>
<div class="row" style="margin-bottom:0.5rem">
<${Button} variant="secondary" size="sm" onClick=${testApi}>sw.api.channels.list()<//>
</div>
${apiResult && html`
<div style=${monoStyle}>
${apiResult.ok
? JSON.stringify(apiResult.data, null, 2)
: 'Error: ' + apiResult.error}
</div>
`}
</div>
<!-- RBAC Test -->
<div class="section">
<h2>sw.can()</h2>
<div class="row">
<input class="sw-input" style="width:180px" value=${permInput}
onInput=${e => setPermInput(e.target.value)} />
<${Button} variant="secondary" size="sm" onClick=${testCan}>Check<//>
${canResult !== null && html`
<span style="font-weight:600; color: ${canResult ? 'var(--success)' : 'var(--danger)'}">
${canResult ? 'ALLOWED' : 'DENIED'}
</span>
`}
</div>
<div class="row" style="margin-top:0.5rem">
<span style="color:var(--text-3); font-size:0.8rem">
sw.isAdmin: ${String(sw.isAdmin)}
</span>
</div>
</div>
<!-- Theme -->
<div class="section">
<h2>sw.theme</h2>
<div class="row">
<span style="color:var(--text-2); font-size:0.85rem">
mode: ${sw.theme.mode} · resolved: ${sw.theme.current}
</span>
</div>
<div class="row">
<${Button} variant="secondary" size="sm" onClick=${() => sw.theme.set('light')}>Light<//>
<${Button} variant="secondary" size="sm" onClick=${() => sw.theme.set('dark')}>Dark<//>
<${Button} variant="secondary" size="sm" onClick=${() => sw.theme.set('system')}>System<//>
</div>
</div>
<!-- Events -->
<div class="section">
<h2>sw.on / sw.emit</h2>
<div class="row" style="margin-bottom:0.5rem">
<input class="sw-input" style="width:180px" value=${emitLabel}
onInput=${e => setEmitLabel(e.target.value)} />
<${Button} variant="secondary" size="sm"
onClick=${() => sw.emit(emitLabel, { demo: true }, { localOnly: true })}>Emit<//>
</div>
<div style=${monoStyle + ' max-height:160px;'}>
${eventLog.length === 0 ? '(listening for events...)' :
eventLog.map(e => `${e.ts} ${e.local ? 'L' : 'R'} ${e.label}`).join('\n')}
</div>
</div>
<!-- Pipe -->
<div class="section">
<h2>sw.pipe</h2>
<div style=${monoStyle}>${JSON.stringify(sw.pipe.list(), null, 2)}</div>
</div>
<!-- Namespaces -->
<div class="section">
<h2>sw.api namespaces</h2>
<div style=${monoStyle}>${Object.keys(sw.api).filter(k => typeof sw.api[k] === 'object').join(', ')}</div>
</div>
</div>
`;
}
// ── Shell Demo Page ──────────────────────
function ShellPage() {
const [confirmResult, setConfirmResult] = useState(null);
const [promptResult, setPromptResult] = useState(null);
return html`
<div class="gallery">
<h1>Shell Demo</h1>
<p class="subtitle">Layer 2 — Application shell (v0.37.4)</p>
<!-- UserMenu -->
<div class="section">
<h2>UserMenu</h2>
<div class="row">
<${UserMenu} />
<${UserMenu} placement="down-left" />
</div>
<p style="color:var(--text-3); font-size:0.8rem; margin-top:0.5rem">
RBAC-gated flyout. Without backend, shows all items for demo.
</p>
</div>
<!-- Imperative Confirm -->
<div class="section">
<h2>sw.confirm()</h2>
<div class="row">
<${Button} variant="secondary" onClick=${async () => {
const ok = await sw.confirm('Delete this item?', { destructive: true });
setConfirmResult(ok);
}}>Confirm (destructive)<//>
<${Button} variant="secondary" onClick=${async () => {
const ok = await sw.confirm('Proceed with this action?');
setConfirmResult(ok);
}}>Confirm (normal)<//>
${confirmResult !== null && html`
<span style="font-weight:600; color: ${confirmResult ? 'var(--success)' : 'var(--danger)'}">
Result: ${String(confirmResult)}
</span>
`}
</div>
</div>
<!-- Imperative Prompt -->
<div class="section">
<h2>sw.prompt()</h2>
<div class="row">
<${Button} variant="secondary" onClick=${async () => {
const val = await sw.prompt('Enter a name:', { defaultValue: 'untitled' });
setPromptResult(val);
}}>Prompt<//>
${promptResult !== null && html`
<span style="font-weight:600; color:var(--text-2)">
Result: ${JSON.stringify(promptResult)}
</span>
`}
</div>
</div>
<!-- Surface Viewport -->
<div class="section">
<h2>SurfaceViewport</h2>
<p style="color:var(--text-3); font-size:0.8rem; margin-bottom:0.5rem">
UserMenu is placed by the surface (top-right), not the shell.
</p>
<div style="border:1px dashed var(--border); border-radius:var(--radius, 6px); height:200px; position:relative; overflow:hidden;">
<${SurfaceViewport} surface=${html`
<div style="padding:1rem; color:var(--text-2); height:100%;">
<p>Mock surface content. The shell provides the viewport; the surface owns everything inside.</p>
<div style="position:absolute; top:0.5rem; right:0.5rem;">
<${UserMenu} placement="down-left" />
</div>
</div>
`} />
</div>
</div>
<!-- App component tree -->
<div class="section">
<h2>App Component Tree</h2>
<div style="font-family:var(--font-mono, monospace); font-size:0.8rem; color:var(--text-2); background:var(--bg-1); padding:0.75rem; border-radius:6px; white-space:pre;">${
`<App>
<AppShell banner message footer>
<SurfaceViewport surface={...} />
</AppShell>
<ToastContainer />
<DialogStack />
</App>`
}</div>
</div>
</div>
`;
}
// ── Surfaces Demo Page ────────────────────
function SurfacesPage() {
return html`
<div class="gallery">
<h1>Surfaces Demo</h1>
<p class="subtitle">v0.37.5 — Login + Settings surfaces</p>
<div class="section">
<h2>Login Surface</h2>
<p style="color:var(--text-3); font-size:0.85rem; margin-bottom:0.75rem">
The login surface is a standalone page (not embedded in AppShell).
It renders at /login with its own layout: Hero panel + Auth panel.
</p>
<div style="font-family:var(--font-mono, monospace); font-size:0.8rem; color:var(--text-2); background:var(--bg-1); padding:0.75rem; border-radius:6px; white-space:pre;">${
`<LoginSurface>
<Hero />
<AuthPanel>
authMode === 'builtin':
<LoginForm /> — sw.auth.login()
<RegisterForm /> — sw.api.auth.register()
authMode === 'oidc' | 'mtls':
<SSOPanel />
</AuthPanel>
</LoginSurface>`
}</div>
<p style="color:var(--text-3); font-size:0.8rem; margin-top:0.5rem">
Files: src/js/sw/surfaces/login/
</p>
</div>
<div class="section">
<h2>Settings Surface</h2>
<p style="color:var(--text-3); font-size:0.85rem; margin-bottom:0.75rem">
Full Preact surface with left nav + section routing.
Core sections (General, Appearance, Profile, Teams, Models, Providers,
Personas, Roles, Usage) are native Preact components.
Deferred sections use BridgeSection wrappers.
</p>
<div style="font-family:var(--font-mono, monospace); font-size:0.8rem; color:var(--text-2); background:var(--bg-1); padding:0.75rem; border-radius:6px; white-space:pre;">${
`<SettingsSurface>
<Topbar />
<Nav>
General, Appearance, Models, Personas, Profile,
Teams, Workflows, Tasks, Git Keys, Data & Privacy
[BYOK] Providers, Roles, Usage
</Nav>
<Content>
section === 'general' → <GeneralSection />
section === 'appearance' → <AppearanceSection />
section === 'profile' → <ProfileSection />
section === 'teams' → <TeamsSection />
section === 'models' → <ModelsSection />
section === 'providers' → <ProvidersSection />
section === 'personas' → <PersonasSection />
section === 'roles' → <RolesSection />
section === 'usage' → <UsageSection />
section === 'tasks' etc → <BridgeSection /> (old JS)
</Content>
</SettingsSurface>`
}</div>
<p style="color:var(--text-3); font-size:0.8rem; margin-top:0.5rem">
Files: src/js/sw/surfaces/settings/
</p>
</div>
</div>
`;
}
function PrimitivesPage() {
const [dialogOpen, setDialogOpen] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const [activeTab, setActiveTab] = useState('tab1');
const [dropVal, setDropVal] = useState('opt2');
const [loading, setLoading] = useState(false);
const menuAnchor = useRef(null);
return html`
<div class="gallery">
<h1>Primitives Gallery</h1>
<p class="subtitle">Layer 0 — Preact + htm components (v0.37.2)</p>
<!-- Buttons -->
<div class="section">
<h2>Button</h2>
<div class="row">
<${Button} variant="primary">Primary<//>
<${Button} variant="secondary">Secondary<//>
<${Button} variant="danger">Danger<//>
<${Button} variant="ghost">Ghost<//>
</div>
<div class="row">
<${Button} size="sm">Small<//>
<${Button} size="md">Medium<//>
<${Button} size="lg">Large<//>
</div>
<div class="row">
<${Button} disabled>Disabled<//>
<${Button} loading=${true}>Loading<//>
</div>
</div>
<!-- Spinner -->
<div class="section">
<h2>Spinner</h2>
<div class="row">
<${Spinner} size="sm" />
<${Spinner} size="md" />
<${Spinner} size="lg" />
</div>
</div>
<!-- Avatar -->
<div class="section">
<h2>Avatar</h2>
<div class="row">
<${Avatar} name="Jane Doe" size="sm" />
<${Avatar} name="Jane Doe" size="md" />
<${Avatar} name="Jane Doe" size="lg" />
<${Avatar} name="X" size="md" />
<${Avatar} src="https://i.pravatar.cc/48" name="Photo" size="lg" />
</div>
</div>
<!-- FormField -->
<div class="section">
<h2>FormField</h2>
<div class="col" style="max-width:320px">
<${FormField} label="Username" required>
<input class="sw-input" placeholder="Enter username" />
<//>
<${FormField} label="Email" error="Invalid email address">
<input class="sw-input" value="bad@" />
<//>
<${FormField} label="Bio" hint="Optional, max 200 chars">
<textarea class="sw-input" rows="2" placeholder="Tell us about yourself" />
<//>
</div>
</div>
<!-- Tooltip -->
<div class="section">
<h2>Tooltip</h2>
<div class="row">
<${Tooltip} content="Top tooltip">
<${Button} variant="secondary">Hover me (top)<//>
<//>
<${Tooltip} content="Bottom tooltip" position="bottom">
<${Button} variant="secondary">Bottom<//>
<//>
<${Tooltip} content="Right tooltip" position="right">
<${Button} variant="secondary">Right<//>
<//>
</div>
</div>
<!-- Banner -->
<div class="section">
<h2>Banner</h2>
<div class="col">
<${Banner} text="Informational banner message" variant="info" />
<${Banner} text="Warning: maintenance scheduled" variant="warn" dismissible />
<${Banner} text="Error: service unavailable" variant="error" />
<${Banner} text="Successfully deployed v0.37.2" variant="success" dismissible />
</div>
</div>
<!-- Tabs -->
<div class="section">
<h2>Tabs</h2>
<${Tabs}
tabs=${[
{ label: 'General', value: 'tab1' },
{ label: 'Appearance', value: 'tab2' },
{ label: 'Notifications', value: 'tab3' },
{ label: 'Disabled', value: 'tab4', disabled: true },
]}
active=${activeTab}
onChange=${setActiveTab}
/>
<div style="padding: 1rem 0; color: var(--text-2); font-size: 0.9rem">
Active tab: ${activeTab}
</div>
</div>
<!-- Dropdown -->
<div class="section">
<h2>Dropdown</h2>
<div class="row">
<div style="width:220px">
<${Dropdown}
options=${[
{ label: 'Claude Sonnet', value: 'opt1' },
{ label: 'Claude Opus', value: 'opt2' },
{ label: 'Claude Haiku', value: 'opt3' },
{ label: 'GPT-4o', value: 'opt4' },
{ label: 'Disabled', value: 'opt5', disabled: true },
]}
value=${dropVal}
onChange=${setDropVal}
searchable
/>
</div>
</div>
</div>
<!-- Menu -->
<div class="section">
<h2>Menu</h2>
<div class="row">
<${Button} ref=${menuAnchor} variant="secondary"
onClick=${() => setMenuOpen(!menuOpen)}>
Open Menu
<//>
<${Menu}
open=${menuOpen}
anchor=${menuAnchor.current}
items=${[
{ label: 'Settings', action: 'settings', icon: '\u2699' },
{ label: 'Profile', action: 'profile', icon: '\ud83d\udc64' },
{ divider: true },
{ label: 'Submenu', children: [
{ label: 'Option A', action: 'a' },
{ label: 'Option B', action: 'b' },
]},
{ divider: true },
{ label: 'Disabled', action: 'x', disabled: true },
{ label: 'Sign Out', action: 'logout' },
]}
onSelect=${(action) => { toast.info('Selected: ' + action); }}
onClose=${() => setMenuOpen(false)}
/>
</div>
</div>
<!-- Dialog -->
<div class="section">
<h2>Dialog</h2>
<div class="row">
<${Button} onClick=${() => setDialogOpen(true)}>Open Dialog<//>
</div>
<${Dialog}
open=${dialogOpen}
title="Example Dialog"
onClose=${() => setDialogOpen(false)}
actions=${[
{ label: 'Cancel', variant: 'secondary', onClick: () => setDialogOpen(false) },
{ label: 'Confirm', variant: 'primary', onClick: () => { setDialogOpen(false); toast.success('Confirmed!'); } },
]}
>
<p>This is a modal dialog with focus trapping, Escape to close, and backdrop click to dismiss.</p>
<//>
</div>
<!-- Drawer -->
<div class="section">
<h2>Drawer</h2>
<div class="row">
<${Button} variant="secondary" onClick=${() => setDrawerOpen(true)}>Open Drawer<//>
</div>
<${Drawer}
open=${drawerOpen}
title="Settings"
onClose=${() => setDrawerOpen(false)}
>
<p style="color: var(--text-2)">Slide-in panel for settings, admin sections, etc.</p>
<div class="spacer" />
<${FormField} label="Display Name">
<input class="sw-input" value="Jane Doe" />
<//>
<//>
</div>
<!-- Toast -->
<div class="section">
<h2>Toast</h2>
<div class="row">
<${Button} variant="secondary" onClick=${() => toast.success('Operation successful')}>Success<//>
<${Button} variant="secondary" onClick=${() => toast.error('Something went wrong')}>Error<//>
<${Button} variant="secondary" onClick=${() => toast.warn('Check your settings')}>Warning<//>
<${Button} variant="secondary" onClick=${() => toast.info('New message received')}>Info<//>
</div>
</div>
</div>
`;
}
const VARIANTS = ['info', 'warn', 'error', 'success'];
function ShellControls({ bannerOn, setBannerOn, bannerVariant, setBannerVariant, bannerText, setBannerText,
msgOn, setMsgOn, msgVariant, setMsgVariant, msgText, setMsgText,
footerOn, setFooterOn }) {
return html`
<div style="display:flex; align-items:center; gap:0.75rem; padding:0.4rem 0; flex-wrap:wrap;">
<${Button} variant=${bannerOn ? 'primary' : 'secondary'} size="sm"
onClick=${() => setBannerOn(!bannerOn)}>Banner<//>
${bannerOn && html`
<${Dropdown} options=${VARIANTS.map(v => ({ label: v, value: v }))}
value=${bannerVariant} onChange=${setBannerVariant} />
<input class="sw-input" style="width:140px" placeholder="text..."
value=${bannerText} onInput=${e => setBannerText(e.target.value)} />
`}
<${Button} variant=${msgOn ? 'primary' : 'secondary'} size="sm"
onClick=${() => setMsgOn(!msgOn)}>Message<//>
${msgOn && html`
<${Dropdown} options=${VARIANTS.map(v => ({ label: v, value: v }))}
value=${msgVariant} onChange=${setMsgVariant} />
<input class="sw-input" style="width:140px" placeholder="text..."
value=${msgText} onInput=${e => setMsgText(e.target.value)} />
`}
<${Button} variant=${footerOn ? 'primary' : 'secondary'} size="sm"
onClick=${() => setFooterOn(!footerOn)}>Footer<//>
</div>
`;
}
function DevRoot() {
const [page, setPage] = useState('primitives');
const [bannerOn, setBannerOn] = useState(false);
const [msgOn, setMsgOn] = useState(false);
const [footerOn, setFooterOn] = useState(false);
const [bannerVariant, setBannerVariant] = useState('info');
const [msgVariant, setMsgVariant] = useState('info');
const [bannerText, setBannerText] = useState('');
const [msgText, setMsgText] = useState('');
const banner = bannerOn ? { text: bannerText || 'Banner', variant: bannerVariant } : null;
const msg = msgOn ? { text: msgText || 'Message', variant: msgVariant } : null;
return html`
<${AppShell}
banner=${banner}
message=${msg}
footer=${footerOn ? html`Switchboard Core · Layer 1 Shell` : null}
>
<div class="dev-root">
<nav class="dev-nav">
<button class=${page === 'primitives' ? 'active' : ''} onClick=${() => setPage('primitives')}>Primitives</button>
<button class=${page === 'shell' ? 'active' : ''} onClick=${() => setPage('shell')}>Shell</button>
<button class=${page === 'sdk' ? 'active' : ''} onClick=${() => setPage('sdk')}>SDK</button>
<button class=${page === 'surfaces' ? 'active' : ''} onClick=${() => setPage('surfaces')}>Surfaces</button>
<div style="flex:1" />
<${ShellControls} ...${{
bannerOn, setBannerOn, bannerVariant, setBannerVariant, bannerText, setBannerText,
msgOn, setMsgOn, msgVariant, setMsgVariant, msgText, setMsgText,
footerOn, setFooterOn
}} />
</nav>
<div class="dev-page">
${page === 'primitives' && html`<${PrimitivesPage} />`}
${page === 'shell' && html`<${ShellPage} />`}
${page === 'sdk' && html`<${SDKPage} />`}
${page === 'surfaces' && html`<${SurfacesPage} />`}
</div>
</div>
<//>
<${ToastContainer} />
<${DialogStack} />
`;
}
render(html`<${DevRoot} />`, document.getElementById('app'));
} catch(e) { console.error('DEV:', e); }
})();
</script>
</body>
</html>

View File

@@ -243,7 +243,6 @@ describe('init() profile gate', () => {
});
// ── Boot-time 401 redirect suppression ──────
// v0.37.14: Stale cookie blank-login-page fix.
// When the login page boots the SDK with stale localStorage tokens,
// the REST client's 401 handler must NOT redirect to /login (we're
// already there). boot() handles 401 gracefully on its own.

Some files were not shown because too many files have changed in this diff Show More