plugin

package
v0.3.0-rc.1 Latest Latest
Warning

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

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

Documentation

Overview

Package plugin is the host side of the OSCTF plugin ABI: the go-plugin handshake, the gRPC transport bridge to the generated stubs (pluginpb), and (in later sub-steps) the loader, lifecycle, and registry wiring. Plugins run as separate processes; the host dials a local gRPC server each serves. See docs/v0.3/02-plugin-abi.md and 03-plugin-loader.md.

Index

Constants

View Source
const (
	KeyAuth          = "auth"
	KeyScoring       = "scoring"
	KeyNotification  = "notification"
	KeyChallengeType = "challenge_type"
)

Dispense keys — one per plugin type. A plugin serves exactly the one its manifest `type` declares; the host dispenses that key and receives the matching gRPC client.

View Source
const ABIMajor = 1

ABIMajor is the go-plugin ProtocolVersion — the ABI MAJOR, bumped ONLY on a breaking change. A plugin built against a different major is refused by go-plugin before any call; the loader logs "ABI major mismatch" and skips it (no crash, no partial init).

View Source
const ABIMinor = 0

ABIMinor is the host's ABI minor. Minor is forward-compatible: the host may call a plugin advertising an OLDER minor (it won't invoke methods/fields the plugin lacks), and a plugin advertising a NEWER minor is accepted (the host uses only what it knows). Carried per-plugin in the manifest and the Info RPC.

View Source
const ABIString = "1.0"

ABIString is the host's advertised "major.minor".

View Source
const PluginConfigEnv = "OSCTF_PLUGIN_CONFIG"

PluginConfigEnv is the single environment variable the host sets on the PLUGIN process carrying its resolved config as a JSON object. It is the SHARED definition: the host writes it here and the public SDK's Config() reads it (plugin/sdk imports this const), so the two sides cannot drift — the pairing is one source of truth, not two strings that must agree. One var (not per-key) so a plugin never reconstructs the host's OSCTF_PLUGIN_<NAME>_<KEY> override names — it just asks for a key.

Variables

View Source
var ErrNotReady = errors.New("plugin: not ready")

ErrNotReady is returned by dispatch when a plugin is not in `ready` state (including absent). The type-specific layer above turns it into the per-type answer from the spec: a clear error for auth/challenge-type, an opt-in fallback for scoring, a counted drop for notification — but the plugin PROCESS is never called from a non-ready state.

View Source
var Handshake = goplugin.HandshakeConfig{
	ProtocolVersion:  ABIMajor,
	MagicCookieKey:   "OSCTF_PLUGIN",
	MagicCookieValue: "osctf-plugin-v1",
}

Handshake gates every plugin connection. The magic cookie guards against launching a non-OSCTF binary; ProtocolVersion is the ABI major (a mismatch is refused pre-call).

Functions

func HostPluginSet

func HostPluginSet() goplugin.PluginSet

HostPluginSet is the set the host offers when dialing a plugin. Each entry's Impl is nil on the host side; Dispense returns the generated gRPC client. The SDK builds the mirror set with Impls set (the plugin author's implementation).

Types

type AuthGRPCPlugin

type AuthGRPCPlugin struct {
	goplugin.NetRPCUnsupportedPlugin
	Impl pluginpb.AuthServer
}

AuthGRPCPlugin bridges the Auth service.

func (*AuthGRPCPlugin) GRPCClient

func (*AuthGRPCPlugin) GRPCServer

func (p *AuthGRPCPlugin) GRPCServer(_ *goplugin.GRPCBroker, s *grpc.Server) error

type Caller

type Caller interface {
	Call(ctx context.Context, method string, fn func(ctx context.Context, client any) error) error
}

Caller invokes a method on ONE plugin with the loader's readiness gate and in-flight budget applied — the handle a registered provider (a scoring engine, auth provider, challenge-type checker) uses to reach the plugin process. A call to a non-ready plugin returns ErrNotReady without touching the process, which is the fail-closed layer INSIDE every registered provider.

type ChallengeTypeGRPCPlugin

type ChallengeTypeGRPCPlugin struct {
	goplugin.NetRPCUnsupportedPlugin
	Impl pluginpb.ChallengeTypeServer
}

ChallengeTypeGRPCPlugin bridges the ChallengeType service.

func (*ChallengeTypeGRPCPlugin) GRPCClient

func (*ChallengeTypeGRPCPlugin) GRPCServer

type Config

type Config struct {
	Enabled      bool          // false skips discovery entirely (pure-core mode, == v0.2 behaviour)
	PluginsDir   string        // OSCTF_PLUGINS_DIR — scanned one level deep
	RuntimeDir   string        // OSCTF_RUNTIME_DIR — where pidfiles are written / the boot sweep reads
	PerPluginCap int           // OSCTF_PLUGIN_MAX_INFLIGHT (per plugin)
	GlobalCap    int           // shared fd-accountant grant (across all plugins); 0 = unbounded
	QueueWait    time.Duration // OSCTF_PLUGIN_QUEUE_WAIT
	DrainTimeout time.Duration // OSCTF_PLUGIN_DRAIN_TIMEOUT
	StartTimeout time.Duration // handshake wait before a launch is deemed failed
	HealthStable time.Duration // OSCTF_PLUGIN_HEALTH_STABLE
	MaxAttempts  int           // OSCTF_PLUGIN_RESTART_CAP
	Log          *slog.Logger
	Registrar    Registrar // wires ready plugins into their type registries; nil = no-op (transport only)
}

Config is what the composition root hands the loader at startup. The budget fields come from the shared fd accountant (GlobalCap) + OSCTF_PLUGIN_MAX_INFLIGHT (PerPluginCap); the rest are discovery and supervision knobs.

type ConfigKey

type ConfigKey struct {
	Type     string `yaml:"type"`
	Required bool   `yaml:"required"`
	Secret   bool   `yaml:"secret"`
	Default  string `yaml:"default"`
}

ConfigKey declares one config value's type and resolution rules. Secret keys resolve ONLY from env; a value in the manifest is a validation error (secrets never live in a file the plugin dir might expose).

type Loader

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

Loader discovers, launches, supervises, and routes to plugins. It holds the tracked set under a mutex, dispatches calls only to `ready` plugins, and bounds concurrent host→plugin work with the two-level budget.

func New

func New(cfg Config) *Loader

New builds a loader from Config, wiring the two-level in-flight budget. Discovery and launch happen in Boot; call it in a goroutine so a slow sweep or a plugin that never handshakes cannot hold up serving.

func (*Loader) Boot

func (l *Loader) Boot(ctx context.Context)

Boot reclaims orphaned plugin children, discovers plugins, and launches a supervisor for each. It NEVER gates serving: the sweep runs before any launch (so this run's fresh pidfiles are not mistaken for orphans), discovery is a bounded one-level scan, and each launch is handed to a supervisor goroutine that does the handshake — so a plugin stuck launching, a binary that never handshakes, or a slow sweep leaves the core answering. Missing or empty OSCTF_PLUGINS_DIR is a silent no-op (a default deployment has no plugins and must behave exactly like v0.2). main runs Boot in a goroutine so even a slow sweep does not delay the HTTP server.

func (*Loader) Snapshot

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

Snapshot returns the current status of every tracked plugin (including those quarantined at load), for the admin plugin view. Ordering is by name for a stable admin display.

func (*Loader) Stop

func (l *Loader) Stop(ctx context.Context)

Stop drains and stops every launched plugin, bounded by ctx. main calls it AFTER the HTTP server has drained, so no in-flight request is left calling a plugin whose registry entry has been removed. The ctx is the SHARED shutdown budget covering both the HTTP drain and this — not a second independent timeout; if it expires, Stop returns and the remaining plugins are left to the OS (their pidfiles let the next boot sweep reclaim them).

type Manifest

type Manifest struct {
	Name        string               `yaml:"name"`
	Type        string               `yaml:"type"`
	ABI         string               `yaml:"abi"`
	Version     string               `yaml:"version"`
	Executable  string               `yaml:"executable"`
	Description string               `yaml:"description"`
	Override    bool                 `yaml:"override"` // opt-in to replace a protected built-in key
	Config      map[string]ConfigKey `yaml:"config"`
}

Manifest is a plugin's plugin.yaml. Names map to registry keys (auth provider id, scoring mode, challenge-type id), so identity is validated strictly and a collision fails loudly.

type NotificationGRPCPlugin

type NotificationGRPCPlugin struct {
	goplugin.NetRPCUnsupportedPlugin
	Impl pluginpb.NotificationServer
}

NotificationGRPCPlugin bridges the Notification service.

func (*NotificationGRPCPlugin) GRPCClient

func (*NotificationGRPCPlugin) GRPCServer

type PluginStatus

type PluginStatus struct {
	Name   string
	Type   string
	State  string
	Reason string
}

PluginStatus is one plugin's state for the admin view — enough to answer "why isn't my notifier working?" without reading boot logs. Reason is redacted of secret config values.

type Registrar

type Registrar interface {
	Register(name, ptype string, caller Caller) error
	Deregister(name, ptype string)
}

Registrar wires a ready plugin's provider into its type's registry and reverts it before the plugin dies. The composition root implements it per type (auth/scoring/challenge-type/ notification); the loader calls Register once when a plugin FIRST becomes ready and Deregister once when it terminates — before the process is killed (revert-before-death).

type ScoringGRPCPlugin

type ScoringGRPCPlugin struct {
	goplugin.NetRPCUnsupportedPlugin
	Impl pluginpb.ScoringServer
}

ScoringGRPCPlugin bridges the Scoring service.

func (*ScoringGRPCPlugin) GRPCClient

func (*ScoringGRPCPlugin) GRPCServer

func (p *ScoringGRPCPlugin) GRPCServer(_ *goplugin.GRPCBroker, s *grpc.Server) error

type ShedError

type ShedError struct {
	Plugin     string
	Level      ShedLevel
	RetryAfter time.Duration
}

ShedError is returned when a call is shed at an in-flight cap after waiting the queue budget. It maps to 503 (errors.Is(err, apperr.ErrUnavailable)) and its message names WHICH cap fired, so the operator log distinguishes the two causes.

func (*ShedError) Error

func (e *ShedError) Error() string

func (*ShedError) Is

func (e *ShedError) Is(target error) bool

Is lets the existing 503 mapping treat a shed as Unavailable without importing this package.

type ShedLevel

type ShedLevel int

ShedLevel says which in-flight cap shed a call — the distinction an operator needs, since "one plugin at its own cap" and "all plugins exhausting the shared budget" call for different responses (throttle/replace that plugin vs. raise the global budget / add capacity).

const (
	ShedPerPlugin ShedLevel = iota // one plugin hit OSCTF_PLUGIN_MAX_INFLIGHT
	ShedGlobal                     // the shared budget (all plugins) is exhausted
)

type State

type State string

State is the lifecycle state of a tracked plugin. Every plugin is in exactly one; the full state machine is specified in docs/v0.3/03-plugin-loader.md before the loader is built, because process supervision is a new concurrency surface and the last two releases shipped bugs of exactly this shape.

const (
	StateDiscovered State = "discovered" // manifest found + validated; not launched
	StateLaunching  State = "launching"  // process started; handshake+Info+Configure in progress
	StateReady      State = "ready"      // launched, configured, registered, health passing — SERVES
	StateUnhealthy  State = "unhealthy"  // was ready; a health check or call failed; supervisor deciding
	StateRestarting State = "restarting" // being torn down + relaunched; backoff between attempts
	StateFailed     State = "failed"     // restart cap exhausted OR failed to load (e.g. invalid config) → quarantined; not retried automatically
	StateDraining   State = "draining"   // reload/shutdown; no new calls, in-flight allowed to finish
	StateStopped    State = "stopped"    // process exited, resources reclaimed, entry removed — terminal
)

Directories

Path Synopsis
Package plugintest provides the hostile plugin doubles and the build/dial harness they are exercised through.
Package plugintest provides the hostile plugin doubles and the build/dial harness they are exercised through.
doubles/configecho command
Double: reads its config through the PUBLIC sdk.Config() and reflects it in Value — so the host→plugin config path (OSCTF_PLUGIN_CONFIG env → sdk.Config) can be asserted end to end.
Double: reads its config through the PUBLIC sdk.Config() and reflects it in Value — so the host→plugin config path (OSCTF_PLUGIN_CONFIG env → sdk.Config) can be asserted end to end.
doubles/crashafter command
Double: CRASHES AFTER SERVING — handshakes and answers Info, then exits non-zero on the first Value call.
Double: CRASHES AFTER SERVING — handshakes and answers Info, then exits non-zero on the first Value call.
doubles/crashlaunch command
Double: CRASH ON LAUNCH — exits non-zero before serving, every time.
Double: CRASH ON LAUNCH — exits non-zero before serving, every time.
doubles/goodscore command
Double: well-behaved baseline.
Double: well-behaved baseline.
doubles/hang command
Double: HANG — Value never returns (and ignores the context).
Double: HANG — Value never returns (and ignores the context).
doubles/ignoreshutdown command
Double: IGNORES SHUTDOWN — serves correctly but traps and ignores SIGINT/SIGTERM, so a graceful stop does not make it exit.
Double: IGNORES SHUTDOWN — serves correctly but traps and ignores SIGINT/SIGTERM, so a graceful stop does not make it exit.
doubles/logecho command
Double: logs via the PUBLIC sdk.Log() on each call, so the plugin→host log path (over go-plugin's stderr channel into the host Logger) can be asserted end to end.
Double: logs via the PUBLIC sdk.Log() on each call, so the plugin→host log path (over go-plugin's stderr channel into the host Logger) can be asserted end to end.
doubles/malformed command
Double: MALFORMED — serves Info fine but returns a gRPC error status on Value (and an out-of-contract Info name mismatch is available via NAME).
Double: MALFORMED — serves Info fine but returns a gRPC error status on Value (and an out-of-contract Info name mismatch is available via NAME).
doubles/nohandshake command
Double: NEVER HANDSHAKES — a valid executable that starts and then blocks forever without calling plugin.Serve, so the go-plugin handshake never completes and the loader's launch is stuck until its StartTimeout.
Double: NEVER HANDSHAKES — a valid executable that starts and then blocks forever without calling plugin.Serve, so the go-plugin handshake never completes and the loader's launch is stuck until its StartTimeout.
doubles/slow command
Double: SLOW — responds correctly, every time, in 4 seconds, and deliberately IGNORES the request context.
Double: SLOW — responds correctly, every time, in 4 seconds, and deliberately IGNORES the request context.
doubles/slowcoop command
Double: SLOW BUT COOPERATIVE — a long call like `slow`, except it HONORS ctx cancellation: on cancel it returns promptly with codes.Canceled instead of running to completion.
Double: SLOW BUT COOPERATIVE — a long call like `slow`, except it HONORS ctx cancellation: on cancel it returns promptly with codes.Canceled instead of running to completion.
doubles/slowshutdown command
Double: SLOW ONLY ON SHUTDOWN — responds normally, but on a stop signal takes far longer than the 30s drain window to exit.
Double: SLOW ONLY ON SHUTDOWN — responds normally, but on a stop signal takes far longer than the 30s drain window to exit.
doubles/wrongabi command
Double: WRONG ABI MAJOR — serves correctly but with a go-plugin ProtocolVersion the host does not speak.
Double: WRONG ABI MAJOR — serves correctly but with a go-plugin ProtocolVersion the host does not speak.

Jump to

Keyboard shortcuts

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