Files
synapbus/specs/019-plugin-system/plan.md
T
Algis DumbrisandClaude Opus 4.7 69cd13dce1 plan(019): plugin system plan + research + data-model + contracts + quickstart
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>
2026-04-19 06:56:36 +03:00

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.