fix: register core capabilities + plugin lifecycle before discovery
Core fix: core capabilities (plugin-manager, capability-registry, contribution-registry, permissions, events) now registered BEFORE plugin discovery, so plugins can resolve required capabilities at load time. Changes: - main.go: register 5 core capabilities before DiscoverPlugins(); add plugin lifecycle (register caps, resolve required/optional, set status LOADED/DEGRADED/MISSING_REQUIRED, register contributions) - api/app.go: ReloadPlugins now re-registers core + plugin capabilities - capability/registry.go: add UnregisterAll() for reload - cmd/smoke-platform/main.go: verify core caps, required resolution, degraded status, total caps >= 7 - PluginCard.svelte: remove ✗ on optional-missing, show degraded info - PluginManager.svelte: preserved (counter shows core+plugin caps) Rule: optional missing => DEGRADED (not FAILED). Required missing => MISSING_REQUIRED_CAPABILITY. Verified: smoke-platform ✅, test.sh (10/10) ✅, check.sh ✅
This commit is contained in:
parent
1c75389535
commit
dd3a5f8ff5
|
|
@ -148,20 +148,98 @@ func main() {
|
||||||
fmt.Printf(" ℹ️ contributes: none\n")
|
fmt.Printf(" ℹ️ contributes: none\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 7. Capability registration ──
|
// ── 7. Capability registration (core + plugin) ──
|
||||||
fmt.Printf("\n[capability registration]\n")
|
fmt.Printf("\n[capability registration]\n")
|
||||||
reg := capability.NewRegistry()
|
reg := capability.NewRegistry()
|
||||||
|
|
||||||
|
// Register core capabilities (same list as main.go)
|
||||||
|
coreCaps := []string{
|
||||||
|
"verstak/core/plugin-manager/v1",
|
||||||
|
"verstak/core/capability-registry/v1",
|
||||||
|
"verstak/core/contribution-registry/v1",
|
||||||
|
"verstak/core/permissions/v1",
|
||||||
|
"verstak/core/events/v1",
|
||||||
|
}
|
||||||
|
if err := reg.Register("verstak-desktop", coreCaps); err != nil {
|
||||||
|
fmt.Printf(" ❌ register core capabilities: %v\n", err)
|
||||||
|
allGood = false
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✅ registered %d core capabilities\n", len(coreCaps))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register plugin capabilities
|
||||||
for _, p := range m.Provides {
|
for _, p := range m.Provides {
|
||||||
err := reg.Register(target.Manifest.ID, []string{p})
|
if err := reg.Register(m.ID, []string{p}); err != nil {
|
||||||
if err != nil {
|
|
||||||
fmt.Printf(" ❌ register capability %s: %v\n", p, err)
|
fmt.Printf(" ❌ register capability %s: %v\n", p, err)
|
||||||
allGood = false
|
allGood = false
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" ✅ registered capability: %s\n", p)
|
fmt.Printf(" ✅ registered plugin capability: %s\n", p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 8. Summary ──
|
// ── 8. Verify core capabilities present ──
|
||||||
|
fmt.Printf("\n[core capability verification]\n")
|
||||||
|
for _, capName := range coreCaps {
|
||||||
|
if reg.Has(capName) {
|
||||||
|
fmt.Printf(" ✅ core capability present: %s\n", capName)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ❌ core capability MISSING: %s\n", capName)
|
||||||
|
allGood = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 9. Verify required capabilities resolved ──
|
||||||
|
fmt.Printf("\n[required capability resolution]\n")
|
||||||
|
missingRequired := reg.CheckRequired(m.Requires)
|
||||||
|
if len(missingRequired) > 0 {
|
||||||
|
for _, miss := range missingRequired {
|
||||||
|
fmt.Printf(" ❌ MISSING required: %s\n", miss)
|
||||||
|
}
|
||||||
|
allGood = false
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✅ all required capabilities resolved\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 10. Check optional capabilities ──
|
||||||
|
fmt.Printf("\n[optional capability resolution]\n")
|
||||||
|
missingOptional := reg.CheckRequired(m.OptionalRequires)
|
||||||
|
if len(missingOptional) > 0 {
|
||||||
|
for _, miss := range missingOptional {
|
||||||
|
fmt.Printf(" ⚠️ missing optional (degraded): %s\n", miss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 11. Determine expected status ──
|
||||||
|
fmt.Printf("\n[plugin status]\n")
|
||||||
|
expectedStatus := "loaded"
|
||||||
|
if len(missingOptional) > 0 {
|
||||||
|
expectedStatus = "degraded"
|
||||||
|
}
|
||||||
|
if len(missingRequired) > 0 {
|
||||||
|
expectedStatus = "missing-required-capability"
|
||||||
|
}
|
||||||
|
fmt.Printf(" ℹ️ expected status: %s\n", expectedStatus)
|
||||||
|
if expectedStatus == "degraded" {
|
||||||
|
fmt.Printf(" ✅ degraded is correct (optional capabilities missing, required OK)\n")
|
||||||
|
} else if expectedStatus == "loaded" {
|
||||||
|
fmt.Printf(" ✅ loaded is correct (all capabilities resolved)\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ❌ unexpected: required capabilities should be resolved\n")
|
||||||
|
allGood = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 12. Total capability count ──
|
||||||
|
fmt.Printf("\n[capability count]\n")
|
||||||
|
totalCaps := len(reg.List())
|
||||||
|
fmt.Printf(" total capabilities: %d\n", totalCaps)
|
||||||
|
if totalCaps >= 7 {
|
||||||
|
fmt.Printf(" ✅ total capabilities >= 7 (%d)\n", totalCaps)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ❌ total capabilities < 7 (got %d, expected >= 7)\n", totalCaps)
|
||||||
|
allGood = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 13. Summary ──
|
||||||
fmt.Printf("\n=== summary ===\n")
|
fmt.Printf("\n=== summary ===\n")
|
||||||
if allGood {
|
if allGood {
|
||||||
fmt.Printf("✅ smoke-platform passed\n")
|
fmt.Printf("✅ smoke-platform passed\n")
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -108,7 +109,55 @@ func (a *App) ReloadPlugins() (int, string) {
|
||||||
|
|
||||||
log.Printf("[api] ReloadPlugins: scanning dirs: %v", discoveryDirs)
|
log.Printf("[api] ReloadPlugins: scanning dirs: %v", discoveryDirs)
|
||||||
|
|
||||||
|
// Unregister all non-core capabilities
|
||||||
|
a.capRegistry.UnregisterAll()
|
||||||
|
|
||||||
|
// Re-register core capabilities
|
||||||
|
coreCaps := []string{
|
||||||
|
"verstak/core/plugin-manager/v1",
|
||||||
|
"verstak/core/capability-registry/v1",
|
||||||
|
"verstak/core/contribution-registry/v1",
|
||||||
|
"verstak/core/permissions/v1",
|
||||||
|
"verstak/core/events/v1",
|
||||||
|
}
|
||||||
|
if err := a.capRegistry.Register("verstak-desktop", coreCaps); err != nil {
|
||||||
|
log.Printf("[api] ReloadPlugins: failed to re-register core capabilities: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
plugins, errs := plugin.DiscoverPlugins(discoveryDirs)
|
plugins, errs := plugin.DiscoverPlugins(discoveryDirs)
|
||||||
|
|
||||||
|
// Plugin lifecycle: register capabilities + contributions
|
||||||
|
for i := range plugins {
|
||||||
|
p := &plugins[i]
|
||||||
|
|
||||||
|
if len(p.Manifest.Provides) > 0 {
|
||||||
|
if err := a.capRegistry.Register(p.Manifest.ID, p.Manifest.Provides); err != nil {
|
||||||
|
log.Printf("[plugin] %s: capability registration failed: %v", p.Manifest.ID, err)
|
||||||
|
p.Status = plugin.StatusFailed
|
||||||
|
p.Error = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
missingRequired := a.capRegistry.CheckRequired(p.Manifest.Requires)
|
||||||
|
if len(missingRequired) > 0 {
|
||||||
|
p.Status = plugin.StatusMissingRequiredCapability
|
||||||
|
p.Error = fmt.Sprintf("missing required: %s", strings.Join(missingRequired, ", "))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
missingOptional := a.capRegistry.CheckRequired(p.Manifest.OptionalRequires)
|
||||||
|
if len(missingOptional) > 0 {
|
||||||
|
p.Status = plugin.StatusDegraded
|
||||||
|
} else {
|
||||||
|
p.Status = plugin.StatusLoaded
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Manifest.Contributes != nil {
|
||||||
|
a.contribRegistry.Register(p.Manifest.ID, p.Manifest.Contributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a.plugins = plugins
|
a.plugins = plugins
|
||||||
|
|
||||||
var buf strings.Builder
|
var buf strings.Builder
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,13 @@ func (r *Registry) Register(pluginID string, capabilities []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnregisterAll removes all capabilities (used before reload).
|
||||||
|
func (r *Registry) UnregisterAll() {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
r.capabilities = make(map[string]*Entry)
|
||||||
|
}
|
||||||
|
|
||||||
// Unregister removes all capabilities provided by a plugin.
|
// Unregister removes all capabilities provided by a plugin.
|
||||||
func (r *Registry) Unregister(pluginID string) {
|
func (r *Registry) Unregister(pluginID string) {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
|
|
|
||||||
76
main.go
76
main.go
|
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -42,6 +43,23 @@ func main() {
|
||||||
permRegistry := permissions.NewRegistry()
|
permRegistry := permissions.NewRegistry()
|
||||||
eventBus := events.NewBus()
|
eventBus := events.NewBus()
|
||||||
|
|
||||||
|
// ─── Register Core Capabilities ─────────────────────────
|
||||||
|
// These are provided by the desktop core itself, not by plugins.
|
||||||
|
// Registered before plugin discovery so that plugins can resolve
|
||||||
|
// required capabilities (e.g. verstak/core/plugin-manager/v1) at load time.
|
||||||
|
corePluginID := "verstak-desktop"
|
||||||
|
coreCaps := []string{
|
||||||
|
"verstak/core/plugin-manager/v1",
|
||||||
|
"verstak/core/capability-registry/v1",
|
||||||
|
"verstak/core/contribution-registry/v1",
|
||||||
|
"verstak/core/permissions/v1",
|
||||||
|
"verstak/core/events/v1",
|
||||||
|
}
|
||||||
|
if err := capRegistry.Register(corePluginID, coreCaps); err != nil {
|
||||||
|
log.Fatalf("[main] failed to register core capabilities: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("[main] registered %d core capabilities", len(coreCaps))
|
||||||
|
|
||||||
// ─── Plugin Discovery ───────────────────────────────────
|
// ─── Plugin Discovery ───────────────────────────────────
|
||||||
discoveryDirs := []string{
|
discoveryDirs := []string{
|
||||||
"~/.config/verstak/plugins",
|
"~/.config/verstak/plugins",
|
||||||
|
|
@ -62,6 +80,64 @@ func main() {
|
||||||
|
|
||||||
log.Printf("[plugin] discovered %d plugins", len(plugins))
|
log.Printf("[plugin] discovered %d plugins", len(plugins))
|
||||||
|
|
||||||
|
// ─── Plugin Lifecycle: Register Capabilities + Contributions ──
|
||||||
|
for i := range plugins {
|
||||||
|
p := &plugins[i]
|
||||||
|
|
||||||
|
// Register provided capabilities
|
||||||
|
if len(p.Manifest.Provides) > 0 {
|
||||||
|
if err := capRegistry.Register(p.Manifest.ID, p.Manifest.Provides); err != nil {
|
||||||
|
log.Printf("[plugin] %s: capability registration failed: %v", p.Manifest.ID, err)
|
||||||
|
p.Status = plugin.StatusFailed
|
||||||
|
p.Error = err.Error()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf("[plugin] %s: registered %d capabilities", p.Manifest.ID, len(p.Manifest.Provides))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve required capabilities
|
||||||
|
missingRequired := capRegistry.CheckRequired(p.Manifest.Requires)
|
||||||
|
if len(missingRequired) > 0 {
|
||||||
|
log.Printf("[plugin] %s: missing required capabilities: %v", p.Manifest.ID, missingRequired)
|
||||||
|
p.Status = plugin.StatusMissingRequiredCapability
|
||||||
|
p.Error = fmt.Sprintf("missing required: %s", strings.Join(missingRequired, ", "))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check optional capabilities for degraded mode
|
||||||
|
missingOptional := capRegistry.CheckRequired(p.Manifest.OptionalRequires)
|
||||||
|
if len(missingOptional) > 0 {
|
||||||
|
log.Printf("[plugin] %s: missing optional capabilities (degraded): %v", p.Manifest.ID, missingOptional)
|
||||||
|
p.Status = plugin.StatusDegraded
|
||||||
|
} else {
|
||||||
|
p.Status = plugin.StatusLoaded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register contributions
|
||||||
|
if p.Manifest.Contributes != nil {
|
||||||
|
contribRegistry.Register(p.Manifest.ID, p.Manifest.Contributes)
|
||||||
|
log.Printf("[plugin] %s: contributions registered", p.Manifest.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[plugin] %s: status=%s", p.Manifest.ID, p.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Log Summary ───────────────────────────────────────
|
||||||
|
loaded := 0
|
||||||
|
degraded := 0
|
||||||
|
failed := 0
|
||||||
|
for _, p := range plugins {
|
||||||
|
switch p.Status {
|
||||||
|
case plugin.StatusLoaded:
|
||||||
|
loaded++
|
||||||
|
case plugin.StatusDegraded:
|
||||||
|
degraded++
|
||||||
|
default:
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("[main] lifecycle summary: loaded=%d degraded=%d failed=%d", loaded, degraded, failed)
|
||||||
|
|
||||||
// Create the App struct
|
// Create the App struct
|
||||||
app := api.NewApp(capRegistry, contribRegistry, permRegistry, eventBus, plugins)
|
app := api.NewApp(capRegistry, contribRegistry, permRegistry, eventBus, plugins)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue