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
- func AppendTrustedKey(path, hexKey, comment string) error
- func ConsentText(m Manifest) string
- func GenerateSigningKey(path string) (ed25519.PublicKey, error)
- func KeyID(pub ed25519.PublicKey) string
- func LoadSigningKey(path string) (ed25519.PrivateKey, error)
- func LoadTrustedKeys(path string) (keys []ed25519.PublicKey, bad []string, err error)
- func PluginDigest(dir string, m Manifest) ([]byte, error)
- func Remove(root, name string) error
- func Unwire()
- func VerifyChecksum(path, expected string) error
- func Wire(o WireOptions)
- type InstallRecord
- type InstalledPlugin
- type Manifest
- type NATSProvider
- type PluginChannelAdapter
- func (a *PluginChannelAdapter) InboundCh() <-chan channels.InboundMessage
- func (a *PluginChannelAdapter) Send(ctx context.Context, outbound channels.OutboundMessage) error
- func (a *PluginChannelAdapter) SetReplyMode(mode string)
- func (a *PluginChannelAdapter) Start(ctx context.Context) error
- func (a *PluginChannelAdapter) Status() msg.ChannelStatus
- func (a *PluginChannelAdapter) Stop() error
- func (a *PluginChannelAdapter) Type() string
- type PluginSupervisor
- type Registry
- type SigStatus
- type SignatureFile
- type SupervisorOptions
- type VerifyResult
- type WireOptions
Constants ¶
const ( TransportNATS = "nats" TransportStdio = "stdio" )
Transport values a manifest may declare.
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" )
const DefaultRoot = ".rysh/channels"
DefaultRoot is the project-local registry directory.
const InstallRecordFileName = "install.json"
InstallRecordFileName is the per-plugin install record.
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 ¶
AppendTrustedKey adds a hex public key (+ optional comment) to the trust file, creating it if needed.
func ConsentText ¶
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 ¶
GenerateSigningKey creates an ed25519 keypair and writes the seed to path (0600). It refuses to overwrite an existing key file.
func KeyID ¶
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 ¶
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 ¶
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 VerifyChecksum ¶
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 ¶
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 ¶
LoadManifest reads and validates dir/rysh.channel.toml.
func ParseManifest ¶
ParseManifest decodes and validates a rysh.channel.toml document.
func ReadPackageManifest ¶
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 ¶
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 ¶
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 ¶
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 ¶
func (a *PluginChannelAdapter) Send(ctx context.Context, outbound channels.OutboundMessage) error
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 ¶
func (a *PluginChannelAdapter) Status() msg.ChannelStatus
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 ¶
func (s *PluginSupervisor) Start(ctx context.Context, cfg msg.ChannelConfig) error
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 ¶
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.
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 ¶
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.