Help us shape the future of Go! Take the 2026 Go Developer Survey

plugin

package
v0.2.3 Latest Latest
Warning

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

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

Documentation

Overview

Package plugin implements the out-of-process channel plugin SDK (openclaw_roadmap design 002, WS2 P1-P3): a third party ships a channel as a separate process speaking a small wire contract, and the in-core PluginChannelAdapter shim proxies the ChannelAdapter interface to it over stdio JSON-RPC (fallback) or the embedded per-session NATS bus (preferred). The channels package never imports this package — it reaches installed plugins through the nil-safe channels.PluginLookup / channels.PluginTypeKnown hooks that Wire installs.

Index

Constants

View Source
const (
	TransportNATS  = "nats"
	TransportStdio = "stdio"
)

Transport values a manifest may declare.

View Source
const (
	// SignatureFileName is the detached signature shipped next to the manifest.
	SignatureFileName = "rysh.channel.sig"
	// DefaultTrustFile lists trusted ed25519 public keys, one hex key per
	// line, optional comment after whitespace, '#' lines ignored.
	// Project-local like the registry root (.rysh/channels).
	DefaultTrustFile = ".rysh/plugin-keys"
	// DefaultSigningKeyFile is where `rysh channel keygen` puts the private
	// key seed by default.
	DefaultSigningKeyFile = ".rysh/plugin-signing.key"
)
View Source
const DefaultRoot = ".rysh/channels"

DefaultRoot is the project-local registry directory.

View Source
const InstallRecordFileName = "install.json"

InstallRecordFileName is the per-plugin install record.

View Source
const ManifestFileName = "rysh.channel.toml"

ManifestFileName is the manifest file every channel plugin package ships at its root (design 002 §4.3).

Variables

This section is empty.

Functions

func AppendTrustedKey

func AppendTrustedKey(path, hexKey, comment string) error

AppendTrustedKey adds a hex public key (+ optional comment) to the trust file, creating it if needed.

func ConsentText

func ConsentText(m Manifest) string

ConsentText renders the install-time consent scan (design 002 §4.4): the declared credentials, scopes, and network egress, plus the signing tier. Pure function — `rysh channel install` prints it before asking for confirmation, and tests assert on it.

func GenerateSigningKey

func GenerateSigningKey(path string) (ed25519.PublicKey, error)

GenerateSigningKey creates an ed25519 keypair and writes the seed to path (0600). It refuses to overwrite an existing key file.

func KeyID

func KeyID(pub ed25519.PublicKey) string

KeyID is the first 16 hex chars of sha256 over the raw public key — short enough to log, long enough to not collide by accident.

func LoadSigningKey

func LoadSigningKey(path string) (ed25519.PrivateKey, error)

LoadSigningKey reads a private key written by GenerateSigningKey.

func LoadTrustedKeys

func LoadTrustedKeys(path string) (keys []ed25519.PublicKey, bad []string, err error)

LoadTrustedKeys parses the trust file: one hex ed25519 public key per line, optional comment after whitespace; '#' lines and blanks ignored. A missing file is an empty trust set, not an error. Malformed lines are skipped (returned in bad) so one typo cannot lock every plugin out.

func PluginDigest

func PluginDigest(dir string, m Manifest) ([]byte, error)

PluginDigest computes the canonical bytes that get signed: a versioned preamble, the manifest fields, and the sha256 of the exec binary. The manifest's `checksum` and `signature` fields are EXCLUDED — both describe the package tarball for the registry flow and would be self-referential here. List fields join on \x1f so multi-value entries cannot collide with the line framing.

func Remove

func Remove(root, name string) error

Remove deletes an installed plugin's directory.

func Unwire

func Unwire()

Unwire clears the hooks (test hygiene).

func VerifyChecksum

func VerifyChecksum(path, expected string) error

VerifyChecksum computes sha256 over the file at path and compares it with expected ("sha256:<hex>" or bare hex).

func Wire

func Wire(o WireOptions)

Wire installs the nil-safe hooks in the channels package so channels.NewAdapter (built-ins first, always) falls through to the plugin registry, and channels.IsValidChannelType accepts installed plugin names. The channels package deliberately does NOT import this package — the CLI layer calls Wire once at startup.

Types

type InstallRecord

type InstallRecord struct {
	Name        string    `json:"name"`
	Version     string    `json:"version"`
	Transport   string    `json:"transport"`
	Tier        string    `json:"tier"`
	Source      string    `json:"source"`             // the path installed from
	Checksum    string    `json:"checksum,omitempty"` // verified tarball checksum, when one was checked
	InstalledAt time.Time `json:"installed_at"`
}

InstallRecord is written next to the manifest at install time.

type InstalledPlugin

type InstalledPlugin struct {
	Manifest Manifest
	Dir      string
}

InstalledPlugin is one installed plugin: its manifest and install dir.

func InstallPackage

func InstallPackage(root, src, expectedChecksum string) (InstalledPlugin, error)

InstallPackage installs src (dir or tarball) into root/{name}. When expectedChecksum is non-empty and src is a tarball, the tarball's sha256 is verified first.

Checksum note (v1): the manifest's own `checksum` field describes the package tarball for the REGISTRY flow — it cannot be verified against a tarball that contains it (self-reference), so local installs verify only an out-of-band checksum passed by the caller (`--checksum`).

type Manifest

type Manifest struct {
	Name      string `toml:"name"`      // channel type string; must not collide with a built-in
	Version   string `toml:"version"`   // plugin version, informational
	Transport string `toml:"transport"` // "nats" (preferred) | "stdio"
	Exec      string `toml:"exec"`      // command the supervisor launches (path within the package; whitespace-split into argv)
	MinRysh   string `toml:"min_rysh"`  // minimum rysh version, informational in v1
	Checksum  string `toml:"checksum"`  // "sha256:<hex>" over the package tarball (registry flow)
	Signature string `toml:"signature"` // reserved for the registry flow (@rysh/@verified); the dev tier signs via the detached rysh.channel.sig instead (signing.go)

	DeclaresCreds  []string `toml:"declares_creds"`  // ${ENV} credentials the plugin needs
	DeclaresScopes []string `toml:"declares_scopes"` // capabilities requested
	NetworkEgress  []string `toml:"network_egress"`  // hosts the plugin declares it reaches (declared, not enforced in v1)
}

Manifest is the parsed rysh.channel.toml (design 002 §4.3).

func LoadManifest

func LoadManifest(dir string) (Manifest, error)

LoadManifest reads and validates dir/rysh.channel.toml.

func ParseManifest

func ParseManifest(data []byte) (Manifest, error)

ParseManifest decodes and validates a rysh.channel.toml document.

func ReadPackageManifest

func ReadPackageManifest(src string) (Manifest, error)

ReadPackageManifest reads and validates the manifest from a package source (directory or tarball) WITHOUT installing anything — the CLI shows the consent scan from this before any files land in .rysh/.

func (Manifest) SigningTier

func (m Manifest) SigningTier() string

SigningTier reports the tier knowable from the MANIFEST ALONE: always "community". The registry tiers (@rysh/@verified, design 005) still need the registry service's key infrastructure and the manifest `signature` field stays a documented stub for them. The dev tier is real but lives outside the manifest — a detached rysh.channel.sig verified against the local trust file (signing.go); use VerifyPlugin(...).TierString() when a package directory is at hand.

func (Manifest) Validate

func (m Manifest) Validate() error

Validate checks the manifest invariants: a well-formed name that does not collide with a built-in channel type, a known transport, and a non-empty exec. Built-in collision is enforced here (install time) AND again at factory resolution order (built-in switch cases run first), so a plugin named "slack" can never shadow the built-in Slack adapter.

type NATSProvider

type NATSProvider func() (*nats.Conn, string, error)

NATSProvider lazily yields the core-side bus handle plus the client URL a nats-transport plugin process is handed via env. Nil when the process has no session bus (stdio plugins are unaffected).

type PluginChannelAdapter

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

PluginChannelAdapter proxies the ChannelAdapter contract to an out-of-process plugin. It mirrors SlackAdapter's discipline: a buffered inbound channel (cap 100) with non-blocking drop+warn pushes, and a cached lastStatus so Status() NEVER blocks the actor.

func NewPluginChannelAdapter

func NewPluginChannelAdapter(m Manifest, config msg.ChannelConfig, opts SupervisorOptions) (*PluginChannelAdapter, error)

NewPluginChannelAdapter builds the shim for an installed plugin manifest. Nothing is spawned until Start.

func (*PluginChannelAdapter) InboundCh

func (a *PluginChannelAdapter) InboundCh() <-chan channels.InboundMessage

InboundCh returns the buffered inbound stream the transport goroutines feed.

func (*PluginChannelAdapter) Send

Send proxies an outbound message (including Kind — e.g. "step" progress titles pass through verbatim; rendering is the plugin's business).

func (*PluginChannelAdapter) SetReplyMode

func (a *PluginChannelAdapter) SetReplyMode(mode string)

SetReplyMode records the mode and forwards it as a control notification (best-effort: a plugin that is mid-restart receives the mode on its next `start`, because the stored config is updated too).

func (*PluginChannelAdapter) Start

func (a *PluginChannelAdapter) Start(ctx context.Context) error

Start spawns the plugin process (via the supervisor, which waits for the `ready` handshake) and issues the `start` request carrying the channel config.

func (*PluginChannelAdapter) Status

Status returns the cached last status — it never blocks and never round-trips to the plugin (the plugin pushes status notifications; the supervisor overwrites it on restart/circuit-break).

func (*PluginChannelAdapter) Stop

func (a *PluginChannelAdapter) Stop() error

Stop requests a graceful plugin shutdown and reaps the process (stop request → brief wait → SIGTERM → SIGKILL).

func (*PluginChannelAdapter) Type

func (a *PluginChannelAdapter) Type() string

Type returns the plugin channel type. Known locally from the manifest — no round-trip (design 002 §4.1).

type PluginSupervisor

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

PluginSupervisor supervises one plugin process for one running channel.

func (*PluginSupervisor) Start

Start spawns the plugin, waits for ready, and issues the `start` request.

func (*PluginSupervisor) Stop

func (s *PluginSupervisor) Stop() error

Stop performs the clean-stop sequence: `stop` request → brief wait → SIGTERM → SIGKILL. The supervisor is terminal after Stop.

type Registry

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

Registry scans installed plugin manifests under a root directory. Lookups re-scan on demand: installs/removals in the same process (or by another process) are visible immediately, and the scan is a couple of stats.

func LoadRegistry

func LoadRegistry(root string) (*Registry, error)

LoadRegistry opens the registry rooted at root (DefaultRoot when empty). A missing root is not an error — the registry is just empty.

func (*Registry) List

func (r *Registry) List() []InstalledPlugin

List returns all installed plugins, sorted by name. Broken or builtin-colliding entries are skipped with a warning.

func (*Registry) Lookup

func (r *Registry) Lookup(name string) (InstalledPlugin, bool)

Lookup resolves an installed plugin by channel-type name. Names colliding with built-ins are never resolvable (second enforcement point after install-time validation), and the manifest must agree with its directory name.

func (*Registry) Root

func (r *Registry) Root() string

Root returns the registry root directory.

type SigStatus

type SigStatus int

SigStatus is the load-time signature verdict.

const (
	SigUnsigned   SigStatus = iota // no signature file — pre-signing behavior
	SigUnknownKey                  // valid signature, key not in the trust file — pre-signing behavior
	SigTrusted                     // valid signature by a trusted key
	SigInvalid                     // signature present but wrong — refuse to load
)

func (SigStatus) String

func (s SigStatus) String() string

type SignatureFile

type SignatureFile struct {
	Algo      string `json:"algo"`       // "ed25519"
	KeyID     string `json:"key_id"`     // KeyID(public key)
	PublicKey string `json:"public_key"` // hex, 32 bytes
	Signature string `json:"signature"`  // hex, over PluginDigest
}

SignatureFile is the JSON content of rysh.channel.sig. The public key is embedded so verification is self-contained; TRUST is decided separately by the trust file, never by the sig file itself.

func SignPluginDir

func SignPluginDir(dir string, priv ed25519.PrivateKey) (SignatureFile, error)

SignPluginDir signs the plugin package at dir (manifest + exec binary) with the private key and writes rysh.channel.sig next to the manifest.

type SupervisorOptions

type SupervisorOptions struct {
	// Dir is the plugin's install directory (.rysh/channels/{name}): a
	// relative exec resolves against it and it becomes the process's cwd.
	Dir string
	// TrustFile lists trusted signing keys (DefaultTrustFile when empty).
	// When Dir is set, spawn refuses a plugin whose signature fails
	// verification (SigInvalid); unsigned/unknown-key plugins spawn as
	// before. Dir=="" skips the check (ad-hoc/test adapters have no package
	// directory to verify).
	TrustFile string
	// ExtraEnv is appended to the child's environment.
	ExtraEnv []string
	// NATSConn is the core-side bus handle (required for transport=nats).
	NATSConn *nats.Conn
	// NATSURL is the loopback bus URL handed to a nats-transport plugin via
	// RYSH_PLUGIN_NATS_URL at spawn.
	NATSURL string

	InitialBackoff time.Duration // crash-restart initial backoff (default 2s)
	MaxBackoff     time.Duration // crash-restart backoff cap (default 60s)
	MaxFailures    int           // consecutive failed restarts before circuit-break (default 5)
	ReadyTimeout   time.Duration // max wait for the ready handshake (default 10s)
	StopGrace      time.Duration // per-stage wait during clean stop (default 2s)
}

SupervisorOptions configures process spawning and the restart discipline. Zero values take the documented defaults, so tests can shrink timings.

type VerifyResult

type VerifyResult struct {
	Status SigStatus
	KeyID  string
	Reason string
}

VerifyResult carries the verdict plus the key id (when a signature parsed) and the reason (when SigInvalid / SigUnknownKey).

func VerifyPlugin

func VerifyPlugin(dir string, m Manifest, trustFile string) VerifyResult

VerifyPlugin evaluates the plugin at dir against the trust file. It never returns a Go error: every failure mode maps onto a SigStatus so callers get exactly one enforcement decision (see the package-top semantics).

func (VerifyResult) TierString

func (r VerifyResult) TierString() string

TierString renders the verdict for `rysh channel list` / install records.

type WireOptions

type WireOptions struct {
	// Root of the plugin registry (DefaultRoot when empty).
	Root string
	// TrustFile lists trusted signing keys (DefaultTrustFile when empty).
	TrustFile string
	// NATS provides the session bus for nats-transport plugins; nil makes
	// nats plugins unresolvable (logged), stdio plugins still work.
	NATS NATSProvider
	// Supervisor is the base supervisor option set (backoffs etc.); Dir,
	// NATSConn, and NATSURL are filled per plugin at resolution time.
	Supervisor SupervisorOptions
}

WireOptions configures the channels-package hooks.

Jump to

Keyboard shortcuts

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