Implements the compile-in plugin system designed in spec 019:
- internal/plugin/ (~1,300 LOC):
* Tiny Plugin interface + 10 optional HasX capability sub-interfaces
* Host struct with Logger / DB / Messenger / Channels / Attachments /
Search / Secrets / Events / Config / DataDir / Tracer / Metrics /
DefaultOwner / BaseURL
* Registry with panic-on-duplicate, stability-level tracking, capability
indexing (MCP tools, actions, panels, channel types, routes, event
subscribers, CLI commands)
* Migrator: per-plugin SHA-256-checksum'd migration chain, namespaced
plugin_<name>_* table enforcement, idempotent re-apply
* Three-phase lifecycle (Migrate → Init → Start) with panic-safe
wrappers around every plugin call; failure per plugin isolated,
core continues
* YAML config loader that preserves unknown top-level keys on round-trip
* Status store exposing /api/plugins/status JSON
* Restart helpers (Noop + SignalRestarter); graceful reload is
in-process for the demo
- internal/plugin/plugintest/ (~345 LOC):
* NopHost(t) with in-memory modernc.org/sqlite
* Run(t, plugin) full-lifecycle smoke helper
* Assertions: HasTool, HasAction, HasPanel, HasChannelType,
HasMigration, PluginStarted, PluginFailed
* ScopedSecrets that returns ErrSecretNotFound for cross-plugin
reads (satisfies SC-006)
- internal/plugins/demo/ (canonical showcase):
* Plugin that exercises every HasX capability (migrations, actions,
HTTP routes, web panel, lifecycle, config schema, stability)
* Own SQL migration creating plugin_demo_notes
* Embedded HTML panel that fetches notes via JS
* 4 unit tests covering smoke, full capability registration, action
handlers, and config-driven max_notes limit
- cmd/plugindemo/ (~290 LOC):
* Demo HTTP server wiring registry to chi
* Mounts /api/plugins/status, /api/admin/plugins/{name}/{enable,disable},
/api/actions/{name}, /api/plugins/<name>/* (per-plugin REST),
/ui/plugins/<name>/ (per-plugin UI)
* SIGHUP-triggered config reload + registry rebuild + mux swap
* SIGTERM/SIGINT graceful shutdown
- test/integration/ (~357 LOC, build-tag "integration"):
* 6 end-to-end tests against a spawned plugindemo binary
* Enable/disable round-trip with data preservation
* SIGHUP reload timing (measured 41 ms — SC-008 target is 2 s)
* Action-404 on disabled plugin, panel-404 on disabled plugin
* REST endpoints + UI panel reachable
Contract deviation: admin toggle endpoints moved from
/api/plugins/{name}/{enable,disable} to /api/admin/plugins/{name}/{...}
to avoid URL collision with chi per-plugin route mounts. rest.md updated.
Scope deferred to next session (mechanical follow-ups):
- Port internal/wiki/ to internal/plugins/wiki/
- Squash 26 migrations to schema/000_initial.sql
- Backup scripts for live kubic instance
- Remaining 9 plugin extractions
- Boundary-lint static analyzer
- Wire into cmd/synapbus/main.go
All unit + integration tests green. Chrome UI smoke test passes.
autonomous_summary.md carries the full verification record.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
2.4 KiB
Go
96 lines
2.4 KiB
Go
package plugin
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Status describes a plugin's current lifecycle state.
|
|
type Status string
|
|
|
|
const (
|
|
StatusRegistered Status = "registered"
|
|
StatusDisabled Status = "disabled"
|
|
StatusMigrated Status = "migrated"
|
|
StatusInitialized Status = "initialized"
|
|
StatusStarted Status = "started"
|
|
StatusFailed Status = "failed"
|
|
StatusStopped Status = "stopped"
|
|
)
|
|
|
|
// StatusEntry is the runtime snapshot of one plugin's state, suitable for
|
|
// JSON serialization by /api/plugins/status.
|
|
type StatusEntry struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
Stability string `json:"stability"`
|
|
Enabled bool `json:"enabled"`
|
|
Status Status `json:"status"`
|
|
StartedAt time.Time `json:"started_at,omitempty"`
|
|
ErrorMessage string `json:"error,omitempty"`
|
|
Capabilities []string `json:"capabilities"`
|
|
ToolsRegistered []string `json:"tools_registered"`
|
|
ActionsRegistered []string `json:"actions_registered"`
|
|
MigrationVersions []int `json:"migration_versions"`
|
|
}
|
|
|
|
// StatusStore is a thread-safe registry of plugin status entries.
|
|
type StatusStore struct {
|
|
mu sync.RWMutex
|
|
entries map[string]*StatusEntry
|
|
order []string
|
|
}
|
|
|
|
func NewStatusStore() *StatusStore {
|
|
return &StatusStore{entries: map[string]*StatusEntry{}}
|
|
}
|
|
|
|
func (s *StatusStore) Set(name string, e StatusEntry) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if _, ok := s.entries[name]; !ok {
|
|
s.order = append(s.order, name)
|
|
}
|
|
e.Name = name
|
|
s.entries[name] = &e
|
|
}
|
|
|
|
func (s *StatusStore) Update(name string, fn func(*StatusEntry)) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
e, ok := s.entries[name]
|
|
if !ok {
|
|
return
|
|
}
|
|
fn(e)
|
|
}
|
|
|
|
func (s *StatusStore) Get(name string) (StatusEntry, bool) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
e, ok := s.entries[name]
|
|
if !ok {
|
|
return StatusEntry{}, false
|
|
}
|
|
return *e, true
|
|
}
|
|
|
|
// All returns a stable-ordered snapshot of all entries.
|
|
func (s *StatusStore) All() []StatusEntry {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
out := make([]StatusEntry, 0, len(s.order))
|
|
for _, n := range s.order {
|
|
out = append(out, *s.entries[n])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// MarshalJSON returns {"plugins": [...]} per the REST contract.
|
|
func (s *StatusStore) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(struct {
|
|
Plugins []StatusEntry `json:"plugins"`
|
|
}{Plugins: s.All()})
|
|
}
|