plugin

package
v0.10.1 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package plugin hosts external database driver plugins: child processes speaking the perk/v1 JSON-RPC stdio protocol. The Loader spawns and handshakes each plugin, registers a database.Shim for it, and routes every driver operation through a bounded concurrent RPC client to the child process.

Index

Constants

View Source
const (
	PhaseResolve    = "resolve"
	PhaseInitialize = "initialize"
	PhaseProtocol   = "protocol"
	PhaseRegister   = "register"
	PhaseShutdown   = "shutdown"
	PhaseOK         = "ok"
)

Lifecycle phase names, stable in JSON and human output. They are the shared vocabulary of the inspect lifecycle, reused by the CLI reports and the TUI trust preview.

View Source
const MaxFrameBytes = 16 << 20

MaxFrameBytes bounds one protocol frame — a UTF-8 JSON object plus a trailing newline — on the wire. A frame that does not fit is oversized and terminates the child.

View Source
const ProtocolVersion = 1

ProtocolVersion is the perk/v1 wire protocol version this host speaks. A plugin whose initialize result carries a different version is rejected at handshake, before registration.

View Source
const RPCErrorCanceled = -32800

RPCErrorCanceled is the perk/v1 error code for a canceled operation; the host maps it exactly to context.Canceled.

Variables

This section is empty.

Functions

func IsTerminal

func IsTerminal(err error) bool

IsTerminal reports whether err is a plugin-terminal failure (child exit or protocol death) rather than an operation error.

func ResolveExecutable

func ResolveExecutable(entry, configPath string) (string, error)

ResolveExecutable maps a plugin entry to its canonical executable path with the exact startup resolution and allowlist, without spawning anything: bare names resolve through PATH, entries with a path separator resolve relative to the config file's directory (or the working directory when configPath is empty, for explicit operands), and the result must be a regular file with at least one executable permission bit. It is the narrow exported face of the loader's resolver so CLI tooling never duplicates the rules.

func SHA256File

func SHA256File(path string) (string, error)

SHA256File computes the lowercase hex SHA-256 digest of the file at path, streaming its bytes. It is the canonical executable fingerprint of the trust model: `plugin add --approve` pins this digest, startup verifies freshly computed digests against the pin before spawning, and every report exposes it for the user to compare.

Types

type Client

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

Client is a bounded concurrent JSON-RPC client for one plugin child. Requests and cancel notifications share one write mutex; responses are routed to pending calls by id; any protocol violation or child death terminates the client, failing every pending call.

func (*Client) Call

func (c *Client) Call(ctx context.Context, method string, params any, result any) error

Call performs one request and unmarshals the result into result. The caller's context cancels the operation: a perk/v1/cancel notification is sent to the plugin and a late response is discarded. Result-shape mismatches are operation errors, never terminal.

func (*Client) Close

func (c *Client) Close() error

Close shuts the child down: stdin closes (EOF), the reader is given up to 5 seconds to reap the process, then the process is killed and reaped forcibly. Idempotent — the second call returns nil. Pending calls fail with the terminal error. Safe after the child already exited.

func (*Client) SetPlugin

func (c *Client) SetPlugin(name string)

SetPlugin records the plugin's self-claimed identity once the initialize handshake succeeds. The Loader calls it so operation errors carry host-known provenance; protocol behavior never depends on it. Safe to call any time; the last write wins.

func (*Client) Snapshot

func (c *Client) Snapshot() Snapshot

Snapshot returns an immutable copy of the client's diagnostics. It never blocks on protocol I/O; the stderr tail is copied under the drain lock only.

type Error

type Error struct {
	Code    int
	Message string
	Kind    Kind
	Plugin  string
	Method  string
	// Hint explains the failure; empty when the plugin sent none.
	Hint string
	// SuggestedStatement is a statement the user may try instead;
	// empty when the plugin sent none. Advisory only — never executed
	// by the host.
	SuggestedStatement string
}

Error is a structured plugin operation error with stable provenance: the JSON-RPC code and message, the normalized Kind, and the host-side Method and Plugin identity. Operation errors are never terminal client failures; inspect the fields with errors.As. Hint and SuggestedStatement are optional advisory guidance carried verbatim from the plugin's error data: the host renders them separately from the error (never merged into Error's text) and never executes a suggested statement.

func (*Error) Error

func (e *Error) Error() string

Error renders the concise stable operation-error text. Method constants already carry the perk/v1 prefix, so the method is rendered exactly once.

type InspectResult

type InspectResult struct {
	// Path is the canonical executable path once resolution succeeded.
	Path string
	// Capabilities is the driver advertisement once the initialize
	// handshake succeeded, or nil when the handshake failed.
	Capabilities *database.Capabilities
	// Snapshot is the final diagnostic snapshot, taken after the child
	// was closed: canonical path, init duration, exit/running state, and
	// the bounded stderr tail. Nil when the child never spawned.
	Snapshot *Snapshot
	// Phase is the failing lifecycle phase — resolve, initialize,
	// protocol, register, or shutdown — or PhaseOK when every phase
	// passed.
	Phase string
	// Error is the failure text when Phase is not PhaseOK.
	Error string
}

InspectResult is the outcome of one inspect lifecycle: the resolved canonical path, the driver advertisement, the final diagnostic snapshot, and — when the lifecycle failed — the failing phase and its error text.

func Inspect

func Inspect(ctx context.Context, entry, configPath string) InspectResult

Inspect runs one plugin through the full resolve, initialize and registration-validation, shutdown lifecycle with its own Loader, so items never mutate or contaminate each other or the global driver registry. configPath is the config file path used to resolve relative entries ("" for explicit operands, which resolve against the working directory). Registration validation uses the side-effect-free database.ValidateShim — no global driver is ever installed. The snapshot is taken after Loader.Close so it reflects the final exit/running state, and it remains available because the loader retains its clients.

type Kind

type Kind string

Kind classifies a plugin operation error. It is the Go-side mirror of the wire's error data.kind and of the Node SDK's ErrorKind constants.

const (
	KindValidation     Kind = "validation"
	KindAuthentication Kind = "authentication"
	KindConnection     Kind = "connection"
	KindOperation      Kind = "operation"
	KindUnsupported    Kind = "unsupported"
	KindCancelled      Kind = "cancelled"
	KindProtocol       Kind = "protocol"
	KindPluginCrash    Kind = "plugin_crash"
)

Stable operation-error kinds. Unknown or blank kinds normalize to KindOperation.

type Loader

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

Loader owns the lifecycle of every configured plugin entry: the spawned child, the registered shim, and every session opened in them. Close is the single idempotent cleanup path. Entries rejected at load (resolution, pin drift, handshake, protocol, or registration failure) are retained with their failure so they stay inspectable and restartable; Restart recovers exactly one entry.

func Load

func Load(ctx context.Context, configPath string, entries []string, register func(database.Shim) error) (*Loader, []error)

Load resolves, spawns, handshakes, and registers one plugin per entry, in order: resolve, dedupe, spawn, initialize, register. Failures are nonfatal — each rejected entry contributes one error and later entries still load. The returned Loader owns every successfully spawned child (children rejected at handshake or registration are terminated immediately) and must be closed by the caller.

func LoadPinned

func LoadPinned(ctx context.Context, configPath string, entries []string, trust map[string]string, register func(database.Shim) error) (*Loader, []error)

LoadPinned is Load with per-entry trust verification: immediately before each child would be spawned, the entry's canonical path is looked up in trust and the configured SHA-256 digest is verified against the current bytes. A pinned entry whose digest cannot be computed or does not match is refused at that point — the child never executes — and contributes one error naming the entry with the expected and actual digests; later entries still load. Entries without a trust record load unpinned for compatibility.

func (*Loader) Close

func (l *Loader) Close() error

Close shuts down every live session (their idempotent Close sends the close RPC), then closes every plugin child. Idempotent: the second call returns nil. Safe after children have exited and while calls are still pending — pending calls fail with the terminal error. The client references are retained after Close so Snapshots keeps reporting final diagnostics; Client.Close is idempotent, so a later Close never touches them again.

func (*Loader) EntryForService

func (l *Loader) EntryForService(service sharedsql.Service) (string, bool)

EntryForService reports the configured entry text of the plugin child backing service, or "" when the service is not a live session of this loader's current client generation. Old generations (sessions opened before a restart) are deliberately not matched: they fail deterministically and a restart of their entry would recover a fresh connection, not this one.

func (*Loader) Restart

func (l *Loader) Restart(ctx context.Context, identifier string) error

Restart recovers exactly one configured entry — loaded, rejected, or crashed — identified by its configured entry text or canonical path. The pin is re-verified immediately before the replacement spawns (drift fails closed and nothing executes); the old child is closed and reaped, the replacement is initialized and validated, and the client used by future session opens is swapped atomically — the global driver registration is never touched. Sessions opened before the restart keep their old client generation and fail deterministically rather than jumping to the replacement. A failed restart leaves the previous state intact and the entry's failure text updated.

Restart is safe to call concurrently with Statuses, other Restarts of other entries, and Close. Close racing Restart wins: the restart aborts at the swap point and its replacement child is closed, so no child is ever left behind. Restarts of the same entry are serialized.

func (*Loader) Snapshots

func (l *Loader) Snapshots() []Snapshot

Snapshots returns one immutable diagnostic snapshot per spawned child, in load order. Safe to call at any time, including after Close: the loader retains every client reference solely so final diagnostics stay inspectable. Each snapshot is a fresh copy — mutating a returned Snapshot or its Stderr slice never affects the loader or its children.

func (*Loader) Statuses

func (l *Loader) Statuses() []Status

Statuses returns one immutable status per configured entry, in config order — including entries rejected at load and entries whose child crashed. Safe to call at any time, including during Restart and after Close; status reads never spawn, mutate, or exchange protocol traffic.

type Snapshot

type Snapshot struct {
	Path            string        `json:"path"`             // canonical executable path
	PID             int           `json:"pid"`              // child pid; 0 once reaped
	Plugin          string        `json:"plugin"`           // self-claimed name after the handshake
	ProtocolVersion int           `json:"protocol_version"` // perk/v1 version claimed at the last successful handshake; 0 before it
	InitDuration    time.Duration `json:"init_duration"`    // initialize RPC duration once Load completes
	InFlight        int           `json:"in_flight"`        // pending requests at snapshot time
	Error           string        `json:"error"`            // terminal protocol/process error text when present
	ExitStatus      int           `json:"exit_status"`      // exit code once reaped; -1 while running or signal-killed
	Running         bool          `json:"running"`          // child process not yet reaped
	Stderr          []string      `json:"stderr"`           // newest bounded diagnostics lines/tail
}

Snapshot is an immutable point-in-time view of one plugin child's diagnostics, suitable for CLI inspection. Snapshot() returns fresh copies; mutating the returned Stderr slice never affects the client. A snapshot exposes process and diagnostics state only — never protocol traffic: stdin/stdout frames, connection targets, form values, credentials, and statements are not retained anywhere.

type Status

type Status struct {
	// Entry is the configured entry text (relative or bare name) whose
	// canonical path resolves to Path.
	Entry string `json:"entry"`
	// Path is the canonical executable path; "" when resolution has
	// never succeeded.
	Path string `json:"path"`
	// Plugin is the host-known identity claimed at the last successful
	// initialize handshake; "" before any successful handshake.
	Plugin string `json:"plugin"`
	// ProtocolVersion is the perk/v1 protocol version negotiated at the
	// last successful handshake; 0 when no handshake has succeeded.
	ProtocolVersion int `json:"protocol_version"`
	// Trusted reports whether the entry is pinned in the config trust
	// map; Fingerprint is the configured sha256 pin.
	Trusted     bool   `json:"trusted"`
	Fingerprint string `json:"fingerprint,omitempty"`
	// PID is the current child's pid; 0 once reaped.
	PID int `json:"pid"`
	// Running reports whether the current child is not yet reaped.
	Running bool `json:"running"`
	// ExitStatus is the current child's exit code once reaped; -1 while
	// running or signal-killed.
	ExitStatus int `json:"exit_status"`
	// InitDuration is the last initialize RPC duration, on success or
	// failure; 0 when no handshake has completed.
	InitDuration time.Duration `json:"init_duration"`
	// InFlight is the number of pending requests on the current child at
	// status time.
	InFlight int `json:"in_flight"`
	// Error is the last terminal/structured failure: the load rejection
	// or restart failure text, or the current child's terminal error.
	Error string `json:"error,omitempty"`
	// Stderr is the current child's newest bounded diagnostics tail.
	Stderr []string `json:"stderr"`
}

Status is one configured plugin entry's live state, read without spawning, mutating, or blocking on protocol I/O. It carries the configured entry text and canonical path, the host-known plugin identity, the perk protocol version negotiated at the last successful handshake, the trust state (configured fingerprint), the child's process and exit state, initialize duration, in-flight count, the last terminal/structured failure, and the bounded stderr tail. Every field is a fresh copy; mutating a returned Status or its Stderr slice never affects the loader or its children.

type TerminalError

type TerminalError struct{ Err error }

TerminalError marks a plugin-terminal failure: the child exited or the perk/v1 protocol died. Every pending call fails with it and later calls return it immediately, so callers can distinguish a dead transport from an operation error (plugin.Error). Detect with errors.As or the IsTerminal helper; the wrapped error's text is unchanged.

func (*TerminalError) Error

func (e *TerminalError) Error() string

func (*TerminalError) Unwrap

func (e *TerminalError) Unwrap() error

Directories

Path Synopsis
Package conformance runs the perk/v1 protocol conformance suite against one external plugin executable, outside Go's unit-test harness: fixture-driven protocol cases and generated transport cases, each in a fresh child spoken to as raw NDJSON-RPC on stdio.
Package conformance runs the perk/v1 protocol conformance suite against one external plugin executable, outside Go's unit-test harness: fixture-driven protocol cases and generated transport cases, each in a fresh child spoken to as raw NDJSON-RPC on stdio.

Jump to

Keyboard shortcuts

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