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")
|
||||
}
|
||||
|
||||
// ── 7. Capability registration ──
|
||||
// ── 7. Capability registration (core + plugin) ──
|
||||
fmt.Printf("\n[capability registration]\n")
|
||||
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 {
|
||||
err := reg.Register(target.Manifest.ID, []string{p})
|
||||
if err != nil {
|
||||
if err := reg.Register(m.ID, []string{p}); err != nil {
|
||||
fmt.Printf(" ❌ register capability %s: %v\n", p, err)
|
||||
allGood = false
|
||||
} 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")
|
||||
if allGood {
|
||||
fmt.Printf("✅ smoke-platform passed\n")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -108,7 +109,55 @@ func (a *App) ReloadPlugins() (int, string) {
|
|||
|
||||
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)
|
||||
|
||||
// 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
|
||||
|
||||
var buf strings.Builder
|
||||
|
|
|
|||
|
|
@ -46,6 +46,13 @@ func (r *Registry) Register(pluginID string, capabilities []string) error {
|
|||
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.
|
||||
func (r *Registry) Unregister(pluginID string) {
|
||||
r.mu.Lock()
|
||||
|
|
|
|||
76
main.go
76
main.go
|
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -42,6 +43,23 @@ func main() {
|
|||
permRegistry := permissions.NewRegistry()
|
||||
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 ───────────────────────────────────
|
||||
discoveryDirs := []string{
|
||||
"~/.config/verstak/plugins",
|
||||
|
|
@ -62,6 +80,64 @@ func main() {
|
|||
|
||||
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
|
||||
app := api.NewApp(capRegistry, contribRegistry, permRegistry, eventBus, plugins)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue