Phase 0 research resolves all 12 open decisions (interface shape, registration, host API, dynamic toggle, migrations, UI panel integration, config format, testing, boundary enforcement, squash, failure notification, integration test). Phase 1 artifacts: data-model.md (Plugin, Registry, Migration, Host, Status, Backup), contracts/plugin.md (Plugin + HasX interfaces), contracts/host.md (Host struct + plugintest constructor), contracts/rest.md (/api/plugins/*), quickstart.md (end-to-end "hello" plugin in 8 steps). All ten constitution gates pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.1 KiB
Implementation Plan: Plugin System for SynapBus Core
Branch: 019-plugin-system | Date: 2026-04-19 | Spec: spec.md
Input: Feature specification from /specs/019-plugin-system/spec.md
Summary
Refactor SynapBus into a tiny core (messaging, channels, reactions, auth, storage, MCP transport, search, attachments, Web UI shell) plus a set of compile-in plugins. Deliver in three phases: Phase 0 backs up the live kubic instance and squashes 26 migrations into a single 000_initial.sql; Phase 1 introduces internal/plugin/ (interface, host, registry, lifecycle) plus the plugintest helper and wires SIGHUP-triggered graceful restart; Phase 2 extracts the wiki feature as the canonical pilot plugin. The remaining nine plugin candidates are mechanical follow-ups not in this spec.
Technical Context
Language/Version: Go 1.25+ (per go.mod)
Primary Dependencies: mark3labs/mcp-go (MCP), go-chi/chi (HTTP), spf13/cobra (CLI), modernc.org/sqlite (storage), jmoiron/sqlx (query helpers), cloudflare/tableflip (graceful restart — NEW), gopkg.in/yaml.v3 (config), xeipuuv/gojsonschema (config-schema validation)
Storage: SQLite via modernc.org/sqlite (pure Go, zero CGO). New core table plugin_migrations. Plugin tables namespaced plugin_<name>_*.
Testing: go test, table-driven; new internal/plugin/plugintest/ package shipped alongside internal/plugin/; integration test boots the binary with a fixture config and exercises wiki via curl + MCP.
Target Platform: linux/amd64, darwin/arm64, darwin/amd64 (per constitution III)
Project Type: Single-binary Go server with embedded Svelte Web UI
Performance Goals: Graceful restart < 2 s visible downtime on dev laptop; plugin Init < 100 ms each; plugintest.Run < 1 s per plugin.
Constraints: Zero CGO; plugins may not import from internal/ outside internal/plugin/*; core may not import from internal/plugins/*; enforced via a compile-time lint pass.
Scale/Scope: ~12 eventual plugins; in this feature 1 (wiki) is extracted. Current codebase is ~44 KLOC Go + 28 KLOC tests across 37 internal/ packages.
Constitution Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
| Principle | Status | Notes |
|---|---|---|
| I. Local-First, Single Binary | ✅ Pass | Plugins compile-in; no RPC, no .so, no Wasm. cloudflare/tableflip is pure-Go, no daemon dependency. |
| II. MCP-Native | ✅ Pass | Plugins contribute MCP tools through HasMCPTools / HasActions; core MCP server remains the only agent interface. |
| III. Pure Go, Zero CGO | ✅ Pass | All new deps (tableflip, yaml.v3, gojsonschema) are pure Go. |
| IV. Multi-Tenant with Ownership | ✅ Pass | Plugin failure notification targets the default-agent owner. Host exposes owner-scoped messaging. |
| V. Embedded OAuth 2.1 | ✅ Pass | Auth stays in core; plugins receive an action-scope check via Registration.RequiredScope. |
| VI. Semantic-Ready Storage | ✅ Pass | Search index remains in core and is exposed via Host.Search. Plugins use it read-only. |
| VII. Swarm Intelligence Patterns | ✅ Pass | Channel-type pattern (HasChannelType) makes auction/blackboard a plug-point rather than a hardcode. |
| VIII. Observable by Default | ✅ Pass | Per-plugin tracer and metrics registerer on Host; /api/plugins/status exposes state. Failure paths log + DM the owner. |
| IX. Progressive Complexity | ✅ Pass | Disabling every plugin leaves a working tier-1 system (messaging + agent registration). |
| X. Web UI as First-Class | ✅ Pass | Plugin panels served from /ui/plugins/<name>; shell continues to own navigation and auth. |
All ten gates pass. No complexity-tracking entries required.
Project Structure
Documentation (this feature)
specs/019-plugin-system/
├── plan.md # this file
├── spec.md # feature spec
├── research.md # Phase 0 research synthesis
├── data-model.md # Phase 1 entity model
├── quickstart.md # "build your first plugin" walkthrough
├── contracts/
│ ├── plugin.md # Plugin + HasX interface contracts
│ ├── host.md # Host API contract
│ └── rest.md # /api/plugins/* contract
├── checklists/
│ └── requirements.md # spec quality checklist
└── tasks.md # Phase 2 output (generated by /speckit.tasks)
Source Code (repository root)
internal/
├── plugin/ # NEW — plugin framework (~300 LOC)
│ ├── plugin.go # Plugin, HasX interfaces
│ ├── host.go # Host struct and scoped accessors
│ ├── registry.go # registry, enable/disable, panic-on-dup
│ ├── lifecycle.go # three-phase boot, shutdown in reverse
│ ├── status.go # Plugin status tracking + /api/plugins/status handler
│ ├── config.go # YAML load, per-plugin config extraction
│ ├── migrator.go # per-plugin migration runner using plugin_migrations table
│ ├── restart.go # SIGHUP → tableflip.Upgrade re-exec
│ └── plugintest/
│ ├── nop_host.go # NopHost with in-memory SQLite
│ ├── run.go # Run(t, plugin) full-lifecycle smoke
│ └── assertions.go # HasTool/HasRoute/HasMigration helpers
├── plugins/ # NEW — all plugins live here
│ ├── standard/
│ │ └── import.go # var Standard = []plugin.Plugin{ ... } consumed by main.go
│ └── wiki/ # FIRST EXTRACTED PLUGIN
│ ├── plugin.go # WikiPlugin implementing Plugin + HasActions + HasHTTPRoutes + HasWebPanels + HasMigrations + HasMCPTools + HasConfigSchema
│ ├── actions.go # create_article, get_article, list_articles, update_article, get_backlinks
│ ├── routes.go # /api/plugins/wiki/* (if any needed for the UI panel)
│ ├── panel.go # /ui/plugins/wiki/* serving embedded HTML+JS
│ ├── store.go # SQL access against plugin_wiki_articles (moved from internal/wiki)
│ ├── schema/
│ │ └── 001_initial.sql # creates plugin_wiki_articles, plugin_wiki_backlinks
│ ├── ui/ # embedded assets (Svelte or plain HTML — choose simplest)
│ │ └── index.html
│ └── plugin_test.go # plugintest.Run + action-level unit tests
│
cmd/synapbus/
├── main.go # calls plugin.NewRegistry(defaultPlugins()) and bootstraps
└── plugins.go # NEW — defaultPlugins() returns []plugin.Plugin
│
schema/ # core migrations
├── 000_initial.sql # NEW — squashed from all 26 prior migrations
└── (legacy files archived into ../legacy-migrations/ at squash time)
│
scripts/
├── backup-kubic.sh # NEW — SSH into kubic, dump DB + attachments + secrets key + hnsw index, produce manifest
├── verify-backup.sh # NEW — restore archive into scratch dir, schema diff
└── generate-squash.sh # NEW — sqlite .schema + light edits → 000_initial.sql
│
docs/plugins/ # NEW
├── authoring.md # step-by-step "write your first plugin"
├── lifecycle.md # migrate → init → start, shutdown semantics
└── capabilities.md # each HasX interface with example snippets
│
test/integration/
└── plugin_system_test.go # NEW — boots binary, curls MCP + REST, asserts enable/disable
Structure Decision: Standard Go internal/ layout. The plugin framework lives at internal/plugin/; all plugins live at internal/plugins/<name>/. A single import barrel internal/plugins/standard/import.go is consumed by cmd/synapbus/plugins.go → defaultPlugins(). No init() registration is used. The compile-time boundary (core files must not import internal/plugins/*) is enforced by a go vet analyzer added in Phase 1.
Complexity Tracking
No constitution violations. No entries required.