Documentation
¶
Overview ¶
Package plugins implements knot's plugin system: a plugin is a folder in the configured PluginsPath whose declarations — permissions, menus, pages, handlers, MCP tools, logos, icons — live statically in a [tool.knot] table. Nothing a plugin declares is produced by executing plugin code; loading is pure parsing, and plugin code only ever runs per-invocation in a user-bound environment.
The [tool.knot] table is sourced two ways, parsed identically:
- Pure-script plugin: knot reads main.py and parses its metadata block.
- Peer plugin: a bin/ peer (a scriptling plugin-protocol executable in any language) returns the table as static manifest data in its handshake (Custom["tool.knot"]); knot reads it there. Such a plugin needs no companion main.py. A peer plugin may still ship a main.py for scriptling handlers, in which case main.py's block is authoritative.
Handlers a plugin declares are addressed as plugin.<name>.<function>: a peer exports them over the plugin protocol, a main.py exposes them by being registered as the plugin.<name> library — the dispatch layer calls the qualified name and does not care which kind answers.
A peer is a component of the plugin: it is spawned and handshaked at load so its manifest and version are available for declaration parsing and requirement verification. Whether it also registers a callable surface depends on the plugin's handler declarations.
Index ¶
- func MCPToolParamSchemaType(declared string) string
- func PluginEnvLibraries() []string
- func QualifiedPermission(pluginName, id string) string
- func ScriptNamespace(pluginName string) string
- func SetRegistry(r *Registry)
- func ValidHandlerName(name string) bool
- type FailedPlugin
- type FieldHandler
- type Handler
- type MCPTool
- type MCPToolParameter
- type Menu
- type Page
- type Peer
- type PermissionDecl
- type Plugin
- type Registry
- func (r *Registry) All() []*Plugin
- func (r *Registry) ByName(pluginName string) *Plugin
- func (r *Registry) Close()
- func (r *Registry) DefaultPageURL() string
- func (r *Registry) Failed() []FailedPlugin
- func (r *Registry) FieldHandler(id string) (*Plugin, *FieldHandler)
- func (r *Registry) FieldHandlers() []FieldHandler
- func (r *Registry) Page(pluginName, pagePath string) (*Plugin, *Page)
- func (r *Registry) Peers(p *Plugin) []Peer
- func (r *Registry) Present() bool
- func (r *Registry) SiteLogoURLs() (light, dark string)
- func (r *Registry) VisibleMenuURLs(user *model.User) map[string]bool
- func (r *Registry) VisibleMenus(user *model.User) []Menu
- func (r *Registry) Warnings() []string
- type ScriptExport
- type ScriptLib
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func MCPToolParamSchemaType ¶
MCPToolParamSchemaType maps a declared parameter type to its JSON schema type; unknown types map to "string".
func PluginEnvLibraries ¶
func PluginEnvLibraries() []string
PluginEnvLibraries lists the library names plugin handler environments register — the same set metadata dependency resolution resolves against. Exported for the drift guard test (service package), which builds a real plugin env and asserts every name imports.
func QualifiedPermission ¶
QualifiedPermission builds the canonical grant for a plugin permission id.
func ScriptNamespace ¶
ScriptNamespace maps a plugin folder name to the scriptling module identifier its main.py is registered under (plugin.<ScriptNamespace>). Folder names may contain "-" (a legal plugin name) but module names may not, so hyphens become underscores; already-valid names are unchanged. Deterministic, so every node computes the same import name.
func SetRegistry ¶
func SetRegistry(r *Registry)
SetRegistry replaces the global registry (tests; Load is the normal path).
func ValidHandlerName ¶
ValidHandlerName reports whether name is a legal handler reference (a function name or module.function path) — the same shape [tool.knot] pages and field_handlers declare, and what handler-URL dispatch accepts.
Types ¶
type FailedPlugin ¶
type FailedPlugin struct {
Name string `json:"name"`
Path string `json:"path"`
Reason string `json:"reason"`
}
FailedPlugin records a plugin whose folder/file was present but which failed validation or requirements; its data (when storage exists) is retained and it is surfaced on the admin plugins page.
type FieldHandler ¶
type FieldHandler struct {
Id string // qualified: plugin.<name>.<id>
Label string
Handler string // function in the entry file
// Permission is an optional additive gate on the options endpoint:
// UseSpaces (space forms drive the fetches) always applies, and a
// declared gate narrows who may invoke the handler further.
Permission string `json:"permission,omitempty"` // qualified grant
}
Page is one declared page: an internal URL under /plugins/<name> whose handler runs per-request as the requesting user. Handler is a function in the entry file, or "module.fn" for a sibling module of a folder plugin. FieldHandler is a declared data source a template's custom fields can bind to: the space form turns the field into an autocompleter whose suggestions come from calling Handler with {_data: id, query: ...}.
type Handler ¶
type Handler struct {
Handler string `json:"handler"` // function name or module.function
Permission string `json:"permission,omitempty"` // qualified grant, as Page; empty: any logged-in user
}
Handler is a declared handler gate: one [[tool.knot.handlers]] entry. The declared permission is the handler's gate wherever it is called — same semantics as row/column gates — overriding the calling page's. A declaration is also the opt-in for plugin-root addressability (/plugins/<name>/<handler>): undeclared handlers inherit the calling page's gate and are only reachable through a page path.
type MCPTool ¶
type MCPTool struct {
Name string `json:"name"` // MCP tool name; defaults to the handler name
Description string `json:"description"` // shown to MCP clients
Handler string `json:"handler"` // function in the entry file
Permission string `json:"permission,omitempty"` // qualified grant, as Handler
Parameters []MCPToolParameter `json:"parameters,omitempty"` // optional: the tool's input schema
}
MCPTool is one [[tool.knot.mcp_tools]] entry: a plugin handler exposed as an MCP tool. The optional permission narrows which users see and may call the tool (knot enforces at both list and execute time); both empty means any MCP user. No input schema is declared — the tool's parameters arrive in the handler's params dict and the MCP schema is an empty object.
type MCPToolParameter ¶
type MCPToolParameter struct {
Name string `json:"name"`
Type string `json:"type"` // string, int, float, bool, list
Description string `json:"description,omitempty"`
Default any `json:"default,omitempty"`
Required bool `json:"required,omitempty"`
}
MCPToolParameter is one declared tool parameter: what MCP clients (and LLMs) see in the tool's input schema. Parameters still arrive in the handler's params dict untyped — the declaration is documentation and client ergonomics, not coercion.
type Menu ¶
type Menu struct {
PluginName string `json:"plugin,omitempty"`
Label string `json:"label"`
URL string `json:"url"`
Permission string `json:"permission,omitempty"`
Icon string `json:"icon,omitempty"`
IconSVG string `json:"-"`
}
Menu is one declared sidebar entry. Permission, when set, is the fully qualified grant (plugin.<name>.<id>) that gates the item. Icon is a relative path to an SVG asset in the plugin folder; IconSVG holds its sanitized inner markup, rendered inline with the site's icon styling so a currentColor-stroked SVG themes like every built-in icon.
type Page ¶
type Page struct {
PluginName string `json:"plugin,omitempty"`
Path string `json:"path"` // "/dashboard" — served at /plugins/<name>/dashboard
Handler string `json:"handler"` // function in the entry file ("module.fn" allowed)
Label string `json:"label,omitempty"` // page title
MenuLabel string `json:"menu_label,omitempty"` // set: the page also appears in the sidebar under this label
Permission string `json:"permission,omitempty"` // qualified grant, as Menu
Icon string `json:"icon,omitempty"`
IconSVG string `json:"-"`
// Default marks the page as the post-login landing page. At most one
// takes effect (first plugin by name, then declaration order) — see
// DefaultPageURL.
Default bool `json:"default,omitempty"`
}
type Peer ¶
type Peer struct {
Name string `json:"name"`
Version string `json:"version"`
Healthy bool `json:"healthy"`
Error string `json:"error,omitempty"`
}
Peer reports one loaded binary peer's handshake identity.
type PermissionDecl ¶
type PermissionDecl struct {
Id string `json:"id"` // fully qualified, e.g. plugin.metrics.read
Label string `json:"label"` // derived display name, e.g. "read"
}
PermissionDecl is one declared permission, qualified to plugin.<name>.<id>.
type Plugin ¶
type Plugin struct {
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description"`
// APIVersion is the plugin system generation this plugin targets
// ([tool.knot] api; absent = 1, the only generation today). Breaking
// changes to the plugin contract — the [tool.knot] shape, the handler
// request/response contract, the block document — happen only behind a
// new generation, and a knot that doesn't know a generation rejects the
// plugin loudly at load rather than mis-parsing it.
APIVersion int `json:"api,omitempty"`
Dir string `json:"-"` // the plugin folder
EntryFile string `json:"-"` // main.py
LogoLight string `json:"logo_light,omitempty"` // relative paths
LogoDark string `json:"logo_dark,omitempty"`
// SiteLogo reports that the plugin's logo pair claims the main page
// logo (login included) — a declared pair is the claim. At most one
// plugin's claim takes effect — see SiteLogoURLs.
SiteLogo bool `json:"site_logo,omitempty"`
Permissions []PermissionDecl `json:"permissions"`
Menus []Menu `json:"menus"`
Pages []Page `json:"pages"`
Handlers []Handler `json:"handlers"`
MCPTools []MCPTool `json:"mcp_tools"`
FieldHandlers []FieldHandler `json:"field_handlers"`
Libs []ScriptLib `json:"libs,omitempty"`
// ActionIcons maps declared icon asset paths to their sanitized inner
// SVG markup ([tool.knot] icons). Menus and pages carry their icons
// statically; row actions are data-driven, so their icons are declared
// once here, sanitized at load like every other plugin asset, and
// addressable by path from a handler's action JSON at runtime.
ActionIcons map[string]string `json:"icons,omitempty"`
// Assets holds the declared assets a peer served from its own fetcher
// (an embedded-assets binary plugin), fetched once at load and served
// from memory. Empty when every asset came from the plugin folder on
// disk — those keep streaming from disk as before.
Assets map[string][]byte `json:"-"`
// RequiredConfig lists the config keys this plugin requires in the
// server's [plugins.<name>] table, declared as [tool.knot] config — a
// list of key names. Presence is validated at load: a missing key
// fails the plugin with a reason on the admin page rather than a
// handler failing at first use.
RequiredConfig []string `json:"required_config,omitempty"`
// Config is the plugin's [plugins.<name>] table from the server
// configuration, attached at load and injected into every handler
// request as request["config"] (an empty table when unconfigured).
Config map[string]any `json:"-"`
// Exports are the modules declared in [tool.knot] export: scriptling
// modules materialized in user tool environments as
// plugin.<name>.<stem> — client SDKs wrapping knot.plugin.call. The
// first declared module is also aliased as plugin.<name> itself. Each
// source is read and linted once at load.
Exports []ScriptExport `json:"exports,omitempty"`
// EntrySource is the entry file's source, read once at load so request
// dispatch does not touch the filesystem. Empty for a peer plugin with
// no main.py.
EntrySource string `json:"-"`
// ScriptNamespace is the plugin.<x> library name a pure-script plugin's
// main.py is registered and addressed under: the folder name sanitized
// to a scriptling identifier (ScriptNamespace). Empty for a peer plugin
// with no main.py. This is the "<name>" a bare handler declaration
// (handler = "status") resolves against.
ScriptNamespace string `json:"-"`
// Namespaces are the plugin.<x> library names this plugin's declared
// handlers resolve under, in the order they must be imported to
// materialize the plugin.<x> bindings for dispatch. A pure-script
// plugin contributes its folder name (its main.py registers as
// plugin.<folder>); a peer plugin contributes each peer's handshake
// name (the peer registers plugin.<peer>). A plugin with both
// contributes both.
Namespaces []string `json:"-"`
// contains filtered or unexported fields
}
Plugin is a successfully loaded plugin: its inert registration data.
func (*Plugin) DefaultNamespace ¶
DefaultNamespace is the plugin.<ns> a bare handler declaration resolves against: the ScriptNamespace when the plugin has a main.py (its handlers live there), otherwise its first peer namespace (a peer plugin's handlers are the peer's exports). Empty only for a plugin with neither, which cannot declare a callable handler. A manifest may still name a specific peer explicitly as plugin.<peer>.<fn> to disambiguate multiple peers.
func (*Plugin) HandlerDecl ¶
HandlerDecl returns the declared gate for a handler name, or nil when the handler has no [[tool.knot.handlers]] entry.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the in-memory result of one boot-time scan. Replace-on-boot, never mutated after Load returns.
func GetRegistry ¶
func GetRegistry() *Registry
GetRegistry returns the boot-time registry, or nil when plugins are not configured / not yet loaded.
func Load ¶
Load scans the plugins path and builds the global registry. PluginsPath empty or missing disables plugins (nil registry, no error). Failures are per-plugin: a broken plugin is recorded and the server continues.
func LoadWithConfigs ¶
LoadWithConfigs is Load with per-plugin configuration: each plugin's [plugins.<name>] table from the server config, attached to the plugin and injected into every handler request as request["config"]. Validation happens at load so a misconfigured server fails the plugin with a reason on the admin page: a table over the size cap, or a key the plugin declared in [tool.knot] config missing. A section naming no loaded plugin is reported as a warning — stale config left behind by a removed plugin.
func (*Registry) Close ¶
func (r *Registry) Close()
Close shuts down every spawned peer process. Called on server shutdown.
func (*Registry) DefaultPageURL ¶
DefaultPageURL returns the URL of the page claimed as the post-login landing page, or "" when no plugin claims one. Plugins are name-sorted, so the first claimant by name wins deterministically on every node.
func (*Registry) Failed ¶
func (r *Registry) Failed() []FailedPlugin
Failed returns the plugins that were present but failed to load.
func (*Registry) FieldHandler ¶
func (r *Registry) FieldHandler(id string) (*Plugin, *FieldHandler)
FieldHandler looks a field handler up by its qualified id.
func (*Registry) FieldHandlers ¶
func (r *Registry) FieldHandlers() []FieldHandler
FieldHandlers lists every declared field handler across plugins.
func (*Registry) Page ¶
Page finds a loaded plugin and its declared page by URL path (the path after /plugins/<name>, e.g. "/dashboard"). Returns nils when unknown.
func (*Registry) Present ¶
Present reports whether the plugins path produced anything at all — loaded plugins, failed plugins, or load warnings. When false, the plugin surface is hidden entirely: no nav item, no admin page, no search entry.
func (*Registry) SiteLogoURLs ¶
SiteLogoURLs returns the served URLs of the plugin logos that replace the main page logo, or empty strings when no plugin declares a pair. All() is name-sorted, so the alphabetically first claim wins deterministically — every node scanning the same folder computes the same winner. An explicit server.ui.logo_url config beats any plugin (the caller applies that).
func (*Registry) VisibleMenuURLs ¶
VisibleMenuURLs is the set of URLs VisibleMenus yields, for membership checks (pin validation).
func (*Registry) VisibleMenus ¶
VisibleMenus returns the menu items the given user may see, gated per item exactly as the sidebar renders them: a declared permission requires one of the user's roles to carry the qualified plugin grant; a group requires membership. This is the single gate — the nav builder, the nav-preferences validation, and search all ask the same question, so "pinnable" and "visible" can never drift apart.
type ScriptExport ¶
ScriptExport is one module declared in [tool.knot] export: its path as declared and its source, read and linted once at plugin load.
type ScriptLib ¶
type ScriptLib struct {
Name string `json:"name"` // from the filename, e.g. libs/calc.py -> "calc"
Version string `json:"version"` // [tool.knot.lib] version, default "1.0"
Source string `json:"-"`
}
ScriptLib is a scriptling-authored library: a .py file in the plugin's libs/ folder, loaded in-process by knot's embedded scriptling (no CLI, no subprocess). Its public surface — functions, classes and constants not prefixed with "_" — becomes the plugin.<name> import in handler environments and user-created tools. Version comes from the optional [tool.knot.lib] table in the library's metadata block, and the consuming plugin's dependency declaration ("plugin.<lib> via <lib> >= x") validates against it the same way Go peer handshakes do.