feat: milestone 5b — frontend bundle host + VerstakPluginAPI stub

- Bundle contract: window.VerstakPluginRegister(id, {components: {...}})
- PluginBundleHost.svelte: loads bundle via GetPluginAssetContent, mounts components
- VerstakPluginAPI.js: restricted API (capabilities, events, settings, commands — all stub)
- ViewContainer: PluginBundleHost replaces placeholder when frontend bundle exists
- PluginManager: settings panel via PluginBundleHost (removed hardcoded form)
- Backend: GetPluginFrontendInfo, GetPluginAssetContent with path security
- Security: reject absolute paths, path traversal, escape from plugin root
- Error boundary: bundle load/execute/mount errors show fallback, not crash
- Tests: 11 backend tests (asset API), frontend bundle checks in smoke
- Docs: bundle contract, VerstakPluginAPI, security constraints
This commit is contained in:
2026-06-17 17:39:50 +08:00
parent 1dff97a9c0
commit 05ef1449bc
13 changed files with 887 additions and 30 deletions
+66
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/verstak/verstak-desktop/internal/core/capability"
"github.com/verstak/verstak-desktop/internal/core/contribution"
@@ -558,6 +559,71 @@ func runContributionsTest(root string) {
}
}
// ── Step 2b: Frontend bundle verification ──
fmt.Printf("\n[frontend bundle verification]\n")
frontendOk := true
// 2b.1 — Check manifest declares frontend.entry
if target.Manifest.Frontend == nil {
fmt.Printf(" ❌ manifest.frontend is nil — plugin has no frontend config\n")
frontendOk = false
} else {
expectedEntry := "frontend/dist/index.js"
if target.Manifest.Frontend.Entry != expectedEntry {
fmt.Printf(" ❌ frontend.entry: expected %q, got %q\n", expectedEntry, target.Manifest.Frontend.Entry)
frontendOk = false
} else {
fmt.Printf(" ✅ frontend.entry: %s\n", target.Manifest.Frontend.Entry)
}
// 2b.2 — Resolve entry path and check file exists on disk
resolvedEntry := filepath.Join(target.RootPath, target.Manifest.Frontend.Entry)
if _, err := os.Stat(resolvedEntry); os.IsNotExist(err) {
fmt.Printf(" ❌ frontend bundle NOT FOUND at resolved path: %s\n", resolvedEntry)
frontendOk = false
} else if err != nil {
fmt.Printf(" ❌ frontend bundle stat error: %v\n", err)
frontendOk = false
} else {
fmt.Printf(" ✅ frontend bundle exists: %s\n", resolvedEntry)
// 2b.3 — Read file and check for "VerstakPluginRegister"
data, err := os.ReadFile(resolvedEntry)
if err != nil {
fmt.Printf(" ❌ failed to read frontend bundle: %v\n", err)
frontendOk = false
} else {
content := string(data)
if strings.Contains(content, "VerstakPluginRegister") {
fmt.Printf(" ✅ bundle contains VerstakPluginRegister contract\n")
} else {
fmt.Printf(" ❌ bundle MISSING VerstakPluginRegister contract\n")
frontendOk = false
}
// 2b.4 — Check for expected component names
expectedComponents := []string{"DiagnosticsPanel", "PlatformTestSettings"}
for _, comp := range expectedComponents {
if strings.Contains(content, comp) {
fmt.Printf(" ✅ component registered: %s\n", comp)
} else {
fmt.Printf(" ❌ component MISSING from bundle: %s\n", comp)
frontendOk = false
}
}
}
}
}
if frontendOk {
fmt.Printf(" ✅ frontend bundle checks passed\n")
} else {
fmt.Printf(" ❌ frontend bundle checks FAILED\n")
exitCode = 1
return
}
// ── Step 3: Register capabilities (simulates main.go + ReloadPlugins) ──
reg := capability.NewRegistry()
coreCaps := []string{