Documentation
¶
Overview ¶
Package pluginhost connects the WASM plugin runtime (internal/wasmhost) to the rest of the core: it loads plugins from a directory, maps their exports onto the hooks engine, and keeps the settings store and the adapters that turn core events into SDK hook payloads.
Package pluginhost holds the host-side glue between the core and the plugin SDK: settings storage, service adapters, manifest validation and extension bridges.
Index ¶
- Constants
- Variables
- func AgentFirstCrawlObserver(e *hooks.Engine) func(agents.FirstCrawl)
- func AgentHitObserver(e *hooks.Engine) func([]agents.Hit)
- func BeforeSave(engine *hooks.Engine) entity.BeforeSaveFunc
- func BindHostHooks(e *hooks.Engine)
- func CompileSchema(schema []byte) (*jsonschema.Schema, error)
- func DecodeArgs(raw json.RawMessage) ([]any, error)
- func ExtSources(h *hooks.Engine) render.ExtSourceFunc
- func HeadFilter(h *hooks.Engine) render.HeadFilter
- func HostFunctionsFor(perms []sdk.Permission) []string
- func MaskWriteOnly(schema []byte, v json.RawMessage) json.RawMessage
- func MergeDefaults(schema []byte, stored json.RawMessage) json.RawMessage
- func PluginCookieName(plugin, name string) string
- func PreserveWriteOnly(schema []byte, old, in json.RawMessage) json.RawMessage
- func SDKRefusal(m sdk.Manifest) error
- func SchemaName(plugin string) string
- func WriteOnlySet(schema []byte, stored json.RawMessage) map[string]bool
- type API
- type Assets
- type Caller
- type Config
- type EntityPaths
- type EntitySink
- type FetchRequest
- type FetchResponse
- type Fetcher
- type FieldError
- type GrantStore
- type Grants
- type Lookup
- type Mailer
- type Migration
- type PageFilter
- type PluginDB
- func (d *PluginDB) Close()
- func (d *PluginDB) Drop(ctx context.Context, plugin string) error
- func (d *PluginDB) Exec(ctx context.Context, plugin, sql string, args ...any) (int64, error)
- func (d *PluginDB) KVDelete(ctx context.Context, plugin, key string) error
- func (d *PluginDB) KVGet(ctx context.Context, plugin, key string) (json.RawMessage, bool, error)
- func (d *PluginDB) KVList(ctx context.Context, plugin, prefix string, limit int) ([]string, error)
- func (d *PluginDB) KVSet(ctx context.Context, plugin, key string, value json.RawMessage, ...) error
- func (d *PluginDB) Migrate(ctx context.Context, plugin string, migs []Migration) (int, error)
- func (d *PluginDB) Provision(ctx context.Context, plugin string) error
- func (d *PluginDB) PublicObjects(ctx context.Context) ([]string, error)
- func (d *PluginDB) Query(ctx context.Context, plugin, sql string, args ...any) (*QueryResult, error)
- func (d *PluginDB) Reprovision(ctx context.Context, plugin string) error
- func (d *PluginDB) RoleError(plugin string) error
- func (d *PluginDB) WarnPublicObjects(ctx context.Context) []string
- type QueryResult
- type RouteRegistry
- type Runtime
- func (r *Runtime) AdminRoutes() *RouteRegistry
- func (r *Runtime) CallHost(ctx context.Context, plugin string, req sdk.CallRequest) []byte
- func (r *Runtime) Close(ctx context.Context)
- func (r *Runtime) Configure(c Config)
- func (r *Runtime) ExtSourceList() []builder.ExtSource
- func (r *Runtime) Grant(ctx context.Context, name string, perms []sdk.Permission) (Status, error)
- func (r *Runtime) Grants() Grants
- func (r *Runtime) Host() *wasmhost.Host
- func (r *Runtime) Load(ctx context.Context, m sdk.Manifest, wasm []byte) Status
- func (r *Runtime) LoadDir(ctx context.Context, dir string) []Status
- func (r *Runtime) LoadWith(ctx context.Context, m sdk.Manifest, wasm []byte, assets Assets) Status
- func (r *Runtime) PublicRoutes() *RouteRegistry
- func (r *Runtime) ScriptsDropped(plugin string) bool
- func (r *Runtime) SetGrantPolicy(f func(plugin string, perms []sdk.Permission) error)
- func (r *Runtime) SetScriptsDeny(f func(plugin string) bool)
- func (r *Runtime) Statuses() []Status
- func (r *Runtime) Unload(ctx context.Context, name string)
- type Scheduler
- type Services
- type SettingsStore
- type Status
Constants ¶
const ( ManifestFile = "plugin.json" WasmFile = "plugin.wasm" )
Plugin file names inside <plugin dir>/<name>/.
const CookiePrefix = "hmp_"
CookiePrefix starts the name of every cookie a plugin may read or set.
const DefaultFetchPerMinute = 600
DefaultFetchPerMinute is the outbound request cap per plugin.
Variables ¶
var ErrCommunityScripts = errors.New("site.scripts cannot be granted to a community package")
ErrCommunityScripts is returned for a site.scripts grant to a community package.
var ErrFetchBlocked = safefetch.ErrBlocked
ErrFetchBlocked is wrapped by every policy refusal (allowlist, address, scheme).
var ErrKVQuota = errors.New("plugin kv quota exceeded")
ErrKVQuota is returned when a plugin's kv store is full.
var ErrNotRequested = errors.New("permission not requested by the plugin")
ErrNotRequested is returned by Grant for a permission the manifest does not list.
var ErrPluginNotFound = errors.New("plugin not found")
ErrPluginNotFound is returned by Grant for a plugin that is not installed.
ErrRoleUnavailable means the plugin's role could not be created or used.
var ErrSQLBlocked = errors.New("sql blocked")
ErrSQLBlocked is wrapped by every guard refusal.
var ErrUnknownPermission = errors.New("unknown permission")
ErrUnknownPermission is returned for a permission name the SDK does not define.
Functions ¶
func AgentFirstCrawlObserver ¶
func AgentFirstCrawlObserver(e *hooks.Engine) func(agents.FirstCrawl)
AgentFirstCrawlObserver returns the callback for agents.NewFirstCrawlTracker that fires agent.first_crawl.
func AgentHitObserver ¶
AgentHitObserver returns a callback for agents.Writer.SetObserver that re-fires every stored hit as the agent.hit action. Nothing is built when no plugin subscribes. The IP never leaves the agents package: sdk.AgentHitEvent has no such field.
func BeforeSave ¶
func BeforeSave(engine *hooks.Engine) entity.BeforeSaveFunc
BeforeSave adapts the engine's strict entity.before_save filter chain to entity.BeforeSaveFunc.
func BindHostHooks ¶
BindHostHooks connects core extension points to the hooks engine. Call once at boot, after the engine exists.
func CompileSchema ¶
func CompileSchema(schema []byte) (*jsonschema.Schema, error)
CompileSchema compiles a plugin settings schema (JSON Schema 2020-12).
func DecodeArgs ¶
func DecodeArgs(raw json.RawMessage) ([]any, error)
DecodeArgs decodes a JSON array of query arguments.
func ExtSources ¶
func ExtSources(h *hooks.Engine) render.ExtSourceFunc
ExtSources adapts the builder.data hook chain to render.ExtSourceFunc: one Apply per batch. Each plugin sees only the requests that name its own declared sources, and only results for those requests are taken back. Nil engine gives nil (no external data).
func HeadFilter ¶
func HeadFilter(h *hooks.Engine) render.HeadFilter
HeadFilter adapts the render.head hook chain to render.HeadFilter. The render package sanitises and escapes whatever tags come back.
func HostFunctionsFor ¶
func HostFunctionsFor(perms []sdk.Permission) []string
HostFunctionsFor returns the names of the host functions a plugin holding perms may call (functions that need no permission included), sorted.
func MaskWriteOnly ¶
func MaskWriteOnly(schema []byte, v json.RawMessage) json.RawMessage
MaskWriteOnly blanks top-level `writeOnly` properties so secrets are never returned by the API.
func MergeDefaults ¶
func MergeDefaults(schema []byte, stored json.RawMessage) json.RawMessage
MergeDefaults fills top-level properties missing from stored with the schema's `default` values.
func PluginCookieName ¶
PluginCookieName is the browser cookie name of a plugin's cookie: plugins are confined to the namespace hmp_<plugin>_<name>, so they can neither read nor set the session cookie, the CSRF cookie or another plugin's cookies. The plugin itself only ever sees and uses <name>.
func PreserveWriteOnly ¶
func PreserveWriteOnly(schema []byte, old, in json.RawMessage) json.RawMessage
PreserveWriteOnly keeps the stored value of a top-level `writeOnly` property when the incoming object omits it or sends an empty string. An explicit null clears the stored value.
func SDKRefusal ¶
SDKRefusal is nil when this host serves the manifest's sdk range, as decided by Manifest.Compatible. Otherwise it names the plugin, the range, the host's SDK version and the SDK minors the host serves. Load and install both report it.
func SchemaName ¶
SchemaName is the Postgres schema of a plugin's data. Plugin names are [a-z][a-z0-9-]*, so "-" becomes "_" (no collision: "_" is not a legal name character).
func WriteOnlySet ¶
func WriteOnlySet(schema []byte, stored json.RawMessage) map[string]bool
WriteOnlySet names the top-level `writeOnly` properties that hold a non-empty stored value, so a form can say "stored" without showing it.
Types ¶
type API ¶
type API struct {
// contains filtered or unexported fields
}
API answers a plugin's hm_call requests. Every call is permission checked against the plugin's grants and bound to a deadline.
type Assets ¶
type Assets struct {
Migrations []Migration // migrations/*.sql
Partials map[string]string // partials/<name>.html
Sections map[string]string // sections/<type>.html
}
Assets are the files a plugin directory carries next to plugin.wasm.
type Config ¶
Config wires the runtime's capabilities. Every field is optional: without Services the host functions answer "unavailable", without Grants nothing is ever granted (a memory store), without Scheduler cron jobs do not run.
type EntityPaths ¶
type EntityPaths interface {
// Path is the canonical public path of ent ("/a/b").
Path(ctx context.Context, ent *entity.Entity) string
// Resolve returns the published entity served at path, or
// entity.ErrEntityNotFound.
Resolve(ctx context.Context, path string) (*entity.Entity, error)
// BaseURL is the configured public origin ("" when none is set).
BaseURL(ctx context.Context) string
}
EntityPaths gives the entities.* host functions the public site's view of an entity: where it lives and which entity lives at a path. The server implements it over the URL registry and the public resolver, so a plugin sees exactly the URLs the site serves.
type EntitySink ¶
type EntitySink struct {
// contains filtered or unexported fields
}
EntitySink turns entity lifecycle events into entity.* hook actions, which run asynchronously off the request path (so a slow plugin can never block a save). It implements entity.EventSink and worker.PublishEventSink.
func NewEntitySink ¶
func NewEntitySink(engine *hooks.Engine, log *slog.Logger) *EntitySink
NewEntitySink builds the sink on engine.
func (*EntitySink) Emit ¶
func (s *EntitySink) Emit(ctx context.Context, ev entity.Event)
Emit implements entity.EventSink.
func (*EntitySink) EntityPublished ¶
func (s *EntitySink) EntityPublished(ctx context.Context, id uuid.UUID, schemaSlug, slug, name string, version int)
EntityPublished implements worker.PublishEventSink for scheduled publishes.
func (*EntitySink) SetLocaleLookup ¶
SetLocaleLookup wires the entity locale reader. The worker interface (internal/worker, not owned by this change) carries no locale, so a scheduled publish looks it up here; an unset or failing lookup leaves the locale of entity.published empty.
type FetchRequest ¶
type FetchRequest struct {
Method string `json:"method,omitempty"`
URL string `json:"url"`
Headers map[string]string `json:"headers,omitempty"`
Body string `json:"body,omitempty"`
BodyB64 string `json:"body_b64,omitempty"`
TimeoutMS int `json:"timeout_ms,omitempty"`
}
FetchRequest is the input of http.fetch.
type FetchResponse ¶
type FetchResponse struct {
Status int `json:"status"`
Headers map[string]string `json:"headers"`
Body string `json:"body,omitempty"`
BodyB64 string `json:"body_b64,omitempty"`
Truncated bool `json:"truncated,omitempty"`
}
FetchResponse is the result of http.fetch.
type Fetcher ¶
type Fetcher struct {
// contains filtered or unexported fields
}
Fetcher performs guarded outbound requests.
func NewFetcher ¶
func NewFetcher() *Fetcher
NewFetcher returns a Fetcher using the system resolver.
func NewFetcherForTests ¶
func NewFetcherForTests() *Fetcher
NewFetcherForTests returns a Fetcher that also reaches loopback and private addresses, so a test can stand an httptest server in for a remote service. The plugin's allowed_hosts still apply. Never use it outside tests.
func (*Fetcher) Do ¶
func (f *Fetcher) Do(ctx context.Context, plugin string, allowed []string, req FetchRequest) (*FetchResponse, error)
Do runs req for a plugin whose manifest allows the given hosts.
type FieldError ¶
FieldError is one schema violation, addressed by JSON pointer.
func Validate ¶
func Validate(sch *jsonschema.Schema, v json.RawMessage) []FieldError
Validate checks v against sch and returns the violations (nil when valid).
type GrantStore ¶
type GrantStore struct {
// contains filtered or unexported fields
}
GrantStore keeps grants in hatamoto_plugin_grants and mirrors them in memory, so the permission check on every host call costs a map lookup. With a nil pool it is memory only (tests).
func NewGrantStore ¶
func NewGrantStore(pool *pgxpool.Pool) *GrantStore
NewGrantStore returns a store over pool (nil: memory only). Call Load once at boot.
func (*GrantStore) Granted ¶
func (g *GrantStore) Granted(plugin string) []sdk.Permission
Granted returns the plugin's granted permissions, sorted.
func (*GrantStore) Has ¶
func (g *GrantStore) Has(plugin string, perm sdk.Permission) bool
Has reports whether plugin holds perm.
func (*GrantStore) Load ¶
func (g *GrantStore) Load(ctx context.Context) error
Load reads every stored grant.
func (*GrantStore) Set ¶
func (g *GrantStore) Set(ctx context.Context, plugin string, perms []sdk.Permission) error
Set stores the plugin's granted set (deduplicated, sorted).
type Grants ¶
type Grants interface {
Has(plugin string, perm sdk.Permission) bool
Granted(plugin string) []sdk.Permission
// Set replaces the plugin's granted set.
Set(ctx context.Context, plugin string, perms []sdk.Permission) error
}
Grants is the owner's decision on which of a plugin's requested capabilities it may use. Nothing is granted by default.
type Mailer ¶
type Mailer interface {
IsConfigured() bool
Send(ctx context.Context, to []string, subject, htmlBody, textBody string) error
}
Mailer is the slice of the email service mail.send uses.
type Migration ¶
Migration is one plugin migration file: <version>_<name>.sql in the goose format. Only the "-- +goose Up" section runs; Down sections are ignored.
type PageFilter ¶
type PageFilter struct {
// contains filtered or unexported fields
}
PageFilter applies the render.page chain. Cookies are the request's plugin cookies by browser name (hmp_<plugin>_<name>; core cookies are never put in).
func NewPageFilter ¶
func NewPageFilter(engine *hooks.Engine) *PageFilter
NewPageFilter returns the adapter over engine; nil engine gives nil.
func (*PageFilter) Active ¶
func (p *PageFilter) Active() bool
Active reports whether any plugin handles render.page; pages are then request-dependent and must not be shared-cached.
func (*PageFilter) Apply ¶
func (p *PageFilter) Apply(ctx context.Context, in sdk.PageContext) sdk.PageContext
Apply runs the chain over in and returns the result (in itself when nothing handled it or everything failed).
type PluginDB ¶
type PluginDB struct {
// AllowPublicObjects lets db.* be granted although objects the application
// role does not own are open to PUBLIC (HATAMOTO_PLUGIN_DB_ALLOW_PUBLIC_OBJECTS=1).
AllowPublicObjects bool
// contains filtered or unexported fields
}
PluginDB provisions and queries the per-plugin databases (see plugindb_provision.go for the model). pool is the core pool: it runs only core-authored SQL (provisioning, credentials, kv). Plugin SQL runs only on the plugin's own pool.
func NewPluginDB ¶
NewPluginDB returns a PluginDB over the core pool.
func (*PluginDB) Drop ¶
Drop removes a plugin's data, schema, role and credentials (tests; uninstall comes with v1-18).
func (*PluginDB) Exec ¶
Exec runs one guarded statement as the plugin and returns the rows it touched.
func (*PluginDB) KVSet ¶
func (d *PluginDB) KVSet(ctx context.Context, plugin, key string, value json.RawMessage, ttl time.Duration) error
KVSet stores value under key; ttl > 0 expires it. A plugin holds at most maxKVKeys live keys and maxKVTotalBytes of values.
func (*PluginDB) Migrate ¶
Migrate applies the pending migrations through goose on the plugin's own pool, so they run as the plugin role with its session user, with a version table inside the plugin's own schema (plugin_<name>.goose_db_version). The SQL guard vets every statement first; the database is the boundary.
func (*PluginDB) Provision ¶
Provision creates the plugin's role and schema (idempotent) and opens its pool lazily. An error means provisioning failed outright; a role that cannot be created is not an error here (see RoleError).
func (*PluginDB) PublicObjects ¶
PublicObjects lists what PUBLIC can use that the application role cannot close (see above). Empty is the healthy answer.
func (*PluginDB) Query ¶
func (d *PluginDB) Query(ctx context.Context, plugin, sql string, args ...any) (*QueryResult, error)
Query runs one guarded statement as the plugin and returns its rows.
func (*PluginDB) Reprovision ¶
Reprovision rotates the plugin's password, replaces its pool and provisions it again, so a role that was missing (no CREATEROLE yet) is retried when the plugin loads or its grants change.
type QueryResult ¶
type QueryResult struct {
Columns []string `json:"columns"`
Rows [][]any `json:"rows"`
Truncated bool `json:"truncated,omitempty"`
}
QueryResult is the result of db.query.
type RouteRegistry ¶
type RouteRegistry struct {
// contains filtered or unexported fields
}
RouteRegistry holds the per-plugin routers of one mount (admin or public).
func NewRouteRegistry ¶
func NewRouteRegistry(call Caller, public bool, log *slog.Logger) *RouteRegistry
NewRouteRegistry returns a registry serving either the admin or the public routes of the plugins.
func (*RouteRegistry) Plugins ¶
func (g *RouteRegistry) Plugins() []string
Plugins lists the plugins that have routes here, sorted.
func (*RouteRegistry) Serve ¶
func (g *RouteRegistry) Serve(w http.ResponseWriter, r *http.Request, plugin, rest string, user *sdk.HTTPUser)
Serve handles the request for plugin; rest is the path below the mount (without the leading slash). user is nil on the public mount.
type Runtime ¶
type Runtime struct {
// contains filtered or unexported fields
}
Runtime loads plugins into a wasmhost.Host and registers their hook handlers on the engine.
func NewRuntime ¶
NewRuntime returns a Runtime. cacheDir is the compiled-code cache ("" = memory only).
func (*Runtime) AdminRoutes ¶
func (r *Runtime) AdminRoutes() *RouteRegistry
AdminRoutes and PublicRoutes are the two route registries the server mounts.
func (*Runtime) CallHost ¶
CallHost runs one host function on behalf of plugin, with the plugin's permission check, and returns the sdk.CallResponse JSON. It is the path the admin's plugin-UI bridge uses.
func (*Runtime) ExtSourceList ¶
ExtSourceList returns the builder data sources of every loaded plugin that holds the builder.data grant, for the editor vocabulary.
func (*Runtime) Grant ¶
Grant sets the capabilities the owner allows plugin: perms must all be listed in its manifest, and replace any earlier grant (grant fewer to revoke). Data the new grants need is prepared at once. It returns the plugin's resulting status.
func (*Runtime) LoadDir ¶
LoadDir loads every <dir>/<name>/ that holds a plugin.json, in name order. A plugin that fails to load is recorded and logged; the others and the server are unaffected. A missing dir means no plugins.
func (*Runtime) LoadWith ¶
LoadWith validates, compiles and registers one plugin with its assets (migrations, partials, sections). Never returns a Go error: the result, including a failure, is the Status.
func (*Runtime) PublicRoutes ¶
func (r *Runtime) PublicRoutes() *RouteRegistry
func (*Runtime) ScriptsDropped ¶
ScriptsDropped reports whether the plugin has asked for a script tag that was dropped because it lacks the site.scripts grant.
func (*Runtime) SetGrantPolicy ¶
func (r *Runtime) SetGrantPolicy(f func(plugin string, perms []sdk.Permission) error)
SetGrantPolicy installs a check Grant runs before it stores a grant; the server wires the package policy here (pluginhost cannot import packages).
func (*Runtime) SetScriptsDeny ¶
SetScriptsDeny installs a policy that refuses a plugin's scripts whatever is granted (a stored grant outlives a policy change); the server wires it.
type Scheduler ¶
type Scheduler interface {
Schedule(id string, every time.Duration, fn func(context.Context) error) error
Unschedule(prefix string)
}
Scheduler runs a function every interval (worker.Scheduler). Scheduling an id again replaces the job; Unschedule stops every job whose id starts with prefix.
type Services ¶
type Services struct {
Entities *entity.Service
Paths EntityPaths // optional: without it entities carry no path/url and cannot be read by path
Terms *taxonomy.Service
Menus *menu.Service
Media *media.Service
Redirects *redirect.Service
Forms *form.Service
Mail Mailer
Settings *SettingsStore
DB *PluginDB
Fetcher *Fetcher
// FetchPerMinute caps one plugin's outbound http.fetch requests per
// minute; 0 means DefaultFetchPerMinute.
FetchPerMinute int
}
Services are the core services host functions call. A nil service makes its functions fail with "unavailable".
type SettingsStore ¶
type SettingsStore struct {
// contains filtered or unexported fields
}
SettingsStore persists plugin settings in hatamoto_plugin_settings.
func NewSettingsStore ¶
func NewSettingsStore(pool *pgxpool.Pool) *SettingsStore
NewSettingsStore returns a store over pool.
func (*SettingsStore) Get ¶
func (s *SettingsStore) Get(ctx context.Context, plugin string) (json.RawMessage, error)
Get returns the stored settings object, or {} when none were saved.
func (*SettingsStore) Put ¶
func (s *SettingsStore) Put(ctx context.Context, plugin string, v json.RawMessage) error
Put upserts the full settings object.
type Status ¶
type Status struct {
Name string
Manifest sdk.Manifest
Loaded bool
Error string // why it did not load
Hooks []string // hook names the module exports a handler for
// DataError says why the plugin's own data (schema, migrations) is not
// ready; the plugin itself is loaded.
DataError string
}
Status is the outcome of loading one plugin; it is what GET /api/v1/plugins reports.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package plugintest builds the test plugins under internal/pluginhost/testdata for tests in any package.
|
Package plugintest builds the test plugins under internal/pluginhost/testdata for tests in any package. |