plugin

package
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Jul 10, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Index

Constants

View Source
const (
	TypeSystem = "system"
	TypeWasm   = "wasm"
	TypeWorker = "worker"

	RiskRead  = "read"
	RiskWrite = "write"
	RiskHost  = "host"
)
View Source
const (
	RuntimeStateArmed   = "armed"
	RuntimeStateStopped = "stopped"
	RuntimeStateFailed  = "failed"
)

Variables

View Source
var (
	// ErrCapabilityDenied is returned when a plugin calls a host API without
	// declaring the matching capability in its verified manifest.
	ErrCapabilityDenied = errors.New("plugin capability denied")
	// ErrHostServiceUnavailable is returned when the capability is granted but
	// the server did not wire the corresponding host service into the broker.
	ErrHostServiceUnavailable = errors.New("plugin host service unavailable")
)
View Source
var (
	// ErrRPCNoService is returned when no plugin has registered the target service.
	ErrRPCNoService = errors.New("rpc service not found")
	// ErrRPCNoMethod is returned when the target service does not expose the method.
	ErrRPCNoMethod = errors.New("rpc method not found")
	// ErrRPCDenied is returned when the caller is not on the service's directed
	// allow-list (and is not the service owner). Distinct from "not found" so the
	// broker can record it as a security deny event.
	ErrRPCDenied = errors.New("rpc call denied")
	// ErrRPCInvalid is returned for a malformed service registration.
	ErrRPCInvalid = errors.New("invalid rpc registration")
)

Inter-plugin RPC (design-09 §F). The registry is the server-owned bus that makes one first-party plugin's logic callable from another without exposing raw handles: a plugin with rpc:expose registers a service; a plugin with rpc:call invokes it through the broker. Every call is capability-gated at the broker, authorized here by a directed caller->callee allow-list, and audited.

View Source
var ErrCircuitOpen = errors.New("plugin circuit breaker open")

ErrCircuitOpen is returned once a plugin has failed CrashThreshold times in a row. The operator must disable+re-enable (restart) the plugin to reset it; a flapping plugin cannot keep consuming resources.

Functions

func CapabilityList

func CapabilityList() []string

func CapabilityRisk added in v0.2.0

func CapabilityRisk(cap string) (string, bool)

func DigestSHA256 added in v0.2.0

func DigestSHA256(artifact []byte) string

func SigningPayload added in v0.2.0

func SigningPayload(m Manifest) []byte

func ValidateManifest

func ValidateManifest(m Manifest) error

func VerifyManifest added in v0.2.0

func VerifyManifest(m Manifest, artifact []byte, policy TrustPolicy) error

Types

type Broker added in v0.2.0

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

Broker is the capability-scoped facade a verified plugin uses to call back into server-owned host services.

func NewBroker added in v0.2.0

func NewBroker(loaded Loaded, services HostServices) (*Broker, error)

NewBroker binds a verified plugin registry entry to server-owned host services.

func (*Broker) CanExposeRPC added in v0.2.0

func (b *Broker) CanExposeRPC() bool

CanExposeRPC reports whether the verified plugin declared rpc:expose, i.e. may register a callable service in the inter-plugin RPC registry. The server checks this before registering a plugin's handlers (design-09 §F).

func (*Broker) HTTPDo added in v0.2.0

HTTPDo performs guarded outbound HTTP and requires http:egress. The broker itself runs the SSRF/egress guard on req.URL BEFORE delegating to the HTTPHost, so egress filtering is structural at this boundary and every HTTPHost (including future ones) is guarded regardless of its own behavior.

func (*Broker) HasCapability added in v0.2.0

func (b *Broker) HasCapability(cap string) bool

HasCapability reports whether the verified plugin declared cap.

func (*Broker) KVGet added in v0.2.0

func (b *Broker) KVGet(ctx context.Context, key string) ([]byte, bool, error)

KVGet reads a KV value and requires kv:read. The plugin-supplied key names only the entry within the plugin's own namespace; the broker pins the bucket so a plugin can never read another plugin's or the operator's keys.

func (*Broker) KVPut added in v0.2.0

func (b *Broker) KVPut(ctx context.Context, key string, value []byte) error

KVPut writes a KV value and requires kv:write. As with KVGet, the bucket is fixed to the plugin's own namespace; the plugin only chooses the entry key.

func (*Broker) Log added in v0.2.0

func (b *Broker) Log(ctx context.Context, level, message string, fields map[string]string) error

Log writes a plugin-authored log entry and requires log:write. The plugin controls the level/message/fields, so the broker bounds them before they reach the sink: the level is mapped to a known set, an oversize message is truncated, and the field count is capped. This prevents a plugin from flooding or poisoning the operator log through unbounded input.

func (*Broker) Notify added in v0.2.0

func (b *Broker) Notify(ctx context.Context, title, body string) error

Notify sends an operator notification and requires notify:send.

func (*Broker) PluginID added in v0.2.0

func (b *Broker) PluginID() string

PluginID returns the verified plugin id attached to this broker.

func (*Broker) RPCCall added in v0.2.0

func (b *Broker) RPCCall(ctx context.Context, service, method string, request []byte) ([]byte, error)

RPCCall invokes method on another plugin's exposed service and requires rpc:call. The broker stamps the VERIFIED caller id (b.pluginID) so the registry can authorize the directed edge; a plugin cannot impersonate another caller. Registry-level denials (ErrRPCDenied) are additionally recorded as a deny event for audit visibility (the capability check itself already recorded an allow). Service/method-not-found are client errors, not security denials, so they are returned without an extra deny event.

type CapabilityError added in v0.2.0

type CapabilityError struct {
	PluginID   string
	Capability string
}

CapabilityError describes the exact capability a plugin lacked.

func (*CapabilityError) Error added in v0.2.0

func (e *CapabilityError) Error() string

func (*CapabilityError) Unwrap added in v0.2.0

func (e *CapabilityError) Unwrap() error

type HTTPHost added in v0.2.0

type HTTPHost interface {
	Do(ctx context.Context, req HostHTTPRequest) (HostHTTPResponse, error)
}

HTTPHost performs guarded outbound HTTP. Implementations must enforce the server's SSRF/egress policy before dialing.

type HostAudit added in v0.2.0

type HostAudit interface {
	RecordHostCall(ctx context.Context, event HostCallEvent)
}

HostAudit records broker allow/deny decisions for host API calls.

type HostCallEvent added in v0.2.0

type HostCallEvent struct {
	PluginID   string
	Action     string
	Capability string
	Decision   string
	Reason     string
}

HostCallEvent records one broker authorization decision.

type HostHTTPRequest added in v0.2.0

type HostHTTPRequest struct {
	Method string
	URL    string
	Header map[string]string
	Body   []byte
}

HostHTTPRequest is the broker's stable outbound HTTP request shape.

type HostHTTPResponse added in v0.2.0

type HostHTTPResponse struct {
	StatusCode int
	Header     map[string]string
	Body       []byte
}

HostHTTPResponse is the broker's stable outbound HTTP response shape.

type HostLogEntry added in v0.2.0

type HostLogEntry struct {
	PluginID string
	Level    string
	Message  string
	Fields   map[string]string
}

HostLogEntry is a plugin-authored structured log entry.

type HostServices added in v0.2.0

type HostServices struct {
	KV     KVHost
	Notify NotifyHost
	HTTP   HTTPHost
	Log    LogHost
	Audit  HostAudit
	// RPC dispatches inter-plugin calls (design-09 §F). When nil, a plugin with
	// rpc:call gets ErrHostServiceUnavailable rather than a panic.
	RPC RPCHost
	// GuardURL, when set, validates an outbound HTTP target's URL/host BEFORE the
	// broker delegates to HTTP.Do. It makes SSRF/egress guarding structural at the
	// broker boundary rather than relying on every HTTPHost implementation to
	// remember to guard. A non-nil error rejects the request before any dial. When
	// nil, the broker falls back to a built-in guard (see defaultGuardURL) so an
	// HTTPHost is never trusted by convention alone.
	GuardURL func(url string) error
}

HostServices are the real server-owned handles exposed through the broker. The broker keeps these handles behind per-call capability checks.

type InterfaceContract added in v0.2.0

type InterfaceContract struct {
	Service string   `json:"service"`
	Methods []string `json:"methods"`
	Scopes  []string `json:"scopes,omitempty"`
}

InterfaceContract declares an interface the plugin exposes (service + methods), callable through the dashboard->plugin gateway under the given scopes.

type InvokeRequest added in v0.2.0

type InvokeRequest struct {
	PluginID string
	Action   string
	Payload  json.RawMessage
}

InvokeRequest asks an armed plugin to perform one action. Payload is the raw JSON body handed to the plugin; the runner frames {action,payload} as a single stdin line and reads the reply from stdout.

type InvokeResponse added in v0.2.0

type InvokeResponse struct {
	OK      bool
	Message string
	Result  json.RawMessage
}

InvokeResponse is the decoded plugin reply. Result carries the plugin's body (e.g. a rendered plan) for the host to act on under its own privileges.

type Invoker added in v0.2.0

type Invoker interface {
	Invoke(ctx context.Context, req InvokeRequest) (InvokeResponse, error)
}

Invoker is an optional runner capability: a request/response action protocol with the plugin. The system runner implements it; the noop runner does not.

type KVHost added in v0.2.0

type KVHost interface {
	Get(ctx context.Context, key string) ([]byte, bool, error)
	Put(ctx context.Context, key string, value []byte) error
}

KVHost is the plugin-facing KV subset. The implementation remains server-owned.

type LoadOutcome added in v0.2.0

type LoadOutcome struct {
	BundlePath string
	PluginID   string
	Loaded     bool
	Reason     string // failure reason when Loaded is false
}

LoadOutcome records the result of attempting to load one bundle so the caller can audit every accept and reject.

type Loaded added in v0.2.0

type Loaded struct {
	Manifest     Manifest
	Capabilities []string
	BundlePath   string
}

Loaded is a verified, registered plugin: the decoded manifest plus the capabilities the trust policy granted at load time. Execution (host-API binding and invocation) is a later milestone; loading establishes the verified registry and is the point at which signature/digest/capability trust is enforced.

type Loader added in v0.2.0

type Loader struct {
	Dir    string
	Policy TrustPolicy
}

Loader discovers and verifies plugin bundles under Dir against an operator TrustPolicy. It never executes anything; it only decides what is trusted enough to register.

func (Loader) Load added in v0.2.0

func (l Loader) Load() ([]Loaded, []LoadOutcome, error)

Load scans the plugin directory and verifies each bundle. It returns the verified plugins (sorted by id) and a per-bundle outcome log. A bundle that fails verification is skipped and recorded as a failure — one bad bundle never aborts the scan or blocks startup. A missing/empty directory loads nothing.

type LogHost added in v0.2.0

type LogHost interface {
	Write(ctx context.Context, entry HostLogEntry) error
}

LogHost records a plugin-authored log entry after the broker stamps plugin id.

type Manifest

type Manifest struct {
	ID               string   `json:"id"`
	Name             string   `json:"name"`
	Type             string   `json:"type"`
	Capabilities     []string `json:"capabilities"`
	Version          string   `json:"version,omitempty"`
	Entrypoint       string   `json:"entrypoint,omitempty"`
	Publisher        string   `json:"publisher,omitempty"`
	DigestSHA256     string   `json:"digest_sha256,omitempty"`
	SignatureEd25519 string   `json:"signature_ed25519,omitempty"`
	// UI + Interfaces are the design-10 dashboard contributions: declarative data
	// (nav/views) + the interfaces the plugin exposes. They are covered by the
	// signature (see SigningPayload) so a tampered contribution fails verification.
	UI         *ManifestUI         `json:"ui,omitempty"`
	Interfaces []InterfaceContract `json:"interfaces,omitempty"`
}

func VerifyInstallManifest added in v0.2.0

func VerifyInstallManifest(manifestBytes, artifact []byte, policy TrustPolicy) (Manifest, error)

type ManifestUI added in v0.2.0

type ManifestUI struct {
	Nav   []NavContribution  `json:"nav,omitempty"`
	Views []ViewContribution `json:"views,omitempty"`
}

ManifestUI is a plugin's dashboard contribution set.

type NavContribution struct {
	Section      string   `json:"section"`
	SectionTitle string   `json:"section_title,omitempty"`
	Title        string   `json:"title"`
	Route        string   `json:"route"`
	Icon         string   `json:"icon,omitempty"`
	Scopes       []string `json:"scopes,omitempty"`
}

NavContribution is a sidebar entry a plugin adds. Route is plugin-relative and mounted at /plugins/<id>/<route>.

type NotifyHost added in v0.2.0

type NotifyHost interface {
	Send(ctx context.Context, title, body string) error
}

NotifyHost sends an operator notification through server-owned channels.

type RPCHandler added in v0.2.0

type RPCHandler func(ctx context.Context, method string, request []byte) ([]byte, error)

RPCHandler serves one inter-plugin RPC method: it receives the method name and raw request bytes and returns raw response bytes. Implementations must be safe for concurrent use; the registry invokes them WITHOUT holding its lock.

type RPCHost added in v0.2.0

type RPCHost interface {
	Call(ctx context.Context, caller, service, method string, request []byte) ([]byte, error)
}

RPCHost dispatches an inter-plugin RPC call to a server-owned registry that resolves the target service, enforces the directed caller->callee allow-list, and invokes the callee's handler. caller is the VERIFIED id of the calling plugin, supplied by the broker; a plugin cannot spoof it.

type RPCRegistry added in v0.2.0

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

RPCRegistry is the server-owned inter-plugin RPC bus. It is safe for concurrent use and implements the broker's RPCHost interface.

func NewRPCRegistry added in v0.2.0

func NewRPCRegistry() *RPCRegistry

NewRPCRegistry returns an empty registry.

func (*RPCRegistry) Allow added in v0.2.0

func (r *RPCRegistry) Allow(callerPluginID, service string)

Allow grants callerPluginID permission to call service. Grants are directed and additive; a service owner can always call its own service.

func (*RPCRegistry) Call added in v0.2.0

func (r *RPCRegistry) Call(ctx context.Context, caller, service, method string, request []byte) ([]byte, error)

Call implements RPCHost: resolve the service, enforce the directed allow-list (the owner may always self-call), check the method, then dispatch to the handler OUTSIDE the lock so a slow or re-entrant handler cannot block the bus.

func (*RPCRegistry) CallOperator added in v0.2.0

func (r *RPCRegistry) CallOperator(ctx context.Context, service, method string, request []byte) ([]byte, error)

CallOperator dispatches a service method on behalf of the OPERATOR (the dashboard gateway), bypassing the plugin->plugin directed allow-list — the HTTP layer has already enforced the interface's declared RBAC scopes + audit. Service/method-not-found are still errors. The handler runs OUTSIDE the lock.

func (*RPCRegistry) Register added in v0.2.0

func (r *RPCRegistry) Register(ownerPluginID, service, version string, methods []string, handler RPCHandler) error

Register adds (or replaces) a service exposed by ownerPluginID. service is the fully-qualified id (e.g. "latticenet.vpn-core/nodes"); it must carry >=1 non-empty method and a non-nil handler. Re-registering the same id replaces it (used on plugin restart). The caller (server) is responsible for verifying the owner declared rpc:expose before registering.

func (*RPCRegistry) Revoke added in v0.2.0

func (r *RPCRegistry) Revoke(callerPluginID, service string)

Revoke removes a previously granted directed edge.

func (*RPCRegistry) Services added in v0.2.0

func (r *RPCRegistry) Services() []RPCServiceDescriptor

Services returns descriptors for every registered service, sorted by id, for discovery (e.g. an rpc.List host call). Handlers are not exposed.

func (*RPCRegistry) Unregister added in v0.2.0

func (r *RPCRegistry) Unregister(service string)

Unregister removes a service and any grants targeting it (e.g. on disable).

type RPCServiceDescriptor added in v0.2.0

type RPCServiceDescriptor struct {
	Service string   `json:"service"`
	Owner   string   `json:"owner"`
	Version string   `json:"version"`
	Methods []string `json:"methods"`
}

RPCServiceDescriptor is the discovery shape returned by RPCRegistry.Services. Handlers are never exposed.

type Runner added in v0.2.0

type Runner interface {
	Name() string
	Start(ctx context.Context, req RunnerStartRequest) (RunnerStartResult, error)
	Stop(ctx context.Context, req RunnerStopRequest) error
}

Runner is the narrow runtime contract concrete plugin runtimes must satisfy. It receives a verified plugin and a capability-scoped broker, never raw server handles. Implementations must honor ctx cancellation and deadlines.

type RunnerStartRequest added in v0.2.0

type RunnerStartRequest struct {
	PluginID string
	Loaded   Loaded
	Broker   *Broker
}

type RunnerStartResult added in v0.2.0

type RunnerStartResult struct {
	Message string
}

type RunnerStopRequest added in v0.2.0

type RunnerStopRequest struct {
	PluginID string
	Reason   string
}

type RuntimeManager added in v0.2.0

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

RuntimeManager binds verified plugins to capability-scoped brokers and tracks runtime health. The current implementation is an execution-safe skeleton: it arms host-API access for a verified plugin but does not spawn processes, load wasm, or invoke artifact code.

func NewRuntimeManager added in v0.2.0

func NewRuntimeManager(services HostServices) *RuntimeManager

func NewRuntimeManagerWithOptions added in v0.2.0

func NewRuntimeManagerWithOptions(opts RuntimeManagerOptions) *RuntimeManager

func (*RuntimeManager) Invoke added in v0.2.0

func (m *RuntimeManager) Invoke(ctx context.Context, pluginID, action string, payload json.RawMessage) (InvokeResponse, error)

Invoke dispatches one action to an armed plugin whose runner supports the request/response protocol. It fails closed if the plugin is not armed or its runner is not an Invoker (e.g. the noop runner), so a disabled or execution-disabled plugin can never be invoked.

func (*RuntimeManager) IsArmed added in v0.2.0

func (m *RuntimeManager) IsArmed(pluginID string) bool

func (*RuntimeManager) Snapshot added in v0.2.0

func (m *RuntimeManager) Snapshot() map[string]RuntimeStatus

func (*RuntimeManager) Start added in v0.2.0

func (m *RuntimeManager) Start(ctx context.Context, loaded Loaded) (RuntimeStatus, error)

Start validates the loaded plugin, creates its broker, and marks it armed. The context is accepted now so future runners can honor cancellation without changing the call site.

func (*RuntimeManager) Status added in v0.2.0

func (m *RuntimeManager) Status(pluginID string) (RuntimeStatus, bool)

func (*RuntimeManager) Stop added in v0.2.0

func (m *RuntimeManager) Stop(pluginID, message string) (RuntimeStatus, error)

type RuntimeManagerOptions added in v0.2.0

type RuntimeManagerOptions struct {
	Services     HostServices
	Runners      map[string]Runner
	StartTimeout time.Duration
}

type RuntimeStatus added in v0.2.0

type RuntimeStatus struct {
	PluginID  string    `json:"plugin_id"`
	State     string    `json:"state"`
	Runner    string    `json:"runner,omitempty"`
	Message   string    `json:"message,omitempty"`
	StartedAt time.Time `json:"started_at,omitempty"`
	StoppedAt time.Time `json:"stopped_at,omitempty"`
	UpdatedAt time.Time `json:"updated_at"`
}

RuntimeStatus is the public, non-secret health view for one plugin runtime. It deliberately excludes local bundle paths and the broker itself.

type SystemRunner added in v0.2.0

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

SystemRunner implements Runner and Invoker.

func NewSystemRunner added in v0.2.0

func NewSystemRunner(opts SystemRunnerOptions) *SystemRunner

NewSystemRunner returns a system runner with the given options and safe defaults for any zero-valued bound.

func (*SystemRunner) Invoke added in v0.2.0

Invoke runs the plugin for one action and returns its decoded reply. The process is spawned with arg-vector exec (NO shell, so payload content can never be interpreted as a command), a confined working directory, an allowlisted environment, capped output, and a deadline that escalates SIGTERM->SIGKILL. Repeated failures trip the circuit breaker. A crashing plugin yields an error, never a host crash.

func (*SystemRunner) Name added in v0.2.0

func (r *SystemRunner) Name() string

func (*SystemRunner) Start added in v0.2.0

Start re-verifies the manifest-pinned digest of the artifact at the FIXED bundle path (TOCTOU defense in case the bundle changed after load), copies the bytes into a confined per-plugin 0700 working dir, and arms the runner. It always executes the staged 0700 copy, never the (possibly read-only or swapped) bundle file, and never resolves a manifest-controlled path.

func (*SystemRunner) Stop added in v0.2.0

Stop clears the plugin's staged state and removes its runtime dir. In-flight invocations are bound to their own context and terminate independently.

type SystemRunnerOptions added in v0.2.0

type SystemRunnerOptions struct {
	// RuntimeDir is the root under which each plugin gets a confined 0700 working
	// directory (RuntimeDir/<pluginID>) holding a 0700 copy of its verified
	// artifact. Required.
	RuntimeDir string
	// EnvAllowlist names the environment variables forwarded to the plugin. Every
	// other variable is stripped; a fixed safe PATH is always provided.
	EnvAllowlist []string
	// InvokeTimeout bounds one invocation (default 10s).
	InvokeTimeout time.Duration
	// StopGrace is the SIGTERM->SIGKILL window for a timed-out/cancelled process
	// (default 3s).
	StopGrace time.Duration
	// MaxOutputBytes caps captured stdout and (separately) stderr per invocation
	// (default 1 MiB), so a plugin cannot exhaust host memory by flooding output.
	MaxOutputBytes int
	// CrashThreshold trips the circuit breaker after this many consecutive failed
	// invocations (default 5).
	CrashThreshold int
	// MaxHostCalls caps broker calls during one invocation (default 64).
	MaxHostCalls int
}

SystemRunnerOptions configures the trusted-subprocess runner.

type TrustPolicy added in v0.2.0

type TrustPolicy struct {
	TrustedPublishers map[string]ed25519.PublicKey
	// AllowUnsignedHostRisk opts OUT of signature enforcement for host-risk
	// plugins. The zero value is fail-closed: host-risk plugins require a trusted
	// publisher signature unless an operator explicitly sets this true (dev only).
	AllowUnsignedHostRisk bool
}

func ParseTrustPolicyJSON added in v0.2.0

func ParseTrustPolicyJSON(data []byte) (TrustPolicy, error)

type ViewAction added in v0.2.0

type ViewAction struct {
	Label     string          `json:"label"`
	Interface string          `json:"interface"`
	Method    string          `json:"method"`
	Form      []ViewFormField `json:"form,omitempty"`
	Scopes    []string        `json:"scopes,omitempty"`
}

ViewAction is a button that calls an interface method (optionally with a form).

type ViewColumn added in v0.2.0

type ViewColumn struct {
	Key    string `json:"key"`
	Label  string `json:"label"`
	Render string `json:"render,omitempty"`
}

ViewColumn is one table column. Render selects a safe dashboard formatter.

type ViewContribution added in v0.2.0

type ViewContribution struct {
	Route        string       `json:"route"`
	Title        string       `json:"title"`
	Kind         string       `json:"kind"`
	ComponentKey string       `json:"component_key,omitempty"`
	Source       *ViewSource  `json:"source,omitempty"`
	Columns      []ViewColumn `json:"columns,omitempty"`
	Actions      []ViewAction `json:"actions,omitempty"`
}

ViewContribution is one declarative view rendered by a fixed dashboard primitive.

type ViewFormField added in v0.2.0

type ViewFormField struct {
	Key     string   `json:"key"`
	Label   string   `json:"label,omitempty"`
	Kind    string   `json:"kind"`
	Options []string `json:"options,omitempty"`
}

ViewFormField is one input in an action form.

type ViewSource added in v0.2.0

type ViewSource struct {
	Interface string `json:"interface"`
	Method    string `json:"method"`
}

ViewSource binds a view's data to a plugin interface method.

Jump to

Keyboard shortcuts

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