Plugin Architecture¶
Decode-Orc uses a plugin-first stage system where every processing stage — including the stages shipped with Decode-Orc itself — is loaded at runtime through the same plugin framework. There is no privileged path for built-in stages; all stages are self-contained shared libraries that are discovered, loaded, and registered through a common host runtime.
Core Goals¶
- Uniform loading: Decode-Orc-supplied stages and third-party stages follow identical contracts, loader paths, and registry entries.
- Independent development: A third-party plugin can be developed, built, and distributed without any changes to the Decode-Orc source repository.
- Stable binary interface: Explicit ABI and API version numbers govern compatibility, and the host refuses to load mismatched plugins with a clear diagnostic.
- Registry-based distribution: Plugins are declared in a persistent YAML
registry and can be fetched automatically from GitHub release assets at
startup. Non-core registry entries must be marked trusted before they are
downloaded or loaded — entries added through the GUI or with
orc-cli plugins add --trustedare trusted at add time, while entries that arrive from outside the application default to untrusted — and downloaded artifacts are verified against a recorded SHA-256 checksum (see Distribution integrity). Plugin binaries are not code-signed.
Runtime Flow¶
Host startup
│
├─ Read user plugin registry YAML
│ ($XDG_CONFIG_HOME/decode-orc/stage-plugins.yaml or
│ ~/.config/decode-orc/stage-plugins.yaml)
│
├─ Collect default plugin search paths
│ (development build dir or executable-relative install dir)
│
├─ For each registry entry
│ ├─ Check trust_state (untrusted non-core entries are skipped
│ │ with a warning diagnostic; nothing is downloaded or loaded)
│ ├─ Resolve local path (download from GitHub releases if absent;
│ │ verify sha256 checksum, quarantining mismatches)
│ ├─ dlopen / LoadLibrary the shared library
│ ├─ Resolve entrypoints:
│ │ orc_get_stage_plugin_descriptor
│ │ orc_register_stage_plugin
│ ├─ Validate host_abi_version, plugin_api_version, toolchain_tag
│ └─ Call orc_register_stage_plugin → StageRegistry::register_stage
│
├─ Merge registry paths, default search paths, and ORC_STAGE_PLUGIN_PATHS
│
└─ Consumer flows (GUI / CLI) query the same StageRegistry via presenters
Loading happens once at startup. Hot-reload is not supported in the current version.
Plugins are loaded with RTLD_LOCAL (each plugin's symbols stay private to
that plugin), and plugin libraries are reference-counted: every registered
stage factory and every live stage instance holds a keep-alive reference, so
a plugin's code is never unmapped while one of its stages can still run.
orc-gui and orc-cli share the same persistent registry file. The difference
between development and packaged installs is the default plugin search path, not
a separate per-application registry.
Plugin Binary Format¶
Plugins are native shared libraries:
| Platform | Extension |
|---|---|
| Linux | .so |
| macOS | .dylib |
| Windows | .dll |
Each plugin must export two C-linkage entrypoints (C++ types cross the boundary — see Binary Compatibility Model below):
// Returns the plugin descriptor (ABI version, API version, toolchain tag,
// metadata).
// All descriptor pointer fields must reference static storage.
const orc::StagePluginDescriptor* orc_get_stage_plugin_descriptor();
// Receives the host service table and registers one or more stage types
// with the host by invoking register_stage once per exported stage.
// Returns true when every stage registered successfully.
bool orc_register_stage_plugin(
const orc::OrcPluginServices* services, void* context,
bool (*register_stage)(void* context, const char* stage_name,
orc::OrcStageFactoryFn factory),
const char** error_message);
Both symbols are declared with ORC_STAGE_PLUGIN_EXPORT (C linkage, default
visibility). Stage factories match orc::OrcStageFactoryFn and return
std::shared_ptr<orc::DAGStage>.
Binary Compatibility Model¶
The plugin boundary is a C++ ABI, not a C ABI. std::shared_ptr,
std::string, STL containers, exceptions, and virtual-function tables all
cross the host/plugin boundary. Matching version numbers are therefore
necessary but not sufficient: a plugin must be built with the same compiler
family, the same C++ standard library, and a compatible build configuration
as the host. On Windows this includes the CRT flavour — a Debug-CRT plugin
cannot be loaded by a Release-CRT host.
Since ABI v5 the toolchain requirement is enforced at load time: the
descriptor's toolchain_tag field (populated by the ORC_SDK_TOOLCHAIN_TAG
macro) encodes the compiler family and major version, the C++ standard
library, and — on Windows — the CRT flavour, e.g. gcc14/libstdc++,
clang17/libc++, msvc19/msvc-stl/release-crt. The host requires the
plugin's tag to equal its own tag exactly and rejects the plugin with a
diagnostic naming both tags otherwise.
The host requires exact equality for both host_abi_version and
plugin_api_version; a mismatch in either causes the plugin to be rejected
with a logged diagnostic. The services_size field in OrcPluginServices
is an intra-version safety net only: it guards access to service-table
fields appended within the current ABI version. It is not a cross-version
compatibility mechanism.
Compatibility Gating¶
Two version numbers plus the toolchain tag govern compatibility. All three
are checked before a plugin is accepted; a mismatch causes the plugin to be
skipped with a logged diagnostic. For guidance on which changes force a
host_abi_version bump, see the
ABI impact decision table.
host_abi_version¶
Controls the binary ABI: the layout of StagePluginDescriptor, the entrypoint
signatures, and the register_stage callback contract.
Current value: 10 (the concrete observer classes and the Observer base
were removed from the plugin SDK — they are host-internal and reached through
the IObservationService added in ABI 9. The deprecated pre-tier observation
include-path shims were removed with them). The authoritative per-version change
log is orc/sdk/abi_history.yaml, rendered as the version-history table in
plugin-sdk.md.
Bumped when any of the following change:
- StagePluginDescriptor field order or alignment
- Entrypoint function signatures
- Callback calling convention
- OrcPluginServices gains or loses a field
- IStageServices gains or loses methods
- A public SDK contract header or class is removed
- The vtable layout of a contract type crossing the boundary (e.g.
VideoFrameRepresentation) changes
plugin_api_version¶
Controls the stage contract: the DAGStage virtual interface,
ParameterizedStage, TriggerableStage, ArtifactPtr, ObservationContext,
and NodeTypeInfo semantics.
Current value: 2
Bumped when any of the following change:
- A DAGStage virtual method is added, removed, or reordered
- ParameterValue variant types change
- NodeTypeInfo struct layout changes
- execute() or trigger() lifecycle semantics change incompatibly
- The primary frame-data type changes (e.g. field → frame representation)
toolchain_tag¶
Identifies the build environment the plugin binary was produced with (ABI v5+). Compared as an exact string against the host's own tag; see the Binary Compatibility Model above for the encoding.
Plugin Registry¶
The registry is a YAML file that tracks the installed plugin set. The current runtime stores it at:
- Linux:
$XDG_CONFIG_HOME/decode-orc/stage-plugins.yamlor~/.config/decode-orc/stage-plugins.yaml - macOS:
~/.config/decode-orc/stage-plugins.yamlunlessXDG_CONFIG_HOMEis set - Windows:
%APPDATA%/decode-orc/stage-plugins.yaml
If none of those platform defaults are available, the host falls back to
.decode-orc/stage-plugins.yaml under the current working directory.
Both orc-gui and orc-cli read and write this same registry file.
Each entry records:
| Field | Description |
|---|---|
plugin_id |
Unique string identifier |
plugin_version |
Plugin release version |
path |
Resolved local path to the plugin binary |
source_repo_url |
Repository URL for the plugin source or release |
artifact_source |
local_path or github_release_asset |
release_asset_url |
Direct URL to the GitHub release asset |
release_tag |
Release tag associated with the asset |
release_asset_name |
Expected artifact filename |
target_platform |
Optional platform hint for cache selection |
local_dev_path |
Optional development override used before remote download |
enabled |
Whether the plugin is loaded at startup |
trust_state |
Trust level, enforced before loading: entries other than trusted are neither downloaded nor dlopened unless is_core_plugin is set. Trust is a distinct decision from adding or enabling: adding a plugin (local file, URL, or from the curated index) records the entry but leaves it untrusted unless the user confirms trust in the explicit confirmation dialog, and toggling the Enabled checkbox never changes trust. Trust is granted through the Plugin Manager's Trusted column (with a confirmation dialog) or orc-cli plugins trust <id> / untrust <id>; entries that arrive from outside the application (e.g. a hand-edited registry file) also default to untrusted |
license_spdx |
SPDX license identifier |
is_core_plugin |
Marks entries supplied by Decode-Orc itself; implicitly trusted |
required_host_abi |
Host ABI the plugin was built for. Enforced before download and load: a non-zero value that does not equal the host's host_abi_version means the entry is neither downloaded nor dlopened — it stays visible with a "needs a rebuild for Orc ABI N" message in orc-cli plugins list and the GUI Plugin Manager. 0 means unspecified (not gated); is_core_plugin entries are exempt |
sha256 |
Optional SHA-256 digest (64 hex chars) of the plugin binary for github_release_asset entries; verified after download and on cache hits |
Entries with artifact_source: github_release_asset and an absent or empty
path are resolved automatically: the host downloads the binary from the
declared GitHub release and caches it to
~/.config/decode-orc/plugin-cache/<platform>/ before loading. The download
only happens for trusted entries. When the entry records a sha256, the
digest is checked both after a fresh download and on every cache hit; a
mismatching file is quarantined (renamed with a .quarantined suffix) and
reported as an error, and a mismatching cache hit triggers one fresh
download attempt. When no sha256 is recorded, the host loads the artifact
but emits a warning that its integrity could not be verified.
Curated plugin index¶
Alongside manual URL entry, the host offers a curated index of third-party
plugins for discovery and one-click install. The index is a versioned YAML
document read from a configurable URL (default: the orc-plugin-registry/
index.yaml on the Decode-Orc default branch; override with the
ORC_PLUGIN_INDEX_URL environment variable). Because the host reads the branch
head over plain HTTPS, a merged registry change publishes immediately — no host
release and no registry tag are required.
The host refreshes the index on demand — when the Plugin Manager's Browse
Plugins… dialog opens or a orc-cli plugins search / info / install command
runs — asynchronously, falling back to the last-good cached copy
(<config>/plugin-index-cache.yaml) when offline.
Schema (registry_schema: 1):
| Field | Description |
|---|---|
registry_schema |
Index schema major version. Hosts ignore unknown fields, so additions within a major are non-breaking; a newer major is parsed best-effort so an older host still resolves compatible builds |
plugins[].id |
Unique plugin identifier |
plugins[].display_name |
Human-readable name |
plugins[].description |
Short description |
plugins[].tags |
Search tags |
plugins[].maintainer |
Maintainer name |
plugins[].license_spdx |
SPDX license identifier (mandatory) |
plugins[].source_repo_url |
Plugin source repository |
plugins[].artifacts[] |
One build per (platform, host ABI) |
artifacts[].platform |
linux, macos, or windows (a more specific value such as linux-x86_64 is matched by prefix) |
artifacts[].host_abi |
Host ABI this build targets |
artifacts[].url |
Direct release-asset download URL |
artifacts[].sha256 |
Mandatory 64-hex digest, carried into the local registry on install |
artifacts[].plugin_version |
Plugin release version |
artifacts[].min_host_app_version |
Minimum host application version (optional) |
Artifacts are resolved by (platform, host ABI) using the same compatibility
gating as the registry (Compatibility Gating): an
older host that finds no matching build reports "no build for this host" before
downloading anything, instead of failing at dlopen. Installing from the index
records a registry entry carrying the index's sha256, left untrusted
until the user confirms trust.
Contribution model: plugin authors open a pull request adding their entry;
repository CI validates every PR (schema conformance, artifact naming including
the ABI token, URL reachability, digest match, and a present SPDX license), and
a maintainer's merge is the curation and trust decision. See
orc-plugin-registry/README.md.
Distribution integrity¶
What the host verifies before running plugin code, and what it does not:
Verified:
trust_state— untrusted non-core registry entries are never downloaded ordlopened (stage_plugin_registry.cpp,stage_registry.cpp).sha256— downloaded artifacts (and cached copies) are checked against the registry digest when one is recorded (plugin_remote_loader.cpp).host_abi_versionandplugin_api_version— exact-match at load time (stage_plugin_loader.cpp).required_host_abi— a non-zero registry value that does not match the host ABI blocks download and load early, before any bytes are fetched (stage_plugin_registry.cpp).toolchain_tag— exact-match against the host's compiler/stdlib/build configuration tag (ABI v5).
What the curated index adds:
- Mandatory digests. Every index artifact must carry a
sha256, enforced by the registry PR validation before merge; installing from the index copies that digest into the local registry so the download is always integrity- checked (unlike a hand-entered URL, where a digest is optional). - Curated review. A human maintainer's merge is the trust decision for a listed plugin: the PR workflow verifies the artifact URL resolves and its bytes hash to the declared digest before the entry can go live.
Not verified (future work):
- Signed index. The index itself is fetched over HTTPS but is not cryptographically signed, so its authenticity rests on transport security and repository access control. Signing the index (e.g. minisign/sigstore) is a documented follow-on.
- Code signing. Plugin binaries carry no cryptographic signature; the
sha256authenticates an artifact only as strongly as the index or registry entry that records it. - Unsigned local registry. The on-disk registry
(
stage-plugins.yaml) and the cached index copy are plain files with no signature. An attacker who can write to the user's config directory could fliptrust_stateor point an entry at a malicious binary with a matching digest; the host trusts the local registry as much as the filesystem it lives on. Confining that directory's permissions is the user's responsibility until registry signing lands.
Project-Level Plugin Metadata¶
Project files may also include a root-level required_plugins block. This is a
snapshot of the subset of third-party plugin registry metadata that is actually
required by the current project DAG.
Each required_plugins entry stores:
| Field | Description |
|---|---|
plugin_id |
Plugin identifier expected by the project |
plugin_version |
Last known plugin version used when saved |
source_repo_url |
Repository URL for the plugin source or release |
artifact_source |
local_path or github_release_asset |
release_asset_url |
Direct release asset URL when known |
release_tag |
Release tag associated with the saved metadata |
release_asset_name |
Expected artifact filename |
target_platform |
Optional platform hint |
local_dev_path |
Optional development override path |
license_spdx |
SPDX license identifier |
is_core_plugin |
Whether the plugin is Decode-Orc supplied |
required_host_abi |
Host ABI version expected by the plugin |
stage_names |
Stage names from that plugin that are still referenced by the project |
The host rewrites this block on every save. It only keeps entries whose
stage_names are still referenced by the project, so stale third-party plugin
references are removed automatically when plugin-backed stages are deleted.
When a project references a missing runtime stage, the loader consults this saved block to enrich the error message with the expected plugin id and, when available, a repository or release URL.
In addition to registry entries, the host also searches the standard plugin install/build locations relative to the executable:
- Linux install:
../lib/orc-stage-plugins - macOS install:
../PlugIns/orc-stage-plugins - Windows install:
orc-stage-plugins - Development builds: the compiled-in build plugin directory when the executable-relative install path does not exist
Artifact Naming Convention¶
All plugin release artifacts follow a consistent naming scheme:
The optional _abi<N> token records the host ABI version the binary targets,
so one release can carry builds for several host ABIs. The host prefers the
asset tagged for its own ABI, falls back to a legacy (untagged) name — which
the load-time gate still validates — and reports "needs a rebuild" rather than
downloading an asset tagged for an incompatible ABI. Untagged names remain
valid.
Examples:
- orc-plugin_skeleton_passthrough_linux.so (legacy, untagged)
- orc-plugin_skeleton_passthrough_linux_abi8.so (ABI-tagged)
- orc-plugin_skeleton_passthrough_macos_abi8.dylib
- orc-plugin_skeleton_passthrough_windows_abi8.dll
External plugin repository names follow the same prefix convention
(orc-plugin_<name>), both for official decode-orc organization repositories
and as the recommended standard for third-party authors.
Stage Services¶
Plugins interact with the host through explicit service interfaces rather than
direct calls into host internals. The host builds an OrcPluginServices table
(declared in <orc/plugin/orc_plugin_services.h>) and passes it as the first
argument to orc_register_stage_plugin(). The table provides:
log— pre-formatted message logging routed to the host logger (plugins use theORC_PLUGIN_LOG_*macros)render_colour_preview— converts a decodedColourFrameCarrierto a display-readyPreviewImagestage_services— optional pointer to the consolidatedIStageServicesinterface (may benullptrwhen the capability is unavailable)observation_service— optional pointer toIObservationService(appended in ABI 9, guarded byservices_size); runs the standard observers by stable string id so plugins no longer link the concrete observer classes.nullptron any host older than ABI 9. Obtained viaorc::plugin::get_observation_service(). See the Plugin SDK Developer Guide
The IStageServices contract (declared in <orc/plugin/orc_stage_services.h>)
currently exposes buffered file-output factories used by sink stages:
create_buffered_file_writer_uint8(), create_buffered_file_writer_uint16(),
and create_buffered_file_writer_int16(). It does not currently provide
artifact delivery, logging, or progress reporting.
Plugins store the table with orc::plugin::set_services() at the start of
orc_register_stage_plugin, and obtain the IStageServices pointer later via
orc::plugin::get_stage_services() (which returns nullptr when the host does
not provide it).
Stage Tools¶
Stages that expose interactive tooling (custom editors, analysis views) publish
optional StageToolDescriptor records through the StageToolProvider mixin
(declared in <orc/plugin/orc_stage_tooling.h>). The host discovers and routes
these descriptors through the presenter layer — no hardcoded stage-name branches
exist in host tool dispatch.
Analysis tools (dropout analysis, SNR analysis, burst-level analysis) follow the
same pattern via AnalysisToolDescriptor and AnalysisToolProvider.
SDK Boundary Enforcement¶
The source-level plugin boundary is an allowlist of SDK contract
headers: the <orc/plugin/...> family (ABI, entrypoints, services) and the
<orc/stage/...> family (stage, frame, observation, preview, and utility
contracts). The complete permitted set is listed in the
Plugin SDK Developer Guide (SDK Headers section); anything
else in the host tree is private.
Enforcement happens at two levels:
- Compile time — the
orc-plugin-sdk/orc::plugin-sdktarget propagates only the SDK include tree, the spdlog/fmt usage requirements the SDK headers themselves need, and theorc-sdk-supportstatic library (support-tier helper symbols). The host libraries are not linked — the former$<LINK_ONLY:orc-core>tether was removed so plugins compile and link against the SDK alone. Private host include directories and third-party dependencies are therefore invisible to plugin translation units; including a private host header fails the plugin's compile. Third-party libraries a plugin uses directly must be declared by the plugin's own CMake target. - Scan gates — two hard-fail CI gates (
ctest -L sdk) run on the in-tree plugin treeorc/plugins/stages/:check_plugin_private_includes.shfails on any include that is not an allowlisted SDK header, a plugin-local header, a standard-library or platform header, or a permitted third-party header;check_plugin_private_links.shfails on plugin build files that link private host targets directly. Third-party authors run the same scripts in standalone mode against their own repository (see Publishing a plugin).
Third-Party Plugin Repositories¶
An official skeleton template lives at simoninns/orc-plugin_skeleton. It provides:
- Minimal buildable plugin scaffold (CMake + SDK-only includes + sample stage)
- Unit tests
- Linux / macOS / Windows CI workflows
- Packaging conventions
- SPDX / licensing guidance
Use it as the starting point for any new out-of-tree plugin. For a step-by-step walkthrough from an empty directory to a loaded plugin, see the Plugin Author Guide; for release and registry submission, see the Plugin Publishing Guide.