extplugin

package
v0.5.8 Latest Latest
Warning

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

Go to latest
Published: Jul 19, 2026 License: MIT Imports: 60 Imported by: 0

Documentation

Overview

Package extplugin spawns and proxies clawpatrol's Terraform-style external plugins. The gateway uses HashiCorp go-plugin for subprocess lifecycle and gRPC for the wire protocol; the gateway-side Manager registers a virtual *config.Plugin per type the subprocess declares in its Manifest, so the rest of the loader (symbol table, framework attrs, ref resolution, dispatch) stays unaware that any of these plugins is out-of-process.

Index

Constants

View Source
const HostServicesBrokerID = wire.HostServicesBrokerID

HostServicesBrokerID is the fixed go-plugin broker stream id the gateway serves host services on and the plugin dials. A constant is safe because clawpatrol never calls GRPCBroker.NextId() — the brokered upstream dial multiplexes over the HandleConn stream, not the broker — so nothing else competes for stream ids. Canonical definition in wire.

View Source
const LockfileName = "clawpatrol.lock.hcl"

LockfileName is the lockfile's basename, written alongside the gateway config.

View Source
const PluginName = wire.PluginName

PluginName is the registered plugin name in go-plugin's plugin map.

View Source
const SessionMetadataKey = wire.SessionMetadataKey

SessionMetadataKey is the gRPC metadata key the per-connection session token rides under on every HostControl call. Lower-case per gRPC's metadata convention. Canonical definition in wire.

Variables

View Source
var HandshakeConfig = wire.HandshakeConfig

HandshakeConfig and PluginName are the go-plugin handshake shared by the gateway and every plugin. The canonical definitions live in the wire leaf package — kept dependency-light so the SDK (and thus a plugin's binary) doesn't pull the manager's graph just to name them. These aliases keep extplugin's many in-package references unchanged.

Functions

func LockfilePathFor added in v0.3.0

func LockfilePathFor(configPath string) string

LockfilePathFor returns the lockfile path that sits beside the given gateway config file.

func RegisterManifest

func RegisterManifest(client *Client, resp *pb.ManifestResponse) hcl.Diagnostics

RegisterManifest converts every type in resp into a virtual *config.Plugin and installs it in the global registry. The (Kind, Type) names are namespaced as "<plugin>.<type>" so two plugins can't collide on, say, "https".

Returns hcl.Diagnostics for any per-type registration failure; the caller should attach the source range of the `plugin` block.

Types

type ApprovedPlugin added in v0.3.0

type ApprovedPlugin struct {
	Name       string
	Network    string
	Privileged bool
}

ApprovedPlugin is one result row from Approve.

type Client

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

Client is the gateway-side handle to one running plugin subprocess. Adapters use it to issue RPCs.

func (*Client) CredentialRPC added in v0.2.10

func (c *Client) CredentialRPC() pb.CredentialClient

CredentialRPC exposes InjectHTTP for credential adapters.

func (*Client) Egress added in v0.3.0

func (c *Client) Egress() []string

Egress reports the lockfile-approved brokered-dial targets the plugin's manifest declared.

func (*Client) EndpointRPC

func (c *Client) EndpointRPC() pb.EndpointClient

EndpointRPC exposes HandleConn for endpoint adapters.

func (*Client) Manifest

func (c *Client) Manifest() *pb.ManifestResponse

Manifest returns the manifest the subprocess reported at startup. Stable across the plugin's lifetime (manifests aren't refreshed in v1).

func (*Client) Name

func (c *Client) Name() string

Name returns the plugin's manifest name (lower-case identifier).

func (*Client) Network added in v0.3.0

func (c *Client) Network() string

Network reports the approved network grant the plugin runs with ("none" or "outbound").

func (*Client) PluginRPC

func (c *Client) PluginRPC() pb.PluginClient

PluginRPC exposes the Build RPC; used by the registration helper.

func (*Client) SandboxMode added in v0.3.0

func (c *Client) SandboxMode() string

SandboxMode reports which sandbox backend the subprocess runs under ("off" when the operator opted out).

func (*Client) SandboxWarning added in v0.3.0

func (c *Client) SandboxWarning() string

SandboxWarning is non-empty when the plugin runs under a degraded fallback backend; it describes what the fallback does not cover.

func (*Client) Source

func (c *Client) Source() string

Source returns the binary path the manager was started with.

func (*Client) TunnelRPC

func (c *Client) TunnelRPC() pb.TunnelClient

TunnelRPC exposes OpenTunnel / Dial / CloseTunnel for tunnel adapters.

type InstalledPlugin added in v0.3.0

type InstalledPlugin struct {
	Name      string
	Source    string
	Version   string
	Network   string
	Updated   bool   // version changed from what was previously locked
	WasLocked string // the previously-locked version ("" if new)
}

InstalledPlugin reports the outcome of installing/updating one plugin.

type Manager

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

Manager spawns and supervises one subprocess per declared plugin source. Manifests fetched at Start() time get registered as virtual *config.Plugin entries by the (config-side) registration code.

Lifecycle: Start each plugin once before the loader's policy decode pass runs (so the registry has the plugin's types). Call Stop on gateway shutdown.

func New

func New(out *log.Logger) *Manager

New constructs an empty Manager. The logger is wrapped so plugin stdio surfaces in the gateway's log stream tagged with the plugin name; pass nil to use a default discarding logger.

func (*Manager) Approve added in v0.3.0

func (m *Manager) Approve(ctx context.Context, specs []config.PluginSource, names []string) ([]ApprovedPlugin, error)

Approve (re)records the current permissions of the named plugins (or all when names is empty) in the lockfile: for a GitHub source it re-resolves the constraint's newest release and downloads it, then writes {source, version, commit, attested, hash, declared network}, bypassing both the network-escalation and the provenance-downgrade check — this is the operator deliberately accepting the current version. It does not register the plugin's types, so it is safe to call without a full config load.

func (*Manager) CheckUpdates added in v0.3.0

func (m *Manager) CheckUpdates(ctx context.Context, specs []config.PluginSource)

CheckUpdates queries GitHub for each pinned GitHub-sourced plugin and records whether a newer release tag satisfying its constraint exists, so the dashboard can surface "update available". It never downloads or re-pins — applying an update is the operator's explicit `plugins update`. Per-plugin lookup errors are logged and skipped so one unreachable repo doesn't blank the whole set.

func (*Manager) Install added in v0.3.0

func (m *Manager) Install(ctx context.Context, specs []config.PluginSource, names []string, upgrade bool) ([]InstalledPlugin, error)

Install downloads and caches each named GitHub-sourced plugin (all when names is empty), recording the resolved source/version, the declared network, and the binary hash in the lockfile. Local-path plugins are skipped (nothing to fetch). When upgrade is true it re-resolves to the newest release tag satisfying the constraint — the explicit upgrade; otherwise it keeps any already-pinned version and just ensures it is downloaded.

Install probes each downloaded plugin's manifest (a throwaway network-denied spawn) to record its declared network, so it requires a working sandbox backend just like a normal load.

func (*Manager) LoadPlugins

func (m *Manager) LoadPlugins(specs []config.PluginSource, stateDir string) hcl.Diagnostics

LoadPlugins satisfies config.PluginLoader. Called from inside config.Load after the operational decode and before pass-1 symbol building. For each plugin source: spawn the subprocess, fetch the manifest, register virtual *config.Plugin entries.

Already-loaded plugins (matched by manifest name) are skipped so reload-style flows don't re-spawn or trip the "duplicate plugin" panic in config.Register.

func (*Manager) LockPlatforms added in v0.3.0

func (m *Manager) LockPlatforms(ctx context.Context, specs []config.PluginSource, names []string) ([]InstalledPlugin, error)

LockPlatforms records, for each named GitHub-sourced plugin at its pinned version, the binary hash of every platform build the release ships — so one committed lockfile verifies the plugin on a mixed-OS team. It downloads and extracts each platform's archive to a temp dir (only the host platform's binary is cached) and adds every hash to the lockfile. A plugin must already be pinned (run `install` first).

func (*Manager) PluginInfos added in v0.3.0

func (m *Manager) PluginInfos() []PluginInfo

PluginInfos returns a dashboard summary of every plugin — loaded ones with their permissions, plus any currently blocked (e.g. a permission escalation) with the failure reason.

func (*Manager) Plugins

func (m *Manager) Plugins() []*Client

Plugins returns every loaded plugin's *Client, sorted by name. Used by callers (clawpatrol validate, dashboard surfaces, etc.) that want to enumerate manifests after LoadPlugins has run.

func (*Manager) PreviewSource added in v0.3.0

func (m *Manager) PreviewSource(ctx context.Context, sp config.PluginSource) (ManifestPreview, error)

PreviewSource resolves the constraint to the newest release and reads its static manifest (no binary download), returning the plugin's metadata and required privileges. The locked version, if any, is included so a caller can show what an upgrade would change. Returns errNoManifest when the release publishes no static manifest.

func (*Manager) SetBlobStore added in v0.3.0

func (m *Manager) SetBlobStore(b runtime.BlobStore)

SetBlobStore wires the persistent byte store that backs the per-plugin HostState service. Without it, a plugin that calls State gets an error (the gateway main provides one; the CLI install/probe paths leave it nil — they never run a plugin long enough to need state).

func (*Manager) SetCacheDir added in v0.5.2

func (m *Manager) SetCacheDir(d string)

SetCacheDir overrides the root for cached plugin binaries, independent of the gateway state dir. The verification commands set it from --plugin-cache-dir so they can lint a config whose state_dir isn't writable here. LoadPlugins deliberately does not touch cacheDir, so a value set here survives a config load.

func (*Manager) SetLockfile added in v0.3.0

func (m *Manager) SetLockfile(path string, readOnly bool)

SetLockfile points the manager at the permission lockfile beside the gateway config. Without it (tests, config.LoadBytes) plugins fall back to their manifest-declared capabilities with no persistence or escalation check. readOnly (used by `clawpatrol validate`) resolves and reports escalations but never writes the lockfile.

func (*Manager) SetStateDir added in v0.3.0

func (m *Manager) SetStateDir(d string)

SetStateDir sets the gateway state dir — where GitHub-sourced plugins are cached, and the dir read_paths may not overlap. The gateway sets it via LoadPlugins; the `plugins install|update|lock` commands set it from the resolved config so the cache lands in the same place.

func (*Manager) SetTransportDialer added in v0.3.0

func (m *Manager) SetTransportDialer(d func(network, addr string) (net.Conn, error))

SetTransportDialer wires the gateway's direct upstream dialer, used to serve a tunnel plugin's HostTunnel.DialUpstream when the tunnel has no `via` parent. The gateway main provides its own dialer; CLI install/probe paths leave it nil (they never run a tunnel).

func (*Manager) Start

Start spawns the plugin binary declared by sp inside the sandbox its grants call for, performs the gRPC handshake, fetches the Manifest, and returns a *Client whose Manifest method exposes the declared types. The caller (the register helper in this package) typically immediately registers every type with the global config registry.

Start blocks until the subprocess is ready or fails. Returns the client + manifest, or an error suitable for surfacing as an HCL diagnostic on the `plugin` block.

func (*Manager) Stop

func (m *Manager) Stop()

Stop tears down every spawned subprocess. Idempotent.

func (*Manager) Verify

func (m *Manager) Verify() hcl.Diagnostics

Verify runs post-load schema validation against every spawned plugin's manifest. Catches problems that wouldn't surface otherwise until a rule happened to target a particular facet or an HCL block happened to use a particular type:

  • Each declared facet's CEL env is built eagerly (with a probe condition) so an invalid identifier in a facet or field name fails the validate command instead of waiting for a rule.
  • Each declared endpoint's Family is resolved against the facet registry (built-in or another plugin's). A typo in Family that no rule references would otherwise just silently route every request to default-deny at runtime.

Returns hcl.Diagnostics with one entry per problem.

func (*Manager) VerifyProvenance added in v0.3.0

func (m *Manager) VerifyProvenance(enabled bool)

VerifyProvenance turns on GitHub build-provenance attestation checks for downloaded plugins. When enabled, an archive that carries an attestation must verify against owner/repo's GitHub Actions identity (via Sigstore); an archive with no attestation falls back to the SHA256SUMS + lockfile-TOFU floor with a warning. The gateway and the install/update/lock commands enable it; tests inject a stub instead.

type ManifestPreview added in v0.3.0

type ManifestPreview struct {
	Name        string   `json:"name"`
	Source      string   `json:"source"`
	Version     string   `json:"version"` // resolved release tag
	Locked      string   `json:"locked,omitempty"`
	Network     string   `json:"network"`              // required network grant
	Egress      []string `json:"egress,omitempty"`     // required brokered-dial targets
	Privileged  bool     `json:"privileged,omitempty"` // requires running unsandboxed
	Credentials []string `json:"credentials,omitempty"`
	Endpoints   []string `json:"endpoints,omitempty"`
	Tunnels     []string `json:"tunnels,omitempty"`
	Facets      []string `json:"facets,omitempty"`
}

ManifestPreview is a plugin version's metadata and required privileges, read from its static manifest with no binary download.

type PluginInfo added in v0.3.0

type PluginInfo struct {
	Name   string `json:"name"`
	Source string `json:"source"`
	// Blocked plugins carry only Name, Source, Blocked, Reason.
	Blocked bool   `json:"blocked,omitempty"`
	Reason  string `json:"reason,omitempty"`

	Version        string   `json:"version,omitempty"`
	Network        string   `json:"network,omitempty"`     // approved grant it runs with
	Egress         []string `json:"egress,omitempty"`      // approved brokered-dial targets
	Privileged     bool     `json:"privileged,omitempty"`  // approved to run unsandboxed
	SandboxMode    string   `json:"sandboxMode,omitempty"` // namespaces | landlock | seatbelt | off
	SandboxWarning string   `json:"sandboxWarning,omitempty"`
	ApprovedHashes []string `json:"approvedHashes,omitempty"` // lockfile-approved binary hashes (one per platform build)
	// UpdateAvailable is the newest release tag satisfying the plugin's
	// constraint that is newer than the locked version (GitHub sources
	// only). Set by the background update check; the operator applies it
	// with `clawpatrol plugins update`.
	UpdateAvailable string   `json:"updateAvailable,omitempty"`
	Credentials     []string `json:"credentials,omitempty"`
	Tunnels         []string `json:"tunnels,omitempty"`
	Endpoints       []string `json:"endpoints,omitempty"`
	Facets          []string `json:"facets,omitempty"`
	// Requested is the privileges a blocked/unapproved plugin's
	// resolvable version declares in its signed static manifest — what it
	// wants, shown so an operator can review before approving. Read
	// without running the plugin; nil when no static manifest is
	// available.
	Requested *RequestedPrivileges `json:"requested,omitempty"`
}

PluginInfo is the dashboard-facing summary of one plugin — either loaded (with its permissions and declared types) or blocked (failed to load, with the reason; chiefly a permission escalation).

type RequestedPrivileges added in v0.3.0

type RequestedPrivileges struct {
	Version     string   `json:"version,omitempty"`
	Network     string   `json:"network"`
	Egress      []string `json:"egress,omitempty"`
	Privileged  bool     `json:"privileged,omitempty"`
	Credentials []string `json:"credentials,omitempty"`
	Endpoints   []string `json:"endpoints,omitempty"`
	Tunnels     []string `json:"tunnels,omitempty"`
	Facets      []string `json:"facets,omitempty"`
}

RequestedPrivileges is what a plugin version declares it needs, read from its signed static manifest (no spawn).

type SubFieldReferencer

type SubFieldReferencer interface {
	SubFieldRefs() map[string]bool
}

SubFieldReferencer is implemented by plugin-facet matchers to surface the set of facet sub-fields the compiled condition reads. Used by the adapter's EvaluateAction handler to decide whether to pull a stream-typed field in full or just enough for log-prefix.

type Verdict added in v0.3.0

type Verdict struct {
	Action string // "allow" | "deny" | "hitl_allow" | "hitl_deny" | "error"
	Reason string
	Rule   string
}

Verdict is the gateway's decision on one Evaluate call. Mirrors runtime / pluginsdk.Verdict so the host-served surface and the existing frame path return the same shape.

Directories

Path Synopsis
Package wire holds the small set of protocol constants shared between the gateway-side plugin manager (extplugin) and the plugin SDK (pluginsdk): the go-plugin handshake, the dispensed plugin name, the host-services broker stream id, and the per-call session metadata key.
Package wire holds the small set of protocol constants shared between the gateway-side plugin manager (extplugin) and the plugin SDK (pluginsdk): the go-plugin handshake, the dispensed plugin name, the host-services broker stream id, and the per-call session metadata key.

Jump to

Keyboard shortcuts

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