plugins

package
v0.34.0 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func MCPToolParamSchemaType

func MCPToolParamSchemaType(declared string) string

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

func QualifiedPermission(pluginName, id string) string

QualifiedPermission builds the canonical grant for a plugin permission id.

func ScriptNamespace

func ScriptNamespace(pluginName string) string

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

func ValidHandlerName(name string) bool

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 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"`
}

func (Page) URL

func (pg Page) URL() string

URL is the served location of the page.

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.
	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

func (p *Plugin) DefaultNamespace() string

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

func (p *Plugin) HandlerDecl(name string) *Handler

HandlerDecl returns the declared gate for a handler name, or nil when the handler has no [[tool.knot.handlers]] entry.

func (*Plugin) Scope

func (p *Plugin) Scope() *plugin.Manager

Scope returns the manager owning this plugin's binary peers (which the handler environment registers as plugin.* libraries), or nil.

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

func Load(pluginsPath string) (*Registry, error)

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

func LoadWithConfigs(pluginsPath string, configs map[string]map[string]any) (*Registry, error)

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) All

func (r *Registry) All() []*Plugin

All returns the successfully loaded plugins in name order.

func (*Registry) ByName

func (r *Registry) ByName(pluginName string) *Plugin

ByName returns the loaded plugin with that name, or nil.

func (*Registry) Close

func (r *Registry) Close()

Close shuts down every spawned peer process. Called on server shutdown.

func (*Registry) DefaultPageURL

func (r *Registry) DefaultPageURL() string

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

func (r *Registry) Page(pluginName, pagePath string) (*Plugin, *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) Peers

func (r *Registry) Peers(p *Plugin) []Peer

Peers reports the binary peers loaded for a plugin, with health.

func (*Registry) Present

func (r *Registry) Present() bool

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

func (r *Registry) SiteLogoURLs() (light, dark string)

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

func (r *Registry) VisibleMenuURLs(user *model.User) map[string]bool

VisibleMenuURLs is the set of URLs VisibleMenus yields, for membership checks (pin validation).

func (*Registry) VisibleMenus

func (r *Registry) VisibleMenus(user *model.User) []Menu

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.

func (*Registry) Warnings

func (r *Registry) Warnings() []string

Warnings returns non-fatal load warnings (ignored files, unloadable binaries that no plugin declared a requirement on, …).

type ScriptExport

type ScriptExport struct {
	Path   string `json:"path"`
	Source string `json:"-"`
}

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.

Jump to

Keyboard shortcuts

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