Building a plugin
Foundry plugins are regular Go packages that register themselves through the plugin registry and optionally implement one or more hook interfaces.
Plugin shape
plugins/myplugin/
plugin.go
plugin.yaml
assets/
css/
js/
Required metadata
Every plugin should ship a plugin.yaml file. The compatibility fields are
the ones that matter most operationally:
name: myplugin
title: My Plugin
version: 0.1.0
description: Adds project-specific behavior
author: Your Team
license: MIT
foundry_api: v1
min_foundry_version: 0.1.0
compatibility_version: v1
dependencies:
- name: github.com/acme/search-core
version: ^1.2.0
config_schema:
- name: mode
type: select
enum: [compact, full]
default: compact
admin:
pages:
- key: search-console
title: Search Console
route: /plugins/search
module: admin/search-console.js
styles:
- admin/search-console.css
settings_sections:
- key: search
title: Search
description: Search tuning
permissions:
content:
documents:
read: true
render:
context:
read: true
write: true
html_slots:
inject: true
capabilities:
requires_admin_approval: false
Available metadata fields
nametitleversiondescriptionauthorhomepagelicensereporequiresdependenciesfoundry_apimin_foundry_versioncompatibility_versionconfig_schemapermissionsadmin.pagesadmin.widgetsadmin.slotsadmin.settings_sectionsscreenshots
Plugin permissions
Foundry now supports a structured permissions block in
plugin.yaml. This does not sandbox in-process Go plugins, but it does make
plugin intent explicit in validation, CLI output, and the admin UI.
permissions:
filesystem:
read:
content: true
data: false
write:
public: true
network:
outbound:
https: true
domains:
- api.example.com
methods:
- GET
- POST
inbound:
register_routes: false
admin_routes: false
public_routes: false
render:
context:
read: true
write: true
html_slots:
inject: true
capabilities:
dangerous: false
requires_admin_approval: false
-
Use the most specific declarations you can. For example, prefer
content.documents.readover broad filesystem access when a plugin only reads content through normal hooks. -
Dangerous plugins, secret access, or local process execution must also declare
permissions.capabilities.requires_admin_approval: true. -
Report declared permissions with
foundry plugin security <name>. -
Run
foundry plugin validate --securityorfoundry plugin validate <name> --security --strict-securityto make security checks explicit in CI and local review. - Foundry now also scans plugin source for risky imports, hooks, and calls, then compares that detected behavior against the declared permissions.
-
CLI and admin install, enable, and update flows require explicit approval for risky
or mismatched plugins. Use
--approve-riskin the CLI when you intend to trust that plugin.
Permission reference
The available permission surface in plugin.yaml is listed below. Declare
only what your plugin actually needs.
-
permissions.filesystem.readContent, data, public, themes, plugins, config, and custom paths. Use this for raw file reads when a normal content or config hook is not enough. -
permissions.filesystem.writeContent, data, public, cache, backups, and custom paths. Use this for writing files into Foundry-managed directories. -
permissions.filesystem.deleteContent, data, public, cache, backups, and custom paths. Use this when the plugin deletes files or directories. -
permissions.network.outboundhttp,https,websocket,grpc,custom_schemes,domains, andmethods. Use this for any outbound request behavior. -
permissions.network.inboundregister_routes,admin_routes,public_routes, andbind_external_services. Use this when a plugin exposes handlers or binds its own listeners. -
permissions.process.execallowedplus optionalcommands. Use for spawning local commands. -
permissions.process.shellallowed. Use when the plugin shells out through a shell interpreter. -
permissions.process.spawn_backgroundallowed. Use when the plugin starts long-running background processes. -
permissions.environment.readallowedplus optionalvariables. Use when environment variables are read. -
permissions.config.readsite,plugin_config,theme_manifest, andraw_files. Use when the plugin reads configuration beyond normal typed config passed through hooks. -
permissions.config.writesite,plugin_config, andtheme_manifest. Use when the plugin mutates configuration on disk. -
permissions.content.documentsread,write,delete,workflow, andversions. Use for document lifecycle access. -
permissions.content.mediaread,write,delete,metadata, andversions. Use for media-library access. -
permissions.content.taxonomiesreadandwrite. Use when the plugin inspects or mutates taxonomy structures. -
permissions.content.shared_fieldsreadandwrite. Use for global shared-field access fromcontent/custom-fields.yaml. -
permissions.render.contextreadandwrite. Use when the plugin reads or enriches template view data. -
permissions.render.html_slots.inject. Use when the plugin injects raw HTML into named theme slots. -
permissions.render.assetsinject_css,inject_js, andinject_remote_assets. Use when the plugin adds frontend assets to a page. -
permissions.render.after_render.mutate_html. Use when the plugin rewrites final rendered HTML. -
permissions.graphread,mutate,routes.inspect,routes.mutate,taxonomies.inspect, andtaxonomies.mutate. Use when the plugin works against the built site graph. -
permissions.admin.extensionspages,widgets,settings_sections, andslots. These must be declared when the plugin contributes admin UI surfaces. -
permissions.admin.usersread,write,revoke_sessions, andreset_passwords. Use when the plugin touches admin account management. -
permissions.admin.audit.read. Use when the plugin reads audit history. -
permissions.admin.diagnosticsreadandvalidate. Use when the plugin hooks into admin diagnostics or validation surfaces. -
permissions.admin.operationsrebuild,clear_cache,backups, andupdates. Use when the plugin interacts with operational controls. -
permissions.runtime.serveron_startedandregister_routes. Use when the plugin reacts to runtime startup or route registration. -
permissions.runtime.metrics.readandpermissions.runtime.logs.read. Use when the plugin inspects runtime telemetry or logs. -
permissions.secrets.accessadmin_tokens,session_store,password_hashes,totp_secrets,env_secrets,deploy_keys, andupdate_credentials. These are high-risk declarations and should be used sparingly. -
permissions.capabilitiesdangerousandrequires_admin_approval. Use these when the plugin has elevated risk and should never be enabled implicitly.
In-process vs RPC plugins
Foundry supports two plugin runtime shapes, and they behave differently when installed or enabled.
-
In-process Go plugins are compiled into the Foundry binary. They
register through Go
init()functions, so enabling one from the Admin UI only changes configuration. It cannot import new Go code into an already-built binary. After enabling a newly installed in-process plugin, runfoundry plugin syncorgo run ./cmd/plugin-sync, rebuild, and restart. -
Out-of-process RPC plugins declare
runtime.mode: rpcinplugin.yaml. They do not need generated Go imports because Foundry launches the configured command and communicates through the RPC protocol.
Built-in in-process plugins that ship with Foundry may already be compiled into the binary. Locally installed third-party in-process plugins still require generated imports and a rebuild.
Plugin runtime contract
Foundry now supports a first-class runtime block in
plugin.yaml for the out-of-process plugin host. The first supported RPC
hook family is context, which lets a plugin enrich render-time view data
without sharing the Foundry process.
runtime:
mode: rpc
protocol_version: v1alpha1
command:
- ./bin/my-plugin
- serve
socket: unix:///tmp/foundry-my-plugin.sock
sandbox:
profile: strict
allow_network: false
allow_filesystem_write: false
allow_process_exec: false
Foundry's current RPC host expects the declared command to speak the
sdk/pluginrpc protocol over stdin/stdout. The host sends a handshake, then
context requests with sanitized page/view data. The plugin responds with a
data map to merge back into the page context.
The current RPC host enforces a narrow boundary:
- sanitized environment only
- no host-granted filesystem write channel
- no host-granted network channel
- no host-granted local process execution channel
- unsupported sandbox expansions are rejected at enable time
A working example ships with Foundry at
plugins/rpc-context-demo/.
Plugin-defined admin pages
Plugins can now declare admin pages in plugin.yaml. Foundry exposes those
pages through the admin extension registry, gives them stable routes inside the admin
shell, and can auto-load a plugin-provided ES module bundle for the page.
admin:
pages:
- key: search-console
title: Search Console
route: /plugins/search
nav_group: admin
capability: plugins.manage
description: Inspect search status and rebuild controls
module: admin/search-console.js
styles:
- admin/search-console.css
The module and styles paths are relative to the plugin
directory. Foundry serves them under
<admin.path>/extensions/<plugin>/... after normalizing the
paths and keeping them inside the plugin root.
Use nav_group to place a plugin-defined page in the default admin theme's
grouped sidebar. Supported values are dashboard, content,
manage, and admin. Omit it to fall back to
admin.
The default admin shell's Extensions page itself lives at
<admin.path>/a-extensions. That route is separate from the
plugin bundle asset namespace so the shell page and plugin assets do not collide.
When that page is active, the default admin shell dispatches
foundry:admin-extension-page, exposes window.FoundryAdmin, and
will automatically import the plugin module if one is declared. A page bundle can export
either mountAdminExtensionPage(ctx) or a default function.
Widgets follow the same model. The default admin theme currently exposes these stable widget slots:
overview.afterdocuments.sidebarmedia.sidebarplugins.sidebar
Widget bundles can export mountAdminExtensionWidget(ctx) or a default
function. The shell dispatches foundry:admin-extension-widget for each
visible widget mount.
Minimal implementation
package myplugin
import "github.com/sphireinc/foundry/internal/plugins"
type Plugin struct{}
func (p *Plugin) Name() string { return "myplugin" }
func init() {
plugins.Register("myplugin", func() plugins.Plugin {
return &Plugin{}
})
}
Available plugin hooks
The current plugin surface is interface-based. A plugin implements only the hooks it needs.
OnConfigLoaded(*config.Config) errorOnContentDiscovered(path string) errorOnFrontmatterParsed(*content.Document) errorOnMarkdownRendered(*content.Document) errorOnDocumentParsed(*content.Document) errorOnDataLoaded(map[string]any) errorOnGraphBuilding(*content.SiteGraph) errorOnGraphBuilt(*content.SiteGraph) errorOnTaxonomyBuilt(*content.SiteGraph) errorOnRoutesAssigned(*content.SiteGraph) errorOnContext(*renderer.ViewData) errorOnAssets(*renderer.ViewData, *renderer.AssetSet) errorOnHTMLSlots(*renderer.ViewData, *renderer.Slots) errorOnBeforeRender(*renderer.ViewData) errorOnAfterRender(url string, html []byte) ([]byte, error)OnAssetsBuilding(*config.Config) errorOnBuildStarted() errorOnBuildCompleted(*content.SiteGraph) errorOnServerStarted(addr string) errorRegisterRoutes(mux *http.ServeMux)Commands() []plugins.Commandfor plugin-provided CLI commands
Injectable HTML slots
Slots are defined by the active theme. For the launch version of Foundry, the following
slot names are the required baseline supported by theme validation and are therefore the
safe targets for OnHTMLSlots:
head.endbody.startbody.endpage.before_mainpage.after_mainpage.before_contentpage.after_contentpost.before_headerpost.after_headerpost.before_contentpost.after_contentpost.sidebar.toppost.sidebar.overviewpost.sidebar.bottom
Plugin command surface
If a plugin implements CLI commands, each command currently exposes these fields:
NameSummaryDescriptionRun func(ctx plugins.CommandContext) error
Typical workflow
- Create the plugin directory under
plugins/. - Add
plugin.goandplugin.yaml. - Register the plugin in
init(). - Enable it in project config.
- Run
foundry plugin syncorgo run ./cmd/plugin-sync. - Run
go test ./...andfoundry plugin validate.
Asset and HTML integration
Plugins can contribute assets through the renderer asset hooks and add markup through
HTML slots. If a plugin ships public assets, place them under assets/;
Foundry will copy enabled plugin assets into public/plugins/<name>/.
Dependency and install notes
-
Use the
requiresfield inplugin.yamlwhen your plugin depends on another plugin by repo identity. -
Use
dependencieswhen you want richer dependency metadata such as version ranges or optional dependencies. -
Plugin installation is intentionally restricted to GitHub over
httpsorgit@github.com. -
Plugin updates now preserve rollback snapshots under
plugins/.rollback/<name>/. - Admin plugin records surface health diagnostics, compatibility fields, dependency metadata, and rollback availability.
- Installing a plugin still means trusting third-party code. Treat it as a supply-chain boundary.
Reference code
The built-in plugins are the best reference implementations:
- Reading Time for document enrichment and slot rendering.
- TOC for content parsing, asset injection, slot rendering, and plugin CLI commands.
- Related Posts for post-render content injection into post slots.
Field safety
Foundry now treats advanced custom fields as a theme-owned contract. That means plugin
code must not write derived render metadata into doc.Fields.
-
Use
doc.Fieldsonly for persisted, schema-backed content that belongs to the document itself. -
Do not attach derived values like
toc,has_toc,reading_time, or similar render metadata to document fields. -
Prefer render-time data surfaces such as
ctx.Data, or recompute the value during rendering when practical. -
The built-in
tocplugin is the reference example for runtime-only derived metadata.
