plugins

package
v0.1.5 Latest Latest
Warning

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

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

Documentation

Overview

Package plugins launches Patchcord plugin processes and performs the protocol handshake described in the vision document (section 8.3).

Index

Constants

View Source
const CurrentProtocolVersion uint32 = 1

CurrentProtocolVersion is the highest plugin protocol version this agent speaks. It is sent in every HandshakeRequest.

View Source
const DefaultReadyTimeout = 10 * time.Second

DefaultReadyTimeout bounds how long Launch waits for a plugin process to report its listen address before giving up.

View Source
const PackageExtension = ".patchcord-plugin"

PackageExtension is the conventional file extension for a plugin package produced by Pack (vision document, section 9.1: ".patchcord-plugin"). Install only distinguishes a package from a raw executable by sniffing its content, not this extension — see internal/cli/plugin.go.

View Source
const PackageManifestFileName = "manifest.json"

PackageManifestFileName is the file a plugin package's source directory must contain at its root (vision document, section 9.1).

Variables

View Source
var ErrInvalidPackageManifest = errors.New("invalid plugin package manifest")

ErrInvalidPackageManifest is returned by ParsePackageManifest and LoadPackageManifest when the manifest is malformed or missing a required field.

View Source
var ErrNotInstalled = errors.New("plugin not installed")

ErrNotInstalled is returned by Get and Uninstall when no plugin with the given id is in the catalog.

Functions

func ExecuteAction

func ExecuteAction(ctx context.Context, client pluginv1.PluginServiceClient, action string, input map[string]any, connector *connectors.ResolvedConnector) (map[string]any, error)

ExecuteAction calls the plugin's ExecuteAction RPC for the given action id, passing input as its arguments and connector as the resolved connector bound to it (nil if none), and returns its output.

func KnownActions

func KnownActions(ctx context.Context, db *sql.DB) (map[string]struct{}, error)

KnownActions returns the set of action identifiers contributed by every installed plugin, for the workflow compiler (internal/workflow.Validate) to check a workflow's steps against.

func KnownConnectorTypes

func KnownConnectorTypes(ctx context.Context, db *sql.DB) (map[string]struct{}, error)

KnownConnectorTypes returns the set of connector type identifiers contributed by every installed plugin, for internal/connectors.Create to check a new connector's --type against — the same role KnownActions plays for the workflow compiler.

func Pack

func Pack(sourceDir string, key ed25519.PrivateKey, w io.Writer) error

Pack archives sourceDir (which must contain a valid manifest.json, vision document section 9.1) into w as a gzip-compressed tar stream, plus a checksums.json covering it (see internal/packaging.SignedArchive). If key is non-nil, the package is also signed — key == nil (no --sign-key) produces a package with integrity data but no provenance. The result is what InstallPackage (and therefore `patchcord plugin install`) expects.

Only regular files and directories are supported; sourceDir must not contain symlinks or other special entries.

func Scaffold

func Scaffold(dir, id, version string) error

Scaffold writes a minimal Go plugin (main.go, one example action) and a manifest.json declaring an executable for the current platform, into dir — enough to `go build` then `plugin pack` without hand-editing the manifest. It returns an error if dir already exists and is not empty.

func SeedEmbedded added in v0.1.2

func SeedEmbedded(ctx context.Context, db *sql.DB, dataDir string, logger *slog.Logger) error

SeedEmbedded installs Patchcord's bundled reference plugins (package embedded — text, json, encoding, http, time) into db's catalog the first time it is called for a given database. Every call after that, for that database, is a no-op: once seeded, an embedded plugin the user has since `plugin uninstall`ed stays uninstalled — this never reinstalls one behind their back, and it never upgrades one already present.

Call it once, right before Supervisor.Start, from anywhere that starts a plugin supervisor (`patchcord serve`/`dev` via internal/runtime.NewAgent, `workflow run`, `connector test`) — see ADR-0059.

A plugin that fails to extract or install is logged and skipped, exactly like a plugin Supervisor.Start itself fails to launch: a bundled plugin must never be able to fail agent or command startup. On a build where the embedding step never ran (e.g. a bare `go build` on a fresh checkout), package embedded reports no files and SeedEmbedded simply records itself as seeded with nothing to install.

func TestConnector

func TestConnector(ctx context.Context, client pluginv1.PluginServiceClient, connector *connectors.ResolvedConnector) (ok bool, message string, err error)

TestConnector calls the plugin's TestConnector RPC for connector and reports whether it succeeded. ok/message is the connector test's own outcome (a failed attempt is a legitimate result); the returned error is reserved for a real RPC-level failure — the plugin not supporting connector testing (codes.Unimplemented), a transport error, or ctx being cancelled.

func Uninstall

func Uninstall(ctx context.Context, db *sql.DB, id string) error

Uninstall removes a plugin from the catalog. It returns ErrNotInstalled if no plugin with that id is in the catalog.

Types

type CatalogEntry

type CatalogEntry struct {
	PluginID        string
	Version         string
	ExecutablePath  string
	ProtocolVersion uint32
	Connectors      []string
	Actions         []string
	Permissions     []string
	InstalledAt     time.Time
}

CatalogEntry is one plugin recorded in the agent's catalog, as returned by its handshake at install time.

func Get

func Get(ctx context.Context, db *sql.DB, id string) (*CatalogEntry, error)

Get returns one installed plugin by id. It returns ErrNotInstalled if no plugin with that id is in the catalog.

func Install

func Install(ctx context.Context, db *sql.DB, path string) (*CatalogEntry, error)

Install launches the plugin binary at path, completes the handshake to validate it and discover its manifest, then records it in the catalog. Installing a plugin whose id is already present replaces its entry.

path is resolved to an absolute path before it is recorded: the catalog entry must remain launchable by the Supervisor regardless of the working directory `patchcord serve` (or any other command that starts plugins) is later run from, which is almost never the directory `plugin install` was run from.

func InstallPackage

func InstallPackage(ctx context.Context, db *sql.DB, dataDir, packagePath string, requireSignature bool) (*CatalogEntry, trust.PolicyResult, error)

InstallPackage installs a plugin from a .patchcord-plugin archive (Pack's output). Its contents are extracted under dataDir/plugins/<id>/<version> — a location the agent owns for as long as the plugin stays installed — then the executable matching the current platform (runtime.GOOS+"-"+ runtime.GOARCH) is selected, made executable, and handed to the existing Install, which launches it, completes the handshake, and records it in the catalog exactly as it does for a raw executable path today.

The package is verified (internal/packaging.Verify) before anything is installed: a checksum mismatch or an invalid signature aborts unconditionally. requireSignature additionally rejects a package that is unsigned, or signed by a key not trusted for its id (internal/trust) — when false, InstallPackage still returns the verification outcome so the caller can warn about either case instead of failing outright.

It returns an error wrapping ErrInvalidPackageManifest if the packaged manifest is malformed, or a plain error if the package declares no executable for the current platform or requires a protocol version this agent does not support.

func List

func List(ctx context.Context, db *sql.DB) ([]CatalogEntry, error)

List returns every installed plugin, ordered by plugin id.

type Manifest

type Manifest struct {
	ProtocolVersion uint32
	PluginID        string
	PluginVersion   string
	Connectors      []string
	Actions         []string
	Permissions     []string
}

Manifest is what a plugin declares about itself during the handshake.

func Handshake

func Handshake(ctx context.Context, client pluginv1.PluginServiceClient) (*Manifest, error)

Handshake calls the plugin's Handshake RPC, validates its response, and returns the resulting manifest.

It takes an already-connected client rather than launching a process itself, which keeps the negotiation logic testable against an in-memory gRPC server instead of a real plugin binary (see handshake_test.go).

type PackageManifest

type PackageManifest struct {
	SchemaVersion   int
	ID              string
	Version         string
	ProtocolVersion uint32
	Permissions     []string
	// Executables maps a "GOOS-GOARCH" platform key (e.g. "darwin-arm64",
	// matching runtime.GOOS+"-"+runtime.GOARCH) to the executable's path
	// relative to the package root.
	Executables map[string]string
}

PackageManifest is the parsed content of a .patchcord-plugin package's manifest.json — declared statically, before the plugin process is ever launched, so its id, version and permissions can be shown to the user (vision document, section 9.2, step 5) and the right platform executable can be selected (step 7). It is distinct from Manifest (handshake.go), which a running plugin process returns over RPC once launched and remains the source of truth for the actions/connectors it actually contributes.

func LoadPackageManifest

func LoadPackageManifest(dir string) (*PackageManifest, error)

LoadPackageManifest reads and parses dir's manifest.json.

func ParsePackageManifest

func ParsePackageManifest(source []byte) (*PackageManifest, error)

ParsePackageManifest parses and validates a plugin package manifest from its JSON source, returning ErrInvalidPackageManifest if a required field is missing, empty, or malformed.

type Process

type Process struct {
	Client       pluginv1.PluginServiceClient
	HealthClient grpc_health_v1.HealthClient
	// contains filtered or unexported fields
}

Process is a running plugin subprocess the agent has connected to.

Its lifetime is independent of the context passed to Launch: that context only bounds the launch attempt itself (waiting for the plugin to report ready). Once Launch returns successfully, the process keeps running until Close is called or it exits on its own — which is exactly what the Plugin Supervisor needs to detect and react to a crash.

func Launch

func Launch(ctx context.Context, path string, readyTimeout time.Duration) (*Process, error)

Launch starts the plugin binary at path, waits for it to report its gRPC listen address on stdout, and dials it. It does not perform the protocol handshake itself; call Handshake with the returned Process's Client.

ctx only bounds this launch attempt: if it is cancelled before the plugin reports ready, the partially-started process is killed and Launch returns ctx's error. It has no effect on the process once Launch has returned successfully.

func (*Process) Close

func (p *Process) Close(ctx context.Context) error

Close closes the connection to the plugin, terminates its process if it isn't already gone, and waits for it to exit, bounded by ctx.

Closing the connection alone would not be enough: the plugin's gRPC server keeps running regardless of whether a client is attached, so the process must be killed explicitly.

func (*Process) ExitErr

func (p *Process) ExitErr() error

ExitErr reports why the process exited. It is only meaningful once the channel returned by Exited is closed.

func (*Process) Exited

func (p *Process) Exited() <-chan struct{}

Exited returns a channel that is closed once the plugin process has exited, whether cleanly (via Close) or unexpectedly (a crash). Use ExitErr after it closes to find out which.

type Supervisor

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

Supervisor launches every plugin recorded in the catalog and keeps them running for as long as the agent does: it detects crashes and unresponsive plugins via periodic health checks, restarts them up to a bounded number of attempts, and quarantines (stops retrying) a plugin that keeps failing. Once started, it also serves as the agent's entry point for actually invoking an action (ExecuteAction), routing the call to whichever running plugin currently contributes it.

A plugin failure — however it manifests — is always contained here and never propagated to the agent: the non-negotiable that a crashed plugin must never take the agent down holds because the Supervisor is the only thing watching plugin processes.

func NewSupervisor

func NewSupervisor(cfg SupervisorConfig, logger *slog.Logger) *Supervisor

NewSupervisor creates a Supervisor. Call Start to launch the catalog's plugins and begin supervising them.

func (*Supervisor) ExecuteAction

func (s *Supervisor) ExecuteAction(ctx context.Context, actionID string, input map[string]any, connector *connectors.ResolvedConnector) (map[string]any, error)

ExecuteAction runs actionID on whichever currently running plugin contributes it, passing connector as the resolved connector bound to it (nil if none). It implements internal/runs's ActionExecutor interface, letting the workflow runner invoke actions without knowing anything about plugin processes, transport, or supervision.

func (*Supervisor) Start

func (s *Supervisor) Start(ctx context.Context, db *sql.DB) error

Start launches every plugin in the catalog and begins supervising them. A plugin that fails to launch is logged and skipped, exactly like a plugin that exhausts its restart attempts: Start itself never fails because of a plugin.

func (*Supervisor) Stop

func (s *Supervisor) Stop(ctx context.Context)

Stop stops supervising every plugin and terminates them, bounded by ctx. It waits for all supervision goroutines to finish before returning.

func (*Supervisor) TestConnector

func (s *Supervisor) TestConnector(ctx context.Context, connector *connectors.ResolvedConnector) (ok bool, message string, err error)

TestConnector attempts to reach the external system connector describes, via whichever currently running plugin declares its type. ok/message is the connector test's own outcome — the returned error means the test could not even be attempted (no running plugin declares that connector type, or the plugin that does returns codes.Unimplemented because it doesn't support testing).

type SupervisorConfig

type SupervisorConfig struct {
	// HealthCheckInterval is how often a running plugin's health is
	// checked. Defaults to 10s when zero.
	HealthCheckInterval time.Duration
	// HealthCheckTimeout bounds each individual health check call.
	// Defaults to 2s when zero.
	HealthCheckTimeout time.Duration
	// MaxRestarts is how many times a plugin is relaunched after a crash
	// or a failed health check before it is quarantined. Defaults to 3
	// when zero; a negative value disables restarts entirely.
	MaxRestarts int
	// RestartDelay is the fixed delay observed before each restart
	// attempt. Defaults to 1s when zero.
	RestartDelay time.Duration
}

SupervisorConfig controls the Plugin Supervisor's health check and restart policy (vision document, section 8.4).

Directories

Path Synopsis
Package embedded holds Patchcord's bundled reference plugins — text, json, encoding, http and time (plugins/examples/*, chosen because none of them has a concrete external service behind it) — as prebuilt executables embedded straight into the patchcord binary for the platform it was built for.
Package embedded holds Patchcord's bundled reference plugins — text, json, encoding, http and time (plugins/examples/*, chosen because none of them has a concrete external service behind it) — as prebuilt executables embedded straight into the patchcord binary for the platform it was built for.

Jump to

Keyboard shortcuts

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