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
- Variables
- func LockfilePathFor(configPath string) string
- func RegisterManifest(client *Client, resp *pb.ManifestResponse) hcl.Diagnostics
- type ApprovedPlugin
- type Client
- func (c *Client) CredentialRPC() pb.CredentialClient
- func (c *Client) Egress() []string
- func (c *Client) EndpointRPC() pb.EndpointClient
- func (c *Client) Manifest() *pb.ManifestResponse
- func (c *Client) Name() string
- func (c *Client) Network() string
- func (c *Client) PluginRPC() pb.PluginClient
- func (c *Client) SandboxMode() string
- func (c *Client) SandboxWarning() string
- func (c *Client) Source() string
- func (c *Client) TunnelRPC() pb.TunnelClient
- type InstalledPlugin
- type Manager
- func (m *Manager) Approve(ctx context.Context, specs []config.PluginSource, names []string) ([]ApprovedPlugin, error)
- func (m *Manager) CheckUpdates(ctx context.Context, specs []config.PluginSource)
- func (m *Manager) Install(ctx context.Context, specs []config.PluginSource, names []string, upgrade bool) ([]InstalledPlugin, error)
- func (m *Manager) LoadPlugins(specs []config.PluginSource, stateDir string) hcl.Diagnostics
- func (m *Manager) LockPlatforms(ctx context.Context, specs []config.PluginSource, names []string) ([]InstalledPlugin, error)
- func (m *Manager) PluginInfos() []PluginInfo
- func (m *Manager) Plugins() []*Client
- func (m *Manager) PreviewSource(ctx context.Context, sp config.PluginSource) (ManifestPreview, error)
- func (m *Manager) SetBlobStore(b runtime.BlobStore)
- func (m *Manager) SetCacheDir(d string)
- func (m *Manager) SetLockfile(path string, readOnly bool)
- func (m *Manager) SetStateDir(d string)
- func (m *Manager) SetTransportDialer(d func(network, addr string) (net.Conn, error))
- func (m *Manager) Start(ctx context.Context, sp config.PluginSource) (*Client, *pb.ManifestResponse, error)
- func (m *Manager) Stop()
- func (m *Manager) Verify() hcl.Diagnostics
- func (m *Manager) VerifyProvenance(enabled bool)
- type ManifestPreview
- type PluginInfo
- type RequestedPrivileges
- type SubFieldReferencer
- type Verdict
Constants ¶
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.
const LockfileName = "clawpatrol.lock.hcl"
LockfileName is the lockfile's basename, written alongside the gateway config.
const PluginName = wire.PluginName
PluginName is the registered plugin name in go-plugin's plugin map.
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 ¶
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
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
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
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) Network ¶ added in v0.3.0
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
SandboxMode reports which sandbox backend the subprocess runs under ("off" when the operator opted out).
func (*Client) SandboxWarning ¶ added in v0.3.0
SandboxWarning is non-empty when the plugin runs under a degraded fallback backend; it describes what the fallback does not cover.
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 ¶
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 ¶
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
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
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
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
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
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 ¶
func (m *Manager) Start(ctx context.Context, sp config.PluginSource) (*Client, *pb.ManifestResponse, error)
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
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 ¶
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.
Source Files
¶
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. |