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.

Field Type Default Required Validation
id string - 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.
name string - Yes 1-200 chars.
version string - Yes Strict semver MAJOR.MINOR.PATCH - all numeric, exactly 3 parts. 1.0.0 is valid; 1.0 and 1.0.0-beta are not.
description string "" No Max 2000 chars.
author string "" No Max 200 chars.
license string "MIT" No Free string; not validated.
homepage string or null null No Max 500 chars. Must start with http:// or https:// if provided.
min_core_version string "1.0.0" No Free string. Recorded, not enforced.
entry_point string "plugin.py" No Must match ^[a-zA-Z0-9_/\-]+\.py$. No .., no leading / or \. Resolved relative to the plugin directory at load time.
class_name string - Yes Must be a valid Python identifier and must not start with _. The loader imports entry_point and looks for this class.
permissions list of PluginPermission [] No SDK capability declarations. The confused-deputy guard enforces these at request time.
api_prefix string or null null No Must match ^/[a-z0-9][a-z0-9/_-]*$. No ... Defaults to /{id} at mount time if omitted. Routes are mounted at /api/v1{api_prefix}.
dependencies list of PluginDependency [] No Inter-module dependencies. Declared only, not enforced at install.
nav_items list of PluginNavItem [] No Sidebar navigation entries the UI renders when the plugin is active.
public_routes list of PluginPublicRoute [] No Unauthenticated routes protected by HMAC. Runtime-only - the published dev SDK manifest does not include this field.
event_subscriptions list of string [] No Event-bus patterns the plugin may subscribe to. EventSDK.subscribe() rejects any pattern not declared here. Bare * and # wildcards are rejected.
settings_schema object or null null No JSON Schema describing the settings the plugin stores via PluginSettingsSDK. FreeSDN records it but does not validate settings values against it at runtime.
python_dependencies list of string [] No Max 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.

Field Type Default Notes
code string (required) The SDK capability string. See the permission codes table below.
name string (required) Human-readable name shown in the UI.
description string "" Short explanation of why the plugin needs this capability.
Code Core permission mapped What it grants
devices.read device:read Read device inventory via ctx.devices.list() and ctx.devices.get()
devices.write device:write Register devices via ctx.devices.register_device()
alerts.read alert:read List alerts via ctx.alerts.list()
alerts.write alert:write Create and resolve alerts via ctx.alerts.create() / ctx.alerts.resolve()

Declares a dependency on a FreeSDN native module.

Field Type Default Notes
module_id string (required) ID of the required module (e.g. network, cameras).
min_version string "1.0.0" Minimum acceptable module version. Not enforced at install.
optional boolean false Whether the plugin degrades gracefully without this module.

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

Field Type Default Notes
path string (required) Route path relative to the plugin’s frontend mount.
label string (required) Display label in the sidebar.
icon string "Package" Lucide icon name.
order integer 50 Sort 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.

Field Type Default Notes
path string (required) Must match ^/[a-z0-9][a-z0-9/_-]*$. No ... Trailing slash is stripped.
methods list 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:

Header Contents
X-FreeSDN-Plugin-Org Organization ID (UUID) the request targets
X-FreeSDN-Plugin-Timestamp Unix timestamp (seconds). Must be within 300 seconds of server time.
X-FreeSDN-Plugin-Nonce 16-128 char unique value per request
X-FreeSDN-Plugin-Signature sha256= + 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:

Rule Runtime Dev SDK (freesdn-sdk validate)
Specifier format Must be name==version exactly Accepts >=, ~=, !=, etc.
Extras syntax (pkg[extra]) Rejected Rejected
Specs starting with - Rejected Rejected
Specs containing ; (environment markers) Rejected Rejected

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.


Resource Limit
ZIP archive (compressed) 50 MB
ZIP archive (uncompressed) 200 MB
python_dependencies entries 50
Automation triggers 50 per plugin
Automation actions 50 per plugin
AI tools 20 per plugin
Registered devices 1,000 per plugin
HTTP response (via PluginHTTPClient) 10 MB
HTTP timeout 60 seconds
AI tool result payload 256 KB
Settings blob per PUT /settings 32 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:

Area Runtime (authoritative) Dev SDK (freesdn-sdk validate)
python_dependencies specifiers name==version only; loose specifiers rejected Accepts >=, ~=, and other PEP 440 forms
public_routes field Present and validated Not present; silently ignored
Scaffold template N/A Generates 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"

Method Path Purpose Required role
GET /api/v1/plugins List installed plugins org_admin
GET /api/v1/plugins/{id} Plugin detail including cached manifest org_admin
POST /api/v1/plugins/install Install from uploaded ZIP super_admin
POST /api/v1/plugins/install-url Install by downloading from a URL; requires PLUGIN_ENABLE_DIRECT_URL_INSTALLS=true and host in PLUGIN_ALLOWED_DOMAINS super_admin
DELETE /api/v1/plugins/{id} Uninstall and remove files super_admin
POST /api/v1/plugins/{id}/upgrade Upgrade via new ZIP super_admin
POST /api/v1/plugins/{id}/enable Enable (org-scoped or global) org_admin / super_admin
POST /api/v1/plugins/{id}/disable Disable without removing org_admin / super_admin
GET /api/v1/plugins/{id}/health Runtime health for your org org_admin
GET /api/v1/plugins/{id}/settings Read org-scoped settings org_admin
PUT /api/v1/plugins/{id}/settings Write org-scoped settings org_admin
GET /api/v1/plugins/{id}/public-auth Public-route auth status - whether a secret is set, declared routes, and required header names org_admin
POST /api/v1/plugins/{id}/public-auth/rotate-secret Generate 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

All product names, logos, and brands are property of their respective owners. FreeSDN is an independent project and is not affiliated with or endorsed by the vendors it integrates with. See Trademarks.