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.
Top-level fields
Section titled “Top-level fields”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. |
id field rules
Section titled “id field rules”The id you choose becomes a permanent namespace prefix for everything the plugin owns:
- REST routes mount at
/api/v1/{id}(unless you overrideapi_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.
Reserved IDs
Section titled “Reserved IDs”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, internalNested models
Section titled “Nested models”PluginPermission
Section titled “PluginPermission”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. |
Permission codes
Section titled “Permission codes”| 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() |
PluginDependency
Section titled “PluginDependency”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. |
PluginNavItem
Section titled “PluginNavItem”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. |
PluginPublicRoute
Section titled “PluginPublicRoute”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. |
Required HMAC headers
Section titled “Required HMAC headers”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.
Python dependency pinning
Section titled “Python dependency pinning”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 |
Hash-pinned requirements.txt
Section titled “Hash-pinned requirements.txt”If python_dependencies is non-empty, you must also ship a requirements.txt in the ZIP root. The runtime verifies:
- The file contains
--hash=sha256:annotations for every package (generate withpip-compile --generate-hashes). - Every bare package name in
python_dependenciesappears in the requirements file. - The runtime opt-in flag
PLUGIN_ALLOW_RUNTIME_PYTHON_DEPS=trueis set (default isfalse).
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.
Capacity limits
Section titled “Capacity limits”| 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 |
Full example manifest
Section titled “Full example manifest”This is the notify-hub example from the published SDK, adapted with correct == pinning:
id: notify-hubname: Notify Hubversion: 1.0.0description: > Forwards FreeSDN alerts and SLA breaches to external notification channels (Slack, PagerDuty, generic webhook).author: Acme Corplicense: MIThomepage: https://example.com/notify-hubmin_core_version: 26.6.0
entry_point: plugin.pyclass_name: NotifyHubPluginapi_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: []SDK vs runtime divergences
Section titled “SDK vs runtime divergences”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.
Validation workflow
Section titled “Validation workflow”Use the dev SDK CLI to catch obvious problems early, then confirm against the runtime.
-
Scaffold a new plugin:
Terminal window freesdn-sdk init my-plugin --author "Your Name" --description "What it does" -
Validate the manifest locally:
Terminal window freesdn-sdk validate -
Run a static import check:
Terminal window freesdn-sdk check -
Build the ZIP:
Terminal window freesdn-sdk package# produces my-plugin-1.0.0.zip -
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"
Management endpoints
Section titled “Management endpoints”| 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 |
Next steps
Section titled “Next steps”- 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