diff --git a/plugins/examples/minimal/main.go b/plugins/examples/minimal/main.go index 9f558b076..6f0d5afec 100644 --- a/plugins/examples/minimal/main.go +++ b/plugins/examples/minimal/main.go @@ -14,11 +14,10 @@ import ( ) type Manifest struct { - Name string `json:"name"` - Author string `json:"author"` - Version string `json:"version"` - Description string `json:"description"` - Capabilities []string `json:"capabilities"` + Name string `json:"name"` + Author string `json:"author"` + Version string `json:"version"` + Description string `json:"description"` } type ArtistInput struct { @@ -34,11 +33,10 @@ type BiographyOutput struct { //go:wasmexport nd_manifest func ndManifest() int32 { manifest := Manifest{ - Name: "Minimal Example", - Author: "Navidrome", - Version: "1.0.0", - Description: "A minimal example plugin", - Capabilities: []string{"MetadataAgent"}, + Name: "Minimal Example", + Author: "Navidrome", + Version: "1.0.0", + Description: "A minimal example plugin", } out, err := json.Marshal(manifest) if err != nil { diff --git a/plugins/manager.go b/plugins/manager.go index 401c9a68c..10d0f98fd 100644 --- a/plugins/manager.go +++ b/plugins/manager.go @@ -282,7 +282,6 @@ func (m *Manager) discoverPlugins(folder string) error { // loadPlugin loads a single plugin from a wasm file func (m *Manager) loadPlugin(name, wasmPath string) error { - // Check if manager is stopped (cache may be closed) if m.stopped.Load() { return fmt.Errorf("manager is stopped") } @@ -296,81 +295,69 @@ func (m *Manager) loadPlugin(name, wasmPath string) error { return fmt.Errorf("manager is stopped") } - // Read wasm file wasmBytes, err := os.ReadFile(wasmPath) if err != nil { return err } - // Get plugin-specific config from conf.Server.PluginConfig pluginConfig := m.getPluginConfig(name) - - // Create Extism manifest for this plugin - // Note: We create a temporary plugin first to get the manifest, - // then we'll create the final one with proper AllowedHosts - tempManifest := extism.Manifest{ + pluginManifest := extism.Manifest{ Wasm: []extism.Wasm{ - extism.WasmData{ - Data: wasmBytes, - Name: "main", - }, + extism.WasmData{Data: wasmBytes, Name: "main"}, }, - Config: pluginConfig, + Config: pluginConfig, + Timeout: uint64(defaultTimeout.Milliseconds()), } - - tempConfig := extism.PluginConfig{ + extismConfig := extism.PluginConfig{ EnableWasi: true, RuntimeConfig: wazero.NewRuntimeConfig().WithCompilationCache(m.cache), } - // Create temporary plugin to read manifest and detect capabilities - tempPlugin, err := extism.NewPlugin(m.ctx, tempManifest, tempConfig, nil) + // Create initial compiled plugin (without AllowedHosts) + compiled, err := extism.NewCompiledPlugin(m.ctx, pluginManifest, extismConfig, nil) if err != nil { return err } - defer tempPlugin.Close(m.ctx) - tempPlugin.SetLogger(extismLogger(name)) - // Call nd_manifest to get plugin manifest - exit, manifestBytes, err := tempPlugin.Call(manifestFunction, nil) + // Create instance to read manifest and detect capabilities + instance, err := compiled.Instance(m.ctx, extism.PluginInstanceConfig{}) if err != nil { + compiled.Close(m.ctx) + return err + } + instance.SetLogger(extismLogger(name)) + + exit, manifestBytes, err := instance.Call(manifestFunction, nil) + if err != nil { + instance.Close(m.ctx) + compiled.Close(m.ctx) return err } if exit != 0 { + instance.Close(m.ctx) + compiled.Close(m.ctx) return fmt.Errorf("calling %s: %d", manifestFunction, exit) } - // Parse manifest (validation happens during unmarshal via generated code) var manifest Manifest if err := json.Unmarshal(manifestBytes, &manifest); err != nil { + instance.Close(m.ctx) + compiled.Close(m.ctx) return fmt.Errorf("invalid plugin manifest: %w", err) } - // Detect capabilities based on exported functions - capabilities := detectCapabilities(tempPlugin) + // Detect capabilities using the instance before closing it + capabilities := detectCapabilities(instance) + instance.Close(m.ctx) - // Now create the final compiled plugin with proper AllowedHosts - finalManifest := extism.Manifest{ - Wasm: []extism.Wasm{ - extism.WasmData{ - Data: wasmBytes, - Name: "main", - }, - }, - Config: pluginConfig, - AllowedHosts: manifest.AllowedHosts(), - Timeout: uint64(defaultTimeout.Milliseconds()), - } - log.Debug(m.ctx, "Loaded plugin", "plugin", name, "name", manifest.Name, "version", manifest.Version, "capabilities", capabilities) - - finalConfig := extism.PluginConfig{ - EnableWasi: true, - RuntimeConfig: wazero.NewRuntimeConfig().WithCompilationCache(m.cache), - } - - compiled, err := extism.NewCompiledPlugin(m.ctx, finalManifest, finalConfig, nil) - if err != nil { - return err + // Recompile only if plugin requires HTTP access (AllowedHosts) + if hosts := manifest.AllowedHosts(); len(hosts) > 0 { + compiled.Close(m.ctx) + pluginManifest.AllowedHosts = hosts + compiled, err = extism.NewCompiledPlugin(m.ctx, pluginManifest, extismConfig, nil) + if err != nil { + return err + } } m.mu.Lock() @@ -382,7 +369,6 @@ func (m *Manager) loadPlugin(name, wasmPath string) error { capabilities: capabilities, } m.mu.Unlock() - return nil } @@ -466,8 +452,6 @@ func (m *Manager) ReloadPlugin(name string) error { log.Error(m.ctx, "Failed to reload plugin, plugin remains unloaded", "plugin", name, err) return fmt.Errorf("failed to reload plugin %q: %w", name, err) } - - log.Info(m.ctx, "Reloaded plugin", "plugin", name) return nil } diff --git a/plugins/manifest.go b/plugins/manifest.go index 249160197..f8bbe979f 100644 --- a/plugins/manifest.go +++ b/plugins/manifest.go @@ -10,3 +10,6 @@ func (m *Manifest) AllowedHosts() []string { } return m.Permissions.Http.AllowedHosts } + +// TODO: ConfigPermission is defined in the schema but not currently enforced. +// Plugins always receive their config section. Implement permission checking or remove from schema. diff --git a/plugins/watcher.go b/plugins/watcher.go index 7225d1bd3..a2e51909f 100644 --- a/plugins/watcher.go +++ b/plugins/watcher.go @@ -13,8 +13,7 @@ import ( // debounceDuration is the time to wait before acting on file events // to handle multiple rapid events for the same file. -// This is a var (not const) to allow tests to use a shorter duration. -var debounceDuration = 500 * time.Millisecond +const debounceDuration = 500 * time.Millisecond // startWatcher starts the file watcher for the plugins folder. // It watches for CREATE, WRITE, and REMOVE events on .wasm files.