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>
3.0 KiB
3.0 KiB
Contract — Host struct
package plugin
import (
"context"
"database/sql"
"encoding/json"
"log/slog"
"github.com/jmoiron/sqlx"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/trace"
"github.com/synapbus/synapbus/internal/attachments"
"github.com/synapbus/synapbus/internal/channels"
"github.com/synapbus/synapbus/internal/eventbus"
"github.com/synapbus/synapbus/internal/messaging"
"github.com/synapbus/synapbus/internal/search"
"github.com/synapbus/synapbus/internal/secrets"
"github.com/synapbus/synapbus/internal/users"
)
// Host is the bundle of core services handed to a plugin at Init.
// Plugins MUST treat it as read-only and MUST NOT cache sub-handles
// across process lifetimes.
type Host struct {
Logger *slog.Logger // pre-tagged plugin=<name>
DB *sqlx.DB // shared core handle
Messaging messaging.API
Channels channels.API
Attachments attachments.Store
Search search.Index
Secrets secrets.Scoped // scoped to calling plugin
Events eventbus.Bus
Config json.RawMessage // plugins[<name>] YAML sub-tree
DataDir string // <--data>/plugins/<name>/
Tracer trace.Tracer
Metrics prometheus.Registerer
DefaultOwner *users.User // for failure notifications
baseURL string // unexported; plugins read via BaseURL()
}
// BaseURL returns the public base URL of this instance, for building
// absolute links in panel HTML.
func (h Host) BaseURL() string { return h.baseURL }
// ExecTx runs a function inside a database transaction against the shared
// handle. Plugins should use this for multi-statement writes.
func (h Host) ExecTx(ctx context.Context, fn func(*sqlx.Tx) error) error { … }
Security / scope invariants
Host.Secretsreturns only secrets that were written with the caller's plugin name. Calling.Get("X")for a secret owned by another plugin returnssecrets.ErrNotFound, same as missing.Host.DBgives full SQL access — but tables read/written are expected to beplugin_<name>_*. A static linter (part of this feature) flags unqualified reads from core tables.Host.DataDiris guaranteed to exist, to be a directory, and to be writable only by this plugin (permissions 0700).Host.Metricsis a sub-registerer; labels automatically carryplugin="<name>".
Test constructor (from plugintest)
package plugintest
// NopHost returns an in-memory Host backed by an in-memory SQLite database
// and a temporary directory. Suitable for unit tests.
func NopHost(t *testing.T) *plugin.Host { … }
// Run performs a full lifecycle dry-run of a plugin against a NopHost:
// 1. Migrations are applied.
// 2. Init is called.
// 3. If HasLifecycle, Start is called.
// 4. All declared capabilities are asserted to be registered.
// 5. Shutdown + close.
// t.Fatal on any error.
func Run(t *testing.T, p plugin.Plugin) { … }