Foundry logo Foundry Plugin authoring

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

  • name
  • title
  • version
  • description
  • author
  • homepage
  • license
  • repo
  • requires
  • dependencies
  • foundry_api
  • min_foundry_version
  • compatibility_version
  • config_schema
  • permissions
  • admin.pages
  • admin.widgets
  • admin.slots
  • admin.settings_sections
  • screenshots

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.read over 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 --security or foundry plugin validate <name> --security --strict-security to 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-risk in 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.read Content, 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.write Content, data, public, cache, backups, and custom paths. Use this for writing files into Foundry-managed directories.
  • permissions.filesystem.delete Content, data, public, cache, backups, and custom paths. Use this when the plugin deletes files or directories.
  • permissions.network.outbound http, https, websocket, grpc, custom_schemes, domains, and methods. Use this for any outbound request behavior.
  • permissions.network.inbound register_routes, admin_routes, public_routes, and bind_external_services. Use this when a plugin exposes handlers or binds its own listeners.
  • permissions.process.exec allowed plus optional commands. Use for spawning local commands.
  • permissions.process.shell allowed. Use when the plugin shells out through a shell interpreter.
  • permissions.process.spawn_background allowed. Use when the plugin starts long-running background processes.
  • permissions.environment.read allowed plus optional variables. Use when environment variables are read.
  • permissions.config.read site, plugin_config, theme_manifest, and raw_files. Use when the plugin reads configuration beyond normal typed config passed through hooks.
  • permissions.config.write site, plugin_config, and theme_manifest. Use when the plugin mutates configuration on disk.
  • permissions.content.documents read, write, delete, workflow, and versions. Use for document lifecycle access.
  • permissions.content.media read, write, delete, metadata, and versions. Use for media-library access.
  • permissions.content.taxonomies read and write. Use when the plugin inspects or mutates taxonomy structures.
  • permissions.content.shared_fields read and write. Use for global shared-field access from content/custom-fields.yaml.
  • permissions.render.context read and write. 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.assets inject_css, inject_js, and inject_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.graph read, mutate, routes.inspect, routes.mutate, taxonomies.inspect, and taxonomies.mutate. Use when the plugin works against the built site graph.
  • permissions.admin.extensions pages, widgets, settings_sections, and slots. These must be declared when the plugin contributes admin UI surfaces.
  • permissions.admin.users read, write, revoke_sessions, and reset_passwords. Use when the plugin touches admin account management.
  • permissions.admin.audit.read. Use when the plugin reads audit history.
  • permissions.admin.diagnostics read and validate. Use when the plugin hooks into admin diagnostics or validation surfaces.
  • permissions.admin.operations rebuild, clear_cache, backups, and updates. Use when the plugin interacts with operational controls.
  • permissions.runtime.server on_started and register_routes. Use when the plugin reacts to runtime startup or route registration.
  • permissions.runtime.metrics.read and permissions.runtime.logs.read. Use when the plugin inspects runtime telemetry or logs.
  • permissions.secrets.access admin_tokens, session_store, password_hashes, totp_secrets, env_secrets, deploy_keys, and update_credentials. These are high-risk declarations and should be used sparingly.
  • permissions.capabilities dangerous and requires_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, run foundry plugin sync or go run ./cmd/plugin-sync, rebuild, and restart.
  • Out-of-process RPC plugins declare runtime.mode: rpc in plugin.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.after
  • documents.sidebar
  • media.sidebar
  • plugins.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) error
  • OnContentDiscovered(path string) error
  • OnFrontmatterParsed(*content.Document) error
  • OnMarkdownRendered(*content.Document) error
  • OnDocumentParsed(*content.Document) error
  • OnDataLoaded(map[string]any) error
  • OnGraphBuilding(*content.SiteGraph) error
  • OnGraphBuilt(*content.SiteGraph) error
  • OnTaxonomyBuilt(*content.SiteGraph) error
  • OnRoutesAssigned(*content.SiteGraph) error
  • OnContext(*renderer.ViewData) error
  • OnAssets(*renderer.ViewData, *renderer.AssetSet) error
  • OnHTMLSlots(*renderer.ViewData, *renderer.Slots) error
  • OnBeforeRender(*renderer.ViewData) error
  • OnAfterRender(url string, html []byte) ([]byte, error)
  • OnAssetsBuilding(*config.Config) error
  • OnBuildStarted() error
  • OnBuildCompleted(*content.SiteGraph) error
  • OnServerStarted(addr string) error
  • RegisterRoutes(mux *http.ServeMux)
  • Commands() []plugins.Command for 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.end
  • body.start
  • body.end
  • page.before_main
  • page.after_main
  • page.before_content
  • page.after_content
  • post.before_header
  • post.after_header
  • post.before_content
  • post.after_content
  • post.sidebar.top
  • post.sidebar.overview
  • post.sidebar.bottom

Plugin command surface

If a plugin implements CLI commands, each command currently exposes these fields:

  • Name
  • Summary
  • Description
  • Run func(ctx plugins.CommandContext) error

Typical workflow

  1. Create the plugin directory under plugins/.
  2. Add plugin.go and plugin.yaml.
  3. Register the plugin in init().
  4. Enable it in project config.
  5. Run foundry plugin sync or go run ./cmd/plugin-sync.
  6. Run go test ./... and foundry 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 requires field in plugin.yaml when your plugin depends on another plugin by repo identity.
  • Use dependencies when you want richer dependency metadata such as version ranges or optional dependencies.
  • Plugin installation is intentionally restricted to GitHub over https or git@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.Fields only 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 toc plugin is the reference example for runtime-only derived metadata.