Skip to content

Manifest Reference

Every FreeSDN SDK plugin ships a plugin.yaml at the root of its ZIP archive. The runtime parses and validates this file through PluginManifest (backend/app/plugins/schema.py). The published dev SDK mirrors this model in freesdn-sdk/sdk/src/freesdn_sdk/manifest.py, but the two diverge in important ways - see the SDK vs runtime differences section before you ship.


These are the direct fields of the plugin.yaml document.

FieldTypeDefaultRequiredValidation
idstring-Yes^[a-z0-9][a-z0-9\-]{0,98}[a-z0-9]$ - 2-100 chars, lowercase alphanumeric and hyphens only. Must not be a reserved ID.
namestring-Yes1-200 chars.
versionstring-YesStrict semver MAJOR.MINOR.PATCH - all numeric, exactly 3 parts. 1.0.0 is valid; 1.0 and 1.0.0-beta are not.
descriptionstring""NoMax 2000 chars.
authorstring""NoMax 200 chars.
licensestring"MIT"NoFree string; not validated.
homepagestring or nullnullNoMax 500 chars. Must start with http:// or https:// if provided.
min_core_versionstring"1.0.0"NoFree string. Recorded, not enforced.
entry_pointstring"plugin.py"NoMust match ^[a-zA-Z0-9_/\-]+\.py$. No .., no leading / or \. Resolved relative to the plugin directory at load time.
class_namestring-YesMust be a valid Python identifier and must not start with _. The loader imports entry_point and looks for this class.
permissionslist of PluginPermission[]NoSDK capability declarations. The confused-deputy guard enforces these at request time.
api_prefixstring or nullnullNoMust match ^/[a-z0-9][a-z0-9/_-]*$. No ... Defaults to /{id} at mount time if omitted. Routes are mounted at /api/v1{api_prefix}.
dependencieslist of PluginDependency[]NoInter-module dependencies. Declared only, not enforced at install.
nav_itemslist of PluginNavItem[]NoSidebar navigation entries the UI renders when the plugin is active.
public_routeslist of PluginPublicRoute[]NoUnauthenticated routes protected by HMAC. Runtime-only - the published dev SDK manifest does not include this field.
event_subscriptionslist of string[]NoEvent-bus patterns the plugin may subscribe to. EventSDK.subscribe() rejects any pattern not declared here. Bare * and # wildcards are rejected.
settings_schemaobject or nullnullNoJSON Schema describing the settings the plugin stores via PluginSettingsSDK. FreeSDN records it but does not validate settings values against it at runtime.
python_dependencieslist of string[]NoMax 50 entries. Must use exact name==version pinning. Loose specifiers (>=, ~=, etc.) are rejected. See dependency pinning.

The id you choose becomes a permanent namespace prefix for everything the plugin owns:

  • REST routes mount at /api/v1/{id} (unless you override api_prefix)
  • All events the plugin emits are namespaced plugin.{id}.{event_type}
  • Automation triggers and actions register as plugin.{id}.{type}
  • AI tools are prefixed plugin_{id}_{name}
  • Device external IDs you register must start with plugin.{id}:
  • Per-org settings are scoped by (plugin_id, organization_id, key)

Choose a stable, descriptive ID. You cannot rename a plugin once installed without uninstalling and reinstalling.

The following IDs are blocked and will cause install to fail:

admin, auth, api, core, system, devices, alerts, users, settings, backup, vpn,
automation, webhooks, ai, collector, integrations, plugins, marketplace, health,
status, metrics, internal

Declares an SDK capability the plugin needs. The runtime enforces these through _require_permission() - if a capability is not declared here, the SDK raises PermissionError when your code tries to use it.

FieldTypeDefaultNotes
codestring(required)The SDK capability string. See the permission codes table below.
namestring(required)Human-readable name shown in the UI.
descriptionstring""Short explanation of why the plugin needs this capability.
CodeCore permission mappedWhat it grants
devices.readdevice:readRead device inventory via ctx.devices.list() and ctx.devices.get()
devices.writedevice:writeRegister devices via ctx.devices.register_device()
alerts.readalert:readList alerts via ctx.alerts.list()
alerts.writealert:writeCreate and resolve alerts via ctx.alerts.create() / ctx.alerts.resolve()

Declares a dependency on a FreeSDN native module.

FieldTypeDefaultNotes
module_idstring(required)ID of the required module (e.g. network, cameras).
min_versionstring"1.0.0"Minimum acceptable module version. Not enforced at install.
optionalbooleanfalseWhether the plugin degrades gracefully without this module.

Adds an entry to the FreeSDN sidebar when the plugin is active for the current organization.

FieldTypeDefaultNotes
pathstring(required)Route path relative to the plugin’s frontend mount.
labelstring(required)Display label in the sidebar.
iconstring"Package"Lucide icon name.
orderinteger50Sort order among nav items. Lower numbers appear first.

Declares an unauthenticated webhook-style route protected by HMAC-SHA256. The route guard validates the HMAC before your handler runs, and replay protection via Valkey ensures each nonce is accepted only once within a 300-second window.

FieldTypeDefaultNotes
pathstring(required)Must match ^/[a-z0-9][a-z0-9/_-]*$. No ... Trailing slash is stripped.
methodslist of string["POST"]Uppercased. Allowed values: POST, PUT, PATCH, DELETE. GET is intentionally not permitted for public routes. Must be non-empty.

Every request to a public route must include all four of these headers:

HeaderContents
X-FreeSDN-Plugin-OrgOrganization ID (UUID) the request targets
X-FreeSDN-Plugin-TimestampUnix timestamp (seconds). Must be within 300 seconds of server time.
X-FreeSDN-Plugin-Nonce16-128 char unique value per request
X-FreeSDN-Plugin-Signaturesha256= + HMAC-SHA256(secret, canonical_message)

The canonical message is the newline-joined concatenation of: timestamp, nonce, METHOD, path, query_string, org_id, sha256(body).

Obtain the HMAC secret by calling POST /api/v1/plugins/{plugin_id}/public-auth/rotate-secret (org_admin required). The plaintext secret is returned once and never again.


The python_dependencies list feeds the hash-pinned install pipeline. The runtime applies a strict validator that the dev SDK does not:

RuleRuntimeDev SDK (freesdn-sdk validate)
Specifier formatMust be name==version exactlyAccepts >=, ~=, !=, etc.
Extras syntax (pkg[extra])RejectedRejected
Specs starting with -RejectedRejected
Specs containing ; (environment markers)RejectedRejected

If python_dependencies is non-empty, you must also ship a requirements.txt in the ZIP root. The runtime verifies:

  1. The file contains --hash=sha256: annotations for every package (generate with pip-compile --generate-hashes).
  2. Every bare package name in python_dependencies appears in the requirements file.
  3. The runtime opt-in flag PLUGIN_ALLOW_RUNTIME_PYTHON_DEPS=true is set (default is false).

Install creates a per-plugin .venv and runs pip install --require-hashes --no-deps --no-cache-dir --only-binary :all: - no sdist code execution, no transitive upgrades, no cache poisoning.


ResourceLimit
ZIP archive (compressed)50 MB
ZIP archive (uncompressed)200 MB
python_dependencies entries50
Automation triggers50 per plugin
Automation actions50 per plugin
AI tools20 per plugin
Registered devices1,000 per plugin
HTTP response (via PluginHTTPClient)10 MB
HTTP timeout60 seconds
AI tool result payload256 KB
Settings blob per PUT /settings32 KiB

This is the notify-hub example from the published SDK, adapted with correct == pinning:

id: notify-hub
name: Notify Hub
version: 1.0.0
description: >
Forwards FreeSDN alerts and SLA breaches to external notification channels
(Slack, PagerDuty, generic webhook).
author: Acme Corp
license: MIT
homepage: https://example.com/notify-hub
min_core_version: 26.6.0
entry_point: plugin.py
class_name: NotifyHubPlugin
api_prefix: /notify-hub
permissions:
- code: alerts.read
name: Read alerts
description: Needed to list active alerts on the status dashboard.
event_subscriptions:
- alert.created
- alert.fired
- alert.resolved
- sla.breach_created
- vpn.connection_down
- vpn.connection_restored
nav_items:
- path: /
label: Notify Hub
icon: Bell
order: 10
settings_schema:
type: object
properties:
slack_webhook_url:
type: string
description: Slack incoming webhook URL
pagerduty_routing_key:
type: string
description: PagerDuty Events API v2 routing key
public_routes:
- path: /ingest
methods: [POST]
python_dependencies: []

Two validators exist for plugin.yaml: the runtime (backend/app/plugins/schema.py) and the dev SDK (freesdn-sdk/sdk/src/freesdn_sdk/manifest.py). They disagree in ways that matter:

AreaRuntime (authoritative)Dev SDK (freesdn-sdk validate)
python_dependencies specifiersname==version only; loose specifiers rejectedAccepts >=, ~=, and other PEP 440 forms
public_routes fieldPresent and validatedNot present; silently ignored
Scaffold templateN/AGenerates requests>=2.28 (which the runtime rejects)

Always test your manifest against the runtime before shipping to the marketplace or distributing the ZIP.


Use the dev SDK CLI to catch obvious problems early, then confirm against the runtime.

  1. Scaffold a new plugin:

    Terminal window
    freesdn-sdk init my-plugin --author "Your Name" --description "What it does"
  2. Validate the manifest locally:

    Terminal window
    freesdn-sdk validate
  3. Run a static import check:

    Terminal window
    freesdn-sdk check
  4. Build the ZIP:

    Terminal window
    freesdn-sdk package
    # produces my-plugin-1.0.0.zip
  5. Install against a real FreeSDN instance (requires super_admin):

    Terminal window
    curl -X POST https://your-instance/api/v1/plugins/install \
    -H "Authorization: Bearer <token>" \
    -F "file=@my-plugin-1.0.0.zip"

MethodPathPurposeRequired role
GET/api/v1/pluginsList installed pluginsorg_admin
GET/api/v1/plugins/{id}Plugin detail including cached manifestorg_admin
POST/api/v1/plugins/installInstall from uploaded ZIPsuper_admin
POST/api/v1/plugins/install-urlInstall by downloading from a URL; requires PLUGIN_ENABLE_DIRECT_URL_INSTALLS=true and host in PLUGIN_ALLOWED_DOMAINSsuper_admin
DELETE/api/v1/plugins/{id}Uninstall and remove filessuper_admin
POST/api/v1/plugins/{id}/upgradeUpgrade via new ZIPsuper_admin
POST/api/v1/plugins/{id}/enableEnable (org-scoped or global)org_admin / super_admin
POST/api/v1/plugins/{id}/disableDisable without removingorg_admin / super_admin
GET/api/v1/plugins/{id}/healthRuntime health for your orgorg_admin
GET/api/v1/plugins/{id}/settingsRead org-scoped settingsorg_admin
PUT/api/v1/plugins/{id}/settingsWrite org-scoped settingsorg_admin
GET/api/v1/plugins/{id}/public-authPublic-route auth status - whether a secret is set, declared routes, and required header namesorg_admin
POST/api/v1/plugins/{id}/public-auth/rotate-secretGenerate new HMAC secret (returned once)org_admin

  • Plugin System overview - two-tier model, lifecycle hooks, SDK interfaces, and the automation and AI bridges
  • Marketplace - browsing, installing from the marketplace, catalog signing, and the Ed25519 publisher key
  • Fabric catalog - how plugin triggers, actions, and AI tools surface in the universal app-interconnect