plugin

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 10, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package plugin provides a minimal plugin system for cleat.

Plugins are Go packages compiled into the worker binary. They register via init() using Register(), and the worker discovers them at startup. The registry, lifecycle management, host function helpers, and crash recovery wrappers live here.

Key types:

  • Plugin — interface all plugins implement
  • PluginInfo — metadata for discovery and documentation
  • Environment — infrastructure access (DB, HTTP mux, logger)
  • Registry — central plugin function registry
  • Manifest — plugin manifest for code generation

Package plugin provides a minimal plugin system for cleat.

Plugins are Go packages compiled into the worker binary. They register themselves via init() and the central registry. The worker discovers them at startup, calls Init(), and optionally calls lifecycle methods based on which optional interfaces the plugin implements.

Design principle: give plugins access to infrastructure through purpose-built interfaces. The Environment struct provides a PluginDB interface (not *sql.DB directly), along with standard library types (*http.ServeMux, *slog.Logger).

Crash recovery boundaries:

Go-compiled plugins share the worker process, so a panic in a plugin host function can crash the entire worker. The RecoverPluginFunc and RecoverPluginStreamFunc wrappers (in recovery.go) add defer/recover boundaries around each host function call. When a panic is caught, the plugin is marked unhealthy and subsequent invocations are rejected without calling into the plugin.

Long-term migration: compile plugins to WASM modules instead of linking them into the worker binary. WASM provides process-level isolation so a plugin crash cannot affect the worker or other plugins. The recovery wrappers exist for Go-compiled plugins only.

Index

Constants

View Source
const RedactedPlaceholder = "[redacted]"

RedactedPlaceholder is what a Secret marshals and prints as.

Variables

View Source
var DefaultOfficialPublicKey = ""

DefaultOfficialPublicKey is the default Ed25519 public key for verifying official cleat plugin signatures. This is a well-known key distributed with the cleat binary. In production, operators can override this with a custom key via configuration.

The hex-encoded value below is a placeholder and MUST be replaced with the actual official cleat signing public key before release.

View Source
var OfficialPublicKeys = map[string]ed25519.PublicKey{}

OfficialPublicKeys maps SigningKeyID to Ed25519 public keys for official plugin verification. Keys are registered at build time or loaded from configuration.

Functions

func DownloadWASM

func DownloadWASM(ctx context.Context, url string) ([]byte, error)

DownloadWASM downloads a WASM binary from the given URL.

func InitAll

func InitAll(ctx context.Context, env *Environment, plugins []*LoadedPlugin)

InitAll calls Init on each loaded plugin in order. Plugins that panic or return an error during Init are marked unhealthy but do not halt initialization of remaining plugins.

Each plugin receives an Environment whose DB field is restricted based on the plugin's declared DatabaseAccess: None gets nil, ReadOnly gets nil (caller must set appropriate read-only adapter), ReadWrite gets the raw env (caller must set a read-write adapter).

NOTE: This function is primarily used by tests. In production, the worker sets per-plugin DB wrappers itself.

func LimitClause

func LimitClause(placeholder string, d Dialect) string

LimitClause returns a row-limiting clause using the given placeholder.

SQL Server has no LIMIT. It uses OFFSET/FETCH, which is only valid after an ORDER BY -- so callers must already be ordering their results, which any query with a row limit should be doing anyway to be deterministic.

func MustRegisterOfficialPublicKey

func MustRegisterOfficialPublicKey(keyID, pubKeyHex string)

MustRegisterOfficialPublicKey is like RegisterOfficialPublicKey but panics on error. Use in init() functions or main() to register well-known keys.

func QuoteIdent

func QuoteIdent(name string, d Dialect) string

QuoteIdent quotes a SQL identifier for the given dialect.

It exists because `key` and `value` -- both natural column names, and both used by shipped plugins -- are reserved words in MySQL and SQL Server. An unquoted `key` produced

Error 1064 (42000): You have an error in your SQL syntax ...
mssql: Incorrect syntax near the keyword 'key'.

on every kvstore and feature-flags route, on both backends. Quoting is per-dialect: PostgreSQL and standard SQL use double quotes, MySQL uses backticks, SQL Server uses square brackets.

Embedded quote characters are doubled/escaped so that a caller cannot inject through an identifier, but identifiers should still come from constants rather than user input.

func Rebind

func Rebind(query string, d Dialect) string

Rebind translates PostgreSQL $N parameter placeholders to the dialect-appropriate form. It also replaces now() with SYSUTCDATETIME() for MSSQL. PostgreSQL placeholders ($1, $2, ...) are left as-is.

func Register

func Register(info PluginInfo, ctor func() Plugin)

Register registers a plugin constructor with its PluginInfo. Call from init().

func RegisterOfficialPublicKey

func RegisterOfficialPublicKey(keyID, pubKeyHex string) error

RegisterOfficialPublicKey registers an Ed25519 public key for verifying official plugin signatures. The keyID is an opaque identifier (e.g., "cleat-official-2026") and the pubKeyHex is a hex-encoded Ed25519 public key (32 bytes = 64 hex chars).

func RegisterPluginTables

func RegisterPluginTables(ctx context.Context, db *sql.DB, pluginName string, tableNames []string) error

RegisterPluginTables inserts entries into admin.plugin_tables so that the tenant provisioning system knows which tables to GRANT. Called during plugin Init after migrations run.

func RunMigrations

func RunMigrations(ctx context.Context, db *sql.DB, dialect Dialect, coreMigrations []Migration, plugins []*LoadedPlugin) error

RunMigrations runs core migrations and plugin migrations in order. Core migrations are run first, then plugins in dependency order. Each plugin's migrations are tracked in a plugin_migrations table so they run only once.

func ValidateCapabilities

func ValidateCapabilities(declared, limits CapabilityLimits) error

ValidateCapabilities checks whether declared capabilities are within the granted limits. Returns an error listing ALL violations, not just the first.

func ValidateManifest

func ValidateManifest(m *Manifest) error

ValidateManifest validates a manifest against programmatic rules. Returns nil if valid, or an error describing all validation failures.

func VerifyChecksum

func VerifyChecksum(data []byte, expectedChecksum string) error

VerifyChecksum checks that the given data matches the expected SHA-256 checksum. If expectedChecksum is empty, verification is skipped.

func VerifyManifestSignature

func VerifyManifestSignature(m *Manifest, allowUnsigned bool) error

VerifyManifestSignature checks the Ed25519 signature on a manifest. If the manifest has no signature field, verification is skipped (returns nil). For official plugins (those using a cleat/ prefix or having no slash in the name), a missing signature is an error unless allowUnsigned is true.

func VerifyWasmSignature

func VerifyWasmSignature(wasmBytes []byte, iv *IndexVersion, pluginName string, allowUnsigned bool) error

VerifyWasmSignature checks the Ed25519 signature of a WASM binary against the expected checksum and the index version's signature field. The signature is computed over the SHA-256 hash of the WASM binary bytes.

func WithCallContext

func WithCallContext(ctx context.Context, cc *CallContext) context.Context

WithCallContext injects call context into the context.

Types

type AuditLogger

type AuditLogger interface {
	// Deploy records a plugin deployment event.
	Deploy(ctx context.Context, pluginName, pluginVersion string) error

	// Deprecate records a plugin deprecation event.
	Deprecate(ctx context.Context, pluginName, pluginVersion string) error

	// CapabilityChange records a change in plugin capabilities.
	CapabilityChange(ctx context.Context, pluginName, pluginVersion, details string) error

	// Invocation records a plugin function invocation. The dropRate
	// (0.0-1.0) controls sampling: 0.995 means ~1 in 200 are logged.
	Invocation(ctx context.Context, pluginName, functionName string, dropRate float64) error

	// EnforceRetention deletes audit log entries that exceed the policy.
	EnforceRetention(ctx context.Context, policy any) (int64, error)
}

AuditLogger is the interface for recording plugin lifecycle events. The concrete implementation writes to the plugin_audit_log table.

type CallContext

type CallContext struct {
	TenantID   string  `json:"tenant_id"`
	WorkflowID string  `json:"workflow_id"`
	DB         *sql.DB // tenant-scoped database connection
}

CallContext carries per-invocation metadata injected by the engine before calling plugin host functions.

func CallContextFromContext

func CallContextFromContext(ctx context.Context) *CallContext

CallContextFromContext extracts call context from the context. Returns nil if not present.

type Capabilities

type Capabilities struct {
	Database         bool     `json:"database" yaml:"database"`
	StartWorkflow    bool     `json:"start_workflow" yaml:"start_workflow"`
	SignalWorkflow   bool     `json:"signal_workflow" yaml:"signal_workflow"`
	HTTPRoutes       bool     `json:"http_routes" yaml:"http_routes"`
	HTTPMiddleware   bool     `json:"http_middleware" yaml:"http_middleware"`
	BackgroundWorker bool     `json:"background_worker" yaml:"background_worker"`
	CallPlugin       []string `json:"call_plugin" yaml:"call_plugin"`
}

Capabilities declares what infrastructure access a plugin needs.

func DefaultCapabilities

func DefaultCapabilities() Capabilities

DefaultCapabilities returns a Capabilities struct with all fields false.

type CapabilityLimits

type CapabilityLimits struct {
	Database         DatabaseAccess `json:"database"`
	StartWorkflow    bool           `json:"start_workflow"`
	SignalWorkflow   bool           `json:"signal_workflow"`
	HTTPRoutes       bool           `json:"http_routes"`
	HTTPMiddleware   bool           `json:"http_middleware"`
	BackgroundWorker bool           `json:"background_worker"`
	CallPlugin       []string       `json:"call_plugin"` // empty = deny all, ["*"] = allow all
}

CapabilityLimits defines the maximum capabilities allowed for a class of plugins. Used by operators to restrict what third-party plugins can do.

func DefaultLimits

func DefaultLimits() CapabilityLimits

DefaultLimits returns the default CapabilityLimits for community plugins. By default, community plugins get NO database access, and signal_workflow, but NOT start_workflow or http_routes.

func DeriveCapabilities

func DeriveCapabilities(p Plugin) CapabilityLimits

DeriveCapabilities derives CapabilityLimits from the optional interfaces a Go compile-time plugin implements. Called by the loader to determine what capabilities a Go plugin has based on which interfaces it satisfies.

func (CapabilityLimits) IsSet

func (l CapabilityLimits) IsSet() bool

IsSet returns true if any capability limit is configured (non-default). Used to distinguish intentionally-set limits from a zero-value struct.

type Command

type Command struct {
	Name        string
	Description string
	Run         func(args []string) error
}

Command describes a CLI subcommand exposed by a plugin.

type DatabaseAccess

type DatabaseAccess string

DatabaseAccess represents the level of database access a plugin is granted.

const (
	DatabaseAccessNone      DatabaseAccess = "none"
	DatabaseAccessReadOnly  DatabaseAccess = "read_only"
	DatabaseAccessReadWrite DatabaseAccess = "read_write"
)

type Dialect

type Dialect string

Dialect identifies the SQL dialect of the backing database.

const (
	DialectPostgres Dialect = "postgres"
	DialectMySQL    Dialect = "mysql"
	DialectMSSQL    Dialect = "mssql"
)

type Environment

type Environment struct {
	DB       PluginDB
	Mux      ServeMux // *http.ServeMux on host, interface{} on TinyGo
	Config   []byte
	Logger   *slog.Logger
	TenantID string
	Done     <-chan struct{}
	Dialect  Dialect

	// StartWorkflow starts a new workflow instance using the latest deployed version.
	// Plugins use this to trigger workflow executions (e.g., from cron schedules
	// or job queues). Returns the run ID of the new workflow instance.
	StartWorkflow func(ctx context.Context, defName string, input json.RawMessage) (runID string, err error)

	// SignalWorkflow delivers a signal to a running workflow instance.
	// The signal name and JSON payload are recorded deterministically
	// in the workflow_signals table.
	SignalWorkflow func(ctx context.Context, workflowID, signalName, payload string) error

	// Audit provides access to the plugin audit log. Plugins can record
	// deployment, deprecation, capability changes, and invocation events.
	// May be nil if the audit log is not configured.
	Audit *AuditLogger
}

Environment provides plugins with access to cleat infrastructure.

type FieldDef

type FieldDef struct {
	Type        string              `json:"type" yaml:"type"`
	Description string              `json:"description,omitempty" yaml:"description,omitempty"`
	Optional    bool                `json:"optional,omitempty" yaml:"optional,omitempty"`
	Format      string              `json:"format,omitempty" yaml:"format,omitempty"`
	Items       *FieldDef           `json:"items,omitempty" yaml:"items,omitempty"`
	Values      []string            `json:"values,omitempty" yaml:"values,omitempty"`
	Fields      map[string]FieldDef `json:"fields,omitempty" yaml:"fields,omitempty"`
	KeyType     *FieldDef           `json:"key_type,omitempty" yaml:"key_type,omitempty"`
	ValueType   *FieldDef           `json:"value_type,omitempty" yaml:"value_type,omitempty"`
}

FieldDef describes a field in a type definition.

type FuncOptions

type FuncOptions struct {
	Name       string // function name (required)
	Idempotent bool   // if true, safe to re-invoke during replay
}

FuncOptions configures a registered host function.

type FuncRegistry

type FuncRegistry interface {
	// Register adds a host function. The engine handles WASM I/O,
	// event history recording, and deterministic replay.
	Register(opts FuncOptions, fn PluginFunc) error
}

FuncRegistry lets plugins register workflow-callable functions. The plugin name is implicit -- each plugin gets its own scoped registry.

type HasBackground

type HasBackground interface {
	Plugin
	Run(ctx context.Context) error
}

HasBackground: plugin runs a background goroutine.

type HasCommands

type HasCommands interface {
	Plugin
	RegisterCommands() []Command
}

HasCommands: plugin adds CLI subcommands.

type HasHealth

type HasHealth interface {
	Plugin
	Health() error // nil = healthy
}

HasHealth: plugin reports its health status.

type HasHostFunctions

type HasHostFunctions interface {
	Plugin
	RegisterHostFunctions(scope FuncRegistry) error
}

HasHostFunctions: plugin adds functions callable from workflows. These functions are automatically recorded in event history and replayed deterministically -- plugin authors don't need to handle replay.

type HasMiddleware

type HasMiddleware interface {
	Plugin
	Middleware(next http.Handler) http.Handler
}

HasMiddleware: plugin wraps the HTTP handler chain.

type HasMigrations

type HasMigrations interface {
	Plugin
	Migrations() []Migration
}

HasMigrations: plugin needs database tables.

type HasRoutes

type HasRoutes interface {
	Plugin
	RegisterRoutes(mux *http.ServeMux) error
}

HasRoutes: plugin exposes HTTP endpoints.

type HealthStatus

type HealthStatus struct {
	Name    string `json:"name"`
	Healthy bool   `json:"healthy"`
	Error   string `json:"error,omitempty"`
}

HealthStatus describes the runtime health of a single plugin.

type HostFuncDef

type HostFuncDef struct {
	Description string  `json:"description" yaml:"description"`
	Input       TypeDef `json:"input" yaml:"input"`
	Output      TypeDef `json:"output" yaml:"output"`
	Idempotent  bool    `json:"idempotent,omitempty" yaml:"idempotent,omitempty"`
	Streaming   bool    `json:"streaming,omitempty" yaml:"streaming,omitempty"`
}

HostFuncDef describes a host function in a plugin manifest.

type IndexEntry

type IndexEntry struct {
	Name        string         `yaml:"name"`
	Description string         `yaml:"description"`
	Author      string         `yaml:"author"`
	Repository  string         `yaml:"repository,omitempty"`
	Versions    []IndexVersion `yaml:"versions"`
}

IndexEntry represents a plugin entry in the index.

func (*IndexEntry) IsOfficial

func (e *IndexEntry) IsOfficial() bool

IsOfficial returns true if the plugin is a cleat official plugin. Official plugins use the "cleat/" prefix or have no slash in the name.

type IndexVersion

type IndexVersion struct {
	Version         string `yaml:"version"`
	WasmURL         string `yaml:"wasm_url"`
	ManifestURL     string `yaml:"manifest_url,omitempty"`
	Checksum        string `yaml:"checksum"`
	MinCleatVersion string `yaml:"min_cleat_version,omitempty"`
	Bundled         bool   `yaml:"bundled,omitempty"`
	Description     string `yaml:"description,omitempty"`

	// Signature is an optional Ed25519 hex-encoded signature of the WASM
	// binary SHA-256 checksum. When present and the plugin is official,
	// the signature is verified during deployment. The signing key is
	// identified by SigningKeyID.
	Signature string `yaml:"signature,omitempty"`

	// SigningKeyID identifies the Ed25519 public key used to produce
	// Signature. Example: "cleat-official-2026".
	SigningKeyID string `yaml:"signing_key_id,omitempty"`
}

IndexVersion represents a specific version in the index.

type JSONColumn

type JSONColumn struct {
	Raw json.RawMessage
}

JSONColumn scans a JSON-valued column into json.RawMessage regardless of whether the driver delivers it as []byte or as string.

database/sql will convert a driver string into *[]byte, but not into a *json.RawMessage: json.RawMessage is a named []byte type and the conversion is not in convertAssign's fast path. lib/pq returns jsonb as []byte, so scanning straight into json.RawMessage works on PostgreSQL and hides the problem; go-mssqldb returns NVARCHAR as string, and every read of a JSON column failed with

sql: Scan error on column index 0, name "value": unsupported Scan,
storing driver.Value type string into type *json.RawMessage

That surfaced as HTTP 500 on single-row reads and as silently empty lists, because the row-scan error was logged and the row skipped.

func (*JSONColumn) Scan

func (j *JSONColumn) Scan(src any) error

Scan implements sql.Scanner.

func (JSONColumn) Value

func (j JSONColumn) Value() (driver.Value, error)

Value implements driver.Valuer so the same type can be used for writes.

It yields a string, not []byte, and that is deliberate. go-mssqldb maps a []byte argument to VARBINARY; inserting that into the NVARCHAR column that backs a JSON value stores the binary representation, which reads back as text that is not valid JSON. The symptom is a 200 with an empty body, because encoding/json fails part-way through writing the response. A string argument is sent as NVARCHAR by go-mssqldb, and lib/pq and go-sql-driver both accept a string for jsonb/JSON columns, so one form is correct everywhere.

type LoadedPlugin

type LoadedPlugin struct {
	Plugin  Plugin
	Healthy bool
	Error   error
}

LoadedPlugin wraps a plugin instance with its current state.

func Discover

func Discover() ([]*LoadedPlugin, error)

Discover instantiates all registered plugins in dependency order without calling Init. The caller should call RunMigrations followed by InitAll to complete plugin initialization.

func LoadAll

func LoadAll(ctx context.Context, env *Environment) ([]*LoadedPlugin, error)

LoadAll instantiates all registered plugins in dependency order, calls Init on each, and returns the successfully loaded plugins. A plugin that panics during Init is disabled and reported.

type Manifest

type Manifest struct {
	Name            string                 `json:"name" yaml:"name"`
	Version         string                 `json:"version" yaml:"version"`
	Description     string                 `json:"description" yaml:"description"`
	Author          string                 `json:"author" yaml:"author"`
	Repository      string                 `json:"repository,omitempty" yaml:"repository,omitempty"`
	MinCleatVersion string                 `json:"min_cleat_version,omitempty" yaml:"min_cleat_version,omitempty"`
	Capabilities    Capabilities           `json:"capabilities,omitempty" yaml:"capabilities,omitempty"`
	HostFunctions   map[string]HostFuncDef `json:"host_functions,omitempty" yaml:"host_functions,omitempty"`
	Types           map[string]TypeDef     `json:"types,omitempty" yaml:"types,omitempty"`
	// Signature is an optional Ed25519 signature of the canonical JSON
	// representation of the manifest (excluding the signature field itself).
	// When present, the runtime verifies the signature against the plugin
	// author's public key before deploying. Initially this is required only
	// for official plugins (cleat/ prefix or no slash in name); community
	// plugins may opt in.
	Signature string `json:"signature,omitempty" yaml:"signature,omitempty"`

	// SigningKeyID identifies the public key used to produce Signature.
	// This is an opaque identifier (e.g., "cleat-official-2026") that the
	// runtime uses to look up the corresponding Ed25519 public key.
	SigningKeyID string `json:"signing_key_id,omitempty" yaml:"signing_key_id,omitempty"`
}

Manifest is the parsed form of a plugin.json or plugin.yaml file.

func LoadManifest

func LoadManifest(path string) (*Manifest, error)

LoadManifest reads and parses a plugin manifest from a file path. Supports .json and .yaml/.yml extensions. Support for YAML requires gopkg.in/yaml.v3 or equivalent; currently only JSON is supported natively.

func (*Manifest) JSONBytes

func (m *Manifest) JSONBytes() ([]byte, error)

JSONBytes returns the manifest as pretty-printed JSON bytes.

type Migration

type Migration struct {
	Version int
	Up      string // required — SQL for PostgreSQL (the default)
	UpMySQL string // optional — MySQL DDL. Empty means PG-only for this version.
	UpMSSQL string // optional — MSSQL DDL. Empty means PG-only for this version.
	Down    string // optional — SQL to roll back
}

Migration describes a single database migration.

Up is the default SQL (PostgreSQL) and is required. UpMySQL and UpMSSQL are optional dialect-specific overrides. If the active dialect is MySQL or MSSQL and the corresponding field is empty, the migration is skipped with a warning.

type PanicError

type PanicError struct {
	Plugin string `json:"plugin"`
	Value  any    `json:"value"`
	Stack  string `json:"stack"`
}

PanicError is returned when a plugin host function panics. It captures the panic value and the full goroutine stack trace so operators can diagnose the root cause.

Long-term, plugins should be compiled to WASM modules for true process-level isolation. See design docs at docs/wasm-migration.md.

func (*PanicError) Error

func (e *PanicError) Error() string

func (*PanicError) Unwrap

func (e *PanicError) Unwrap() error

Unwrap returns nil — PanicError is a terminal error, not a wrapper.

type Plugin

type Plugin interface {
	Info() PluginInfo
	Init(ctx context.Context, env *Environment) error
}

Plugin is the only required interface. Every plugin must implement this.

type PluginDB

type PluginDB interface {
	Begin(ctx context.Context) (PluginTx, error)
	Exec(ctx context.Context, query string, args ...any) (int64, error)
	Query(ctx context.Context, query string, args ...any) (Rows, error)
	QueryRow(ctx context.Context, query string, args ...any) RowScanner
	Ping(ctx context.Context) error
}

PluginDB is the database handle available to plugins. It intentionally does not mirror *sql.DB — plugins get a scoped interface appropriate to their declared DatabaseAccess level.

type PluginFunc

type PluginFunc func(ctx context.Context, inputJSON string) (outputJSON string, err error)

PluginFunc is a plugin host function implementation. Takes JSON input, returns JSON output.

func RecoverPluginFunc

func RecoverPluginFunc(pluginName string, tracker *PluginHealthTracker, fn PluginFunc) PluginFunc

RecoverPluginFunc wraps a PluginFunc with panic recovery. If the wrapped function panics, the panic is caught, the plugin is marked unhealthy via the provided tracker, and a PanicError is returned. The full stack trace is logged using the standard log package.

type PluginHealthTracker

type PluginHealthTracker struct {
	// contains filtered or unexported fields
}

PluginHealthTracker tracks the runtime health of Go-compiled plugins. When a plugin's host function panics, it is marked unhealthy and all subsequent invocations are blocked without calling into the plugin.

Migration note: WASM-compiled plugins provide process-level isolation and do not need this tracker because a WASM crash cannot take down the worker. The tracker exists for Go-compiled plugins only.

func NewPluginHealthTracker

func NewPluginHealthTracker() *PluginHealthTracker

NewPluginHealthTracker creates a new PluginHealthTracker.

func (*PluginHealthTracker) IsHealthy

func (t *PluginHealthTracker) IsHealthy(pluginName string) bool

IsHealthy reports whether the plugin is healthy.

func (*PluginHealthTracker) MarkHealthy

func (t *PluginHealthTracker) MarkHealthy(pluginName string)

MarkHealthy clears any previous unhealthy status for the given plugin.

func (*PluginHealthTracker) MarkUnhealthy

func (t *PluginHealthTracker) MarkUnhealthy(pluginName string, err error)

MarkUnhealthy marks a plugin as unhealthy with the given fatal error. Once marked, all future invocations of the plugin's host functions will return this error without executing the function.

func (*PluginHealthTracker) UnhealthyError

func (t *PluginHealthTracker) UnhealthyError(pluginName string) error

UnhealthyError returns the error that caused the plugin to be marked unhealthy, or nil if the plugin is healthy.

func (*PluginHealthTracker) UnhealthyStatus

func (t *PluginHealthTracker) UnhealthyStatus() []HealthStatus

UnhealthyStatus returns the current health status of all plugins that have been marked unhealthy. Healthy plugins are not included because the tracker only records failures — it does not maintain a registry of all plugin names.

type PluginIndex

type PluginIndex struct {
	Plugins []IndexEntry `yaml:"plugins"`
}

PluginIndex represents the parsed index.yaml file.

func FetchIndex

func FetchIndex(ctx context.Context, urlStr string) (*PluginIndex, error)

FetchIndex downloads and parses the plugin index from a URL or file path. Supports http://, https:// URLs and local file paths (absolute or relative).

func (*PluginIndex) Resolve

func (idx *PluginIndex) Resolve(name, constraint string) (*IndexEntry, *IndexVersion, error)

Resolve finds the best matching version for a plugin name and constraint. Constraint can be empty (latest), "latest", or a semver constraint (^1.0.0, ~1.2.0, >=1.0.0, =1.0.0, or bare 1.0.0 treated as exact match).

type PluginInfo

type PluginInfo struct {
	Name           string         `json:"name"`
	Version        string         `json:"version"`
	Description    string         `json:"description"`
	Author         string         `json:"author,omitempty"`
	Requires       []string       `json:"requires,omitempty"`
	DatabaseAccess DatabaseAccess `json:"database_access,omitempty"`
}

PluginInfo describes a plugin for discovery and documentation.

func List

func List() []PluginInfo

List returns metadata for all registered plugins.

type PluginStreamFunc

type PluginStreamFunc func(ctx context.Context, inputJSON string) (<-chan StreamEvent, error)

PluginStreamFunc is a plugin host function that returns a stream of events. Takes JSON input and returns a channel that receives stream events.

func RecoverPluginStreamFunc

func RecoverPluginStreamFunc(pluginName string, tracker *PluginHealthTracker, fn PluginStreamFunc) PluginStreamFunc

RecoverPluginStreamFunc wraps a PluginStreamFunc with panic recovery. If the wrapped function panics during setup (before returning the channel), the panic is caught and handled like RecoverPluginFunc. Panics during channel consumption are not caught here — the consumer must handle them.

type PluginTx

type PluginTx interface {
	Exec(ctx context.Context, query string, args ...any) (int64, error)
	Query(ctx context.Context, query string, args ...any) (Rows, error)
	QueryRow(ctx context.Context, query string, args ...any) RowScanner
	Commit() error
	Rollback() error
}

PluginTx is a transaction scoped to a plugin operation.

type Query

type Query struct {
	Default string // required — PostgreSQL
	MySQL   string // optional
	MSSQL   string // optional
}

Query holds dialect-specific variants of a runtime SQL query. Default (PostgreSQL) is required; MySQL and MSSQL are optional overrides.

func (Query) For

func (q Query) For(d Dialect) string

For returns the SQL string appropriate for the given dialect. Falls back to Default when no dialect-specific override exists.

type RowScanner

type RowScanner interface {
	Scan(dest ...any) error
}

RowScanner abstracts a single row result for single-row queries.

type Rows

type Rows interface {
	RowScanner
	Next() bool
	Close() error
	Err() error
}

Rows is the result of a multi-row query.

type Secret

type Secret string

Secret is a string that refuses to leave the process.

It exists because five plugins independently stored a credential in a plain string field and returned it, unredacted, from their list and get endpoints: webhookingest and notifications (HMAC signing keys), pagerdutyalert (routing key), slacknotify (webhook URL, which IS the credential), and datadogexport (API key). Any bug that let a caller read one tenant's configuration row therefore disclosed the credential itself rather than merely its existence.

The obvious fix is a redaction helper called on the way out. That was rejected: a helper you have to remember is a helper the next plugin author forgets, and the failure is silent and invisible in review -- the response looks fine because the field is populated. datadogexport had exactly that shape and it did not hold; its redactAPIKey() was defeated by adding ?show_api_key=true to the URL, with no authorization check behind it.

So the redaction is in the TYPE. A Secret always marshals as RedactedPlaceholder, whatever the caller does, and there is no code path through encoding/json that emits the real value. Forgetting to redact now requires not using the type, which TestPluginSecretsUseTheSecretType catches.

It stays usable at both ends:

UnmarshalJSON accepts a real value, because create and update requests
carry one.
Scan and Value carry the real value to and from the database, because that
is where it legitimately lives.
Reveal returns the real value for the code that must actually use it --
signing an HMAC, calling an API. It is a method rather than a conversion so
that every intentional use is greppable.

String and Format redact too, so a Secret cannot leak through a log line or a %v in an error either. That is not hypothetical: the value most likely to end up in an error message is the one that failed to work.

func (Secret) Format

func (Secret) Format(f fmt.State, verb rune)

Format redacts for every verb, including %v and %#v.

String alone is not enough: %#v ignores Stringer and prints the underlying value, and %q would too.

func (Secret) MarshalJSON

func (Secret) MarshalJSON() ([]byte, error)

MarshalJSON always emits the placeholder. This is the whole point of the type; there is deliberately no option to disable it.

func (Secret) Reveal

func (s Secret) Reveal() string

Reveal returns the real value.

A method rather than a plain conversion so that every intentional use is one grep away. A reviewer asking "where does this credential actually get used" gets an answer from `grep -rn '.Reveal()'`.

func (*Secret) Scan

func (s *Secret) Scan(src any) error

Scan reads the real value back from the database.

func (Secret) String

func (Secret) String() string

String redacts, so a Secret cannot leak through fmt, a log line, or an error.

func (*Secret) UnmarshalJSON

func (s *Secret) UnmarshalJSON(b []byte) error

UnmarshalJSON accepts a real value, because create and update requests carry one.

It rejects the placeholder. Without that, a client that read a config, edited one unrelated field and PUT the whole object back would silently overwrite the stored credential with the literal string "[redacted]" -- a round-trip-shaped data loss that is easy to build a UI on top of by accident. The caller is told to omit the field instead.

func (Secret) Value

func (s Secret) Value() (driver.Value, error)

Value carries the real value to the database, which is where it legitimately lives.

type ServeMux

type ServeMux = *http.ServeMux

ServeMux is *http.ServeMux on the host, or interface{} in TinyGo WASM builds where net/http is unavailable.

type Stoppable

type Stoppable interface {
	Plugin
	Stop(ctx context.Context) error
}

Stoppable: plugin needs cleanup on shutdown.

type StreamEvent

type StreamEvent struct {
	Index   int    `json:"i"`
	Content string `json:"c"`
	Finish  bool   `json:"f"`
}

StreamEvent represents a single chunk of a streaming response.

type StreamFuncRegistry

type StreamFuncRegistry interface {
	RegisterStream(opts FuncOptions, fn PluginStreamFunc) error
}

StreamFuncRegistry lets plugins register streaming host functions.

type TenantPools

type TenantPools struct {
	// Owner pool for administrative operations (claiming, migrations).
	OwnerDB *sql.DB
	// contains filtered or unexported fields
}

TenantPools manages per-tenant database connection pools. Each pool connects directly as the tenant's login role, so there is no SET ROLE / RESET ROLE to escape — the connection IS the tenant.

func NewTenantPools

func NewTenantPools(ownerDB *sql.DB, baseDSN string, maxConns int) *TenantPools

NewTenantPools creates a TenantPools manager. maxConns is the max open connections per tenant pool (0 = default 25). baseDSN is a connection string template like: "host=localhost port=5432 dbname=cleat sslmode=disable" The user and password are added per tenant.

func (*TenantPools) Close

func (tp *TenantPools) Close()

Close closes all tenant pools.

func (*TenantPools) EvictIdle

func (tp *TenantPools) EvictIdle(maxIdle time.Duration) int

EvictIdle closes pools that haven't been used for the given duration. Returns the number of pools evicted.

func (*TenantPools) For

func (tp *TenantPools) For(ctx context.Context, tenantID string) (*sql.DB, error)

For returns a tenant-scoped *sql.DB. Pools are created lazily and cached. Caller does NOT close the returned DB — TenantPools manages the lifecycle.

type TypeDef

type TypeDef struct {
	Type     string              `json:"type" yaml:"type"`
	Fields   map[string]FieldDef `json:"fields,omitempty" yaml:"fields,omitempty"`
	Optional bool                `json:"optional,omitempty" yaml:"optional,omitempty"`
}

TypeDef describes a type in a plugin manifest.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL