Documentation
¶
Overview ¶
Package plugin implements a Lua-based plugin host for hygge.
Overview ¶
Plugins are portable .lua scripts (or directories containing one) that register into hygge's existing tool / hook / command / subagent registries through a neovim-style hygge.* global module. Sources are declared in config.toml as github: or local: URIs and managed via `hygge plugins install/update/remove/list/show`.
Architecture ¶
The core abstractions are:
- Plugin: one loaded plugin. Implementations: luaPlugin today, subprocessPlugin reserved for later. The Registry owns plugins and dispatches lifecycle calls into them.
- Host: the bridge from a plugin into hygge's running state.
- Loader: the extension point that decides which runtime to use for a given plugin manifest.
- Registry: manages the set of installed plugins and their lifecycle.
Lua plugin lifecycle ¶
When a Lua plugin is loaded the script is executed top-to-bottom inside a sandboxed gopher-lua LState. During that initial execution the script calls hygge.register_tool / hygge.register_hook / etc. to declare its contributions. After Load returns, the registered adapters are live in the host registries. The LState is kept alive for the process lifetime; each adapter invocation acquires a per-plugin mutex before touching the LState.
Concurrency ¶
gopher-lua's LState is NOT safe for concurrent use. Each luaPlugin holds a single LState and a sync.Mutex that serialises all calls into it. Concurrent tool/hook/command invocations are queued per plugin. This is by design: plugin code is rarely the bottleneck, and the alternative (one LState per goroutine with shared state through channels) is vastly more complex.
Error handling ¶
Any Lua runtime error during a handler causes the handler to fail-open (return Allow / pass-through) with a slog.Warn — mirroring the shell-hook contract established in the hook package (T1.4). Errors during Load surface as proper Go errors so the Registry can skip the offending plugin without crashing the others.
Forward compatibility: subprocess plugins ¶
The Plugin and Host interfaces are implementation-agnostic. A future subprocess JSON-RPC loader can implement Plugin / Host and slot in without touching the Registry or any caller. The Loader interface is the declared extension point. A stub SubprocessLoader is included in this package to keep the abstraction honest; it always errors.
Index ¶
- func LuaLSTypeStub() []byte
- type CapabilityDecl
- type CommandRegistration
- type ExecOptions
- type ExecResult
- type HookRegistration
- type Host
- type Loader
- type LuaLoader
- type Manifest
- type OwnedRegistration
- type PackageManager
- type Plugin
- type PluginTool
- type PluginToolResult
- type Registry
- func (r *Registry) Close(ctx context.Context) error
- func (r *Registry) Get(name string) (Plugin, bool)
- func (r *Registry) Install(ctx context.Context, uri string) error
- func (r *Registry) List() []Plugin
- func (r *Registry) LoadAll(ctx context.Context, sourceURIs []string)
- func (r *Registry) OwnedRegistrations() []OwnedRegistration
- func (r *Registry) PM() *PackageManager
- func (r *Registry) Remove(ctx context.Context, name string) error
- func (r *Registry) SetInjectMessage(fn func(ctx context.Context, sessionID, role, content string) error)
- func (r *Registry) Source(name string) (Source, bool)
- func (r *Registry) UnregisterAll(pluginName string)
- func (r *Registry) Update(ctx context.Context, name string) error
- type RegistryOptions
- type Source
- type SourceKind
- type SubagentRegistration
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func LuaLSTypeStub ¶ added in v0.4.1
func LuaLSTypeStub() []byte
LuaLSTypeStub returns Hygge's LuaLS/LuaCATS definition file for plugin authors. Callers receive a copy so the embedded bytes cannot be mutated.
Types ¶
type CapabilityDecl ¶
type CapabilityDecl struct {
Tools bool `toml:"tools"`
Hooks bool `toml:"hooks"`
Commands bool `toml:"commands"`
Subagents bool `toml:"subagents"`
Messages bool `toml:"messages"`
}
CapabilityDecl is the optional [capabilities] block in plugin.toml.
type CommandRegistration ¶
type CommandRegistration struct {
// Name is the command name without the leading slash.
Name string
// Description is the one-line summary.
Description string
// Args declares the named arguments.
Args []command.ArgSpec
// Execute runs the command. Returns the outcome text (surfaced as
// Outcome.Message).
Execute func(ctx context.Context, input string) (string, error)
}
CommandRegistration describes a slash command being registered by a plugin.
type ExecOptions ¶
type ExecOptions struct {
// Env is extra environment variables to pass. Merged on top of
// the parent's environment filtered through procenv.Allowlist.
Env map[string]string
// Dir is the working directory; empty means the session's pwd.
Dir string
// Timeout caps the subprocess runtime. Zero means 30 s.
Timeout time.Duration
}
ExecOptions configures a plugin Exec call.
type ExecResult ¶
ExecResult holds the result of a plugin Exec call.
type HookRegistration ¶
type HookRegistration struct {
// Name is the unique hook name.
Name string
// Event is the hook.Event this hook fires for.
Event hook.Event
// Mode is the hook execution mode. Defaults to hook.ModeSync.
Mode hook.Mode
// Timeout is the per-invocation timeout. Defaults to 5 s.
Timeout time.Duration
// Handler is the Go function to invoke.
Handler func(ctx context.Context, in hook.Input) (hook.Action, error)
}
HookRegistration describes a hook being registered by a plugin.
type Host ¶
type Host interface {
// PluginName returns the calling plugin's name, used for logging and
// rate-limit accounting. Each plugin's bindings carry their own Host
// wrapper that fills this in.
PluginName() string
RegisterTool(PluginTool) error
RegisterHook(HookRegistration) error
RegisterCommand(CommandRegistration) error
RegisterSubagent(SubagentRegistration) error
// SendMessage injects a message into sessionID with the given role and
// content. Role must be "user" or "assistant". Plugin calls are rate-
// limited to 10 per turn to prevent runaway loops.
SendMessage(ctx context.Context, sessionID, role, content string) error
// Notify emits a user-visible notification. level is "info", "warn",
// or "error".
Notify(level, message string)
// Log writes a structured log entry under the plugin's name.
Log(level, message string, fields map[string]any)
// Exec runs a subprocess. The call goes through the permission engine
// under CategoryShell so a plugin cannot bypass user policy.
Exec(ctx context.Context, command string, args []string, opts ExecOptions) (ExecResult, error)
// Config returns the [plugins.<plugin-name>] TOML table as a generic
// map. Empty when no overrides are set.
Config() map[string]any
// ProfileDir returns the resolved active profile directory, or "" when
// no named profile is active. Plugins can use this to load files that
// live adjacent to the active profile config.
ProfileDir() string
}
Host is the bridge from a plugin into hygge's running state.
Every Plugin implementation receives a Host at Load time. The Lua loader implements gopher-lua bindings that route to Host; a future subprocess loader would implement a JSON-RPC dispatcher that does the same.
Host MUST be safe for concurrent use.
type Loader ¶
type Loader interface {
// CanLoad inspects the manifest and the files at dir and returns true
// if this loader handles them. Must not read the entrypoint file.
CanLoad(dir string, m Manifest) bool
// Load creates and initialises a Plugin. Called after CanLoad returns
// true. The returned Plugin has not yet had Load(ctx, h) called on it;
// that is the caller's responsibility.
Load(name, source, dir string, m Manifest) (Plugin, error)
}
Loader is the extension point that decides which plugin runtime to use.
For v0.3 only LuaLoader is provided. A future SubprocessLoader would handle non-.lua entrypoints via a JSON-RPC subprocess protocol.
The Registry tries each registered Loader in order and uses the first one that reports CanLoad = true. If no loader matches, plugin load fails with a clear error.
type LuaLoader ¶
type LuaLoader struct{}
LuaLoader is the concrete loader for .lua-based plugins.
type Manifest ¶
type Manifest struct {
// Name is the unique plugin identifier. Derived from the manifest
// file or synthesised from the source URI.
Name string `toml:"name"`
// Version is a human-facing version string (e.g. "1.2.3").
// Optional.
Version string `toml:"version"`
// Description is the one-line summary. Optional.
Description string `toml:"description"`
// Entrypoint is the path to the entry .lua file, relative to the
// manifest directory. Defaults to "plugin.lua" when absent.
Entrypoint string `toml:"entrypoint"`
// Capabilities is an optional declaration of which hygge APIs the
// plugin uses. UX-only; registrations happen at runtime regardless.
Capabilities CapabilityDecl `toml:"capabilities"`
// contains filtered or unexported fields
}
Manifest is the parsed content of a plugin.toml manifest file.
Single-file plugins (a lone plugin.lua at the root of a repo) do not require a manifest; the loader synthesises a trivial one with Name = basename of the source URI and Entrypoint = "plugin.lua".
func ParseManifest ¶
ParseManifest reads and validates a plugin.toml file.
func SynthesiseManifest ¶
SynthesiseManifest creates a minimal Manifest for a single-file plugin. name is the plugin name (typically the source URI basename).
func (Manifest) Synthesised ¶
Synthesised reports whether this manifest was created by the loader (single-file plugin, no explicit plugin.toml).
type OwnedRegistration ¶
type OwnedRegistration struct {
Owner string // plugin name; "" for builtins
Name string
Kind string // "tool", "hook", "command", "subagent"
}
OwnedRegistration tracks which plugin registered a tool/hook/command/subagent so they can be cleaned up in a future reload operation.
type PackageManager ¶
type PackageManager struct {
// contains filtered or unexported fields
}
PackageManager handles plugin source resolution and cache management.
func NewPackageManager ¶
func NewPackageManager(cacheDir string) *PackageManager
NewPackageManager constructs a PackageManager using the given cacheDir. Typically $XDG_STATE_HOME/hygge/plugins.
func NewPackageManagerWithGitRunner ¶ added in v0.4.0
func NewPackageManagerWithGitRunner(cacheDir string, runner gitexec.Runner) *PackageManager
NewPackageManagerWithGitRunner constructs a PackageManager with an injected git runner. Passing nil uses the production non-interactive runner.
func (*PackageManager) CacheDir ¶
func (pm *PackageManager) CacheDir(src Source) string
CacheDir returns the cache directory for the given source.
func (*PackageManager) Remove ¶
func (pm *PackageManager) Remove(src Source) error
Remove deletes the plugin's cache directory. For local sources it's a no-op (we never copy local dirs into the cache).
func (*PackageManager) Resolve ¶
Resolve ensures the plugin source is available on disk and returns the directory containing the plugin files (plugin.toml + entry .lua).
For local: sources, the path is returned directly (no copy). For github: sources, the repo is cloned into the cache on first call and the cache path is returned.
type Plugin ¶
type Plugin interface {
// Name returns the plugin's unique identifier (derived from the manifest
// or the source URI basename).
Name() string
// Source returns the URI the plugin was installed from, e.g.
// "github:cfbender/hygge-policy-guard" or
// "local:/Users/cfb/code/my-plugin".
Source() string
// Manifest returns the parsed manifest for this plugin.
Manifest() Manifest
// Load runs the plugin's initialisation. For Lua this executes the
// script top-to-bottom in its sandbox. A future subprocess plugin
// would spawn the process and exchange the initialize handshake.
//
// Load is called exactly once, before any dispatch. Calling Load twice
// is a programmer error.
Load(ctx context.Context, h Host) error
// Close releases resources. For Lua, closes the LState; for a
// subprocess, sends shutdown and force-kills on timeout. Idempotent.
Close(ctx context.Context) error
}
Plugin is one loaded plugin.
Implementations: luaPlugin today, subprocessPlugin tomorrow. The Registry owns plugins and dispatches lifecycle calls into them.
Implementations need not be safe for concurrent calls to non-event methods (Load, Close). Execute/handler dispatch methods MUST be safe for concurrent use since multiple sessions may invoke a plugin simultaneously — the luaPlugin implementation satisfies this through a per-plugin mutex.
type PluginTool ¶
type PluginTool struct {
// Name is the stable identifier the model uses to invoke this tool.
// Must match [a-z][a-z0-9_]*.
Name string
// Description is the human-language summary surfaced to the model.
Description string
// InputSchema is a JSON Schema object for the model. When nil, an
// empty object schema is synthesised.
InputSchema json.RawMessage
// Parallelizable controls whether the tool may be invoked concurrently
// with other parallelizable tools in the same turn. Defaults to false
// for safety — plugin tools should only set this to true when their
// Execute function has no observable side effects beyond reading state
// (e.g. searching a local index, calling a read-only external API).
//
// Plugin tools registered with Parallelizable = true are subject to the
// same concurrency semantics as built-in parallel tools: their bus events
// arrive in undefined order relative to sibling parallel calls, and the
// gopher-lua LState mutex serialises execution within a single Lua plugin
// even when Parallelizable is true.
Parallelizable bool
// Execute runs the tool.
Execute func(ctx context.Context, input json.RawMessage) (PluginToolResult, error)
}
PluginTool describes a tool being registered by a plugin.
type PluginToolResult ¶
PluginToolResult is the outcome of a plugin tool call.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry manages the set of installed plugins and dispatches lifecycle calls.
func NewRegistry ¶
func NewRegistry(opts RegistryOptions) (*Registry, error)
NewRegistry constructs an empty Registry.
func (*Registry) Install ¶
Install resolves, validates, and loads a plugin from the given source URI. Unlike LoadAll (which silently skips failures), Install returns an error so the CLI can surface it to the user.
func (*Registry) LoadAll ¶
LoadAll loads every plugin from the given list of source URIs. Failures in individual plugins are logged and skipped — one bad plugin should not block the rest from loading.
func (*Registry) OwnedRegistrations ¶
func (r *Registry) OwnedRegistrations() []OwnedRegistration
OwnedRegistrations returns a copy of the owned registration list. Useful for auditing which plugin contributed which tool/hook/command/subagent.
func (*Registry) PM ¶
func (r *Registry) PM() *PackageManager
PM returns the underlying PackageManager, for CLI commands that need to inspect cache directories.
func (*Registry) SetInjectMessage ¶
func (r *Registry) SetInjectMessage(fn func(ctx context.Context, sessionID, role, content string) error)
SetInjectMessage wires the InjectMessage callback after construction. This allows the plugin registry to be created before the agent is available (avoiding a circular dependency), with the callback set once the agent is constructed.
func (*Registry) UnregisterAll ¶ added in v0.4.0
UnregisterAll removes every host registration owned by pluginName from the tool, hook, slash-command, and subagent registries. It is safe to call for a plugin that has no tracked registrations.
type RegistryOptions ¶
type RegistryOptions struct {
// CacheDir is the root directory for plugin caches.
// Defaults to $XDG_STATE_HOME/hygge/plugins.
CacheDir string
// ToolRegistry receives plugin-registered tools. Required.
Tools *tool.Registry
// HookRegistry receives plugin-registered hooks. Required.
Hooks *hook.Registry
// CommandRegistry receives plugin-registered commands. Required.
Commands *command.Registry
// SubagentRegistry receives plugin-registered subagent types. Required.
Subagents *subagent.Registry
// Permission is the engine used by Exec calls from plugins. Required.
Permission *permission.Engine
// InjectMessage, when non-nil, is called by plugins that invoke
// hygge.send_message. Typically wired to agent.Agent.InjectMessage.
InjectMessage func(ctx context.Context, sessionID, role, content string) error
// PluginConfigs maps plugin names to their [plugins.<name>] TOML tables.
PluginConfigs map[string]map[string]any
// ProfileDir is the resolved active profile directory. Plugins can use
// this to load files that live next to the profile config. May be empty
// when no profile is active or the profile is the unnamed default.
ProfileDir string
// Pwd is the working directory for Exec calls.
Pwd string
// Loaders is the ordered set of loaders to try. When nil,
// defaultLoaders() is used.
Loaders []Loader
}
RegistryOptions configures a plugin Registry.
type Source ¶
type Source struct {
Kind SourceKind
Raw string // the original URI string
User string // github only: owner
Repo string // github only: repository
Path string // local only: filesystem path (absolute)
Ref string // github only: tag, branch, or commit sha. "" = default branch
Branch bool // true when Ref was specified with # (known branch, not tag)
}
Source represents a parsed plugin source URI.
func ParseSource ¶
ParseSource parses a plugin source URI.
Grammar:
github:USER/REPO → default branch github:USER/REPO@REF → tag, commit, or branch via @ separator github:USER/REPO#BRANCH → explicitly a branch (use # to distinguish from tags) local:/abs/path → no clone, use path directly npm:... → error: not supported in v0.3
func (Source) CacheDirName ¶
CacheDirName returns a safe filesystem directory name for this source. Format: <scheme>-<user-or-hash>-<repo>-<ref>
type SourceKind ¶
type SourceKind string
SourceKind is the type of plugin source URI.
const ( // SourceGitHub is a plugin sourced from a GitHub repository. SourceGitHub SourceKind = "github" // SourceLocal is a plugin sourced from a local filesystem path. SourceLocal SourceKind = "local" )
Source kind constants.
type SubagentRegistration ¶
type SubagentRegistration struct {
// Name is the stable identifier.
Name string
// Description is the one-line summary.
Description string
// SystemPrompt is the full system prompt.
SystemPrompt string
// Tools is the tool-name allowlist. Empty means "default sub-agent
// tools".
Tools []string
// Model, when non-empty, overrides the parent's provider/model.
// Shape: "<provider>/<model-id>".
Model string
}
SubagentRegistration describes a sub-agent type being registered by a plugin.