Documentation
¶
Overview ¶
Package plugins launches Patchcord plugin processes and performs the protocol handshake described in the vision document (section 8.3).
Index ¶
- Constants
- Variables
- func ExecuteAction(ctx context.Context, client pluginv1.PluginServiceClient, action string, ...) (map[string]any, error)
- func KnownActions(ctx context.Context, db *sql.DB) (map[string]struct{}, error)
- func KnownConnectorTypes(ctx context.Context, db *sql.DB) (map[string]struct{}, error)
- func Pack(sourceDir string, key ed25519.PrivateKey, w io.Writer) error
- func Scaffold(dir, id, version string) error
- func TestConnector(ctx context.Context, client pluginv1.PluginServiceClient, ...) (ok bool, message string, err error)
- func Uninstall(ctx context.Context, db *sql.DB, id string) error
- type CatalogEntry
- func Get(ctx context.Context, db *sql.DB, id string) (*CatalogEntry, error)
- func Install(ctx context.Context, db *sql.DB, path string) (*CatalogEntry, error)
- func InstallPackage(ctx context.Context, db *sql.DB, dataDir, packagePath string, ...) (*CatalogEntry, trust.PolicyResult, error)
- func List(ctx context.Context, db *sql.DB) ([]CatalogEntry, error)
- type Manifest
- type PackageManifest
- type Process
- type Supervisor
- func (s *Supervisor) ExecuteAction(ctx context.Context, actionID string, input map[string]any, ...) (map[string]any, error)
- func (s *Supervisor) Start(ctx context.Context, db *sql.DB) error
- func (s *Supervisor) Stop(ctx context.Context)
- func (s *Supervisor) TestConnector(ctx context.Context, connector *connectors.ResolvedConnector) (ok bool, message string, err error)
- type SupervisorConfig
Constants ¶
const CurrentProtocolVersion uint32 = 1
CurrentProtocolVersion is the highest plugin protocol version this agent speaks. It is sent in every HandshakeRequest.
const DefaultReadyTimeout = 10 * time.Second
DefaultReadyTimeout bounds how long Launch waits for a plugin process to report its listen address before giving up.
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.
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 ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 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.
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 ¶
Get returns one installed plugin by id. It returns ErrNotInstalled if no plugin with that id is in the catalog.
func Install ¶
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.
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.
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 ¶
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 ¶
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 ¶
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.
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 ¶
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).