silo-plugin-sdk
Public Go SDK for building Silo plugins. Not a runtime plugin — this is a library that plugin authors depend on via go.mod.
silo-plugin-sdk is the source of truth for the plugin authoring contract.
First-party consumers—including the Silo host and the separate metadata,
marker, autoscan, and watch-provider plugin repositories—pin tagged semantic
versions. Local multi-repository workspaces may use go.work, but CI and
release builds resolve the SDK from a published module tag.
Packages
github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1 — generated protobuf code.
github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/capability — stable capability type constants for manifests and peer discovery.
github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/config — config-schema helpers.
github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/convert — type conversions.
github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/httpclient — credentialed JSON-over-HTTP client with bounded responses and typed status errors.
github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/imagevariant — canonical image-size variant strings.
github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/manifest — manifest loading/rendering.
github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/runtime — manifest subcommand + Runtime server scaffolding.
github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/runtimedefault — default Runtime implementation with BindHostBroker already wired; embed it to skip boilerplate.
github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/runtimehost — typed client for the host's RuntimeHost service, including event publishing, host info, catalog browsing, installed-plugin discovery, scoped streams, plugin-to-plugin HTTP calls, and plugin-owned config writes.
Capability families
The SDK ships protobuf contracts for every capability the host understands:
metadata_provider.v1
marker_provider.v1
media_analyzer.v1
image_resolver.v1
scheduled_task.v1
event_consumer.v1
auth_provider.v1
http_routes.v1
request_router.v1
scan_source.v1
watch_sync_provider.v1
network_access_provider.v1
audiobook_backend.v1
ebook_backend.v1
Plugins implement one or more, advertise them in manifest.json, and serve them over gRPC.
Author workflow
A typical plugin:
- Defines a
manifest.json using the protobuf-derived schema.
- Exposes a
Runtime gRPC server plus one or more capability servers.
- Supports the
manifest subcommand via pkg/pluginsdk/runtime so the host can introspect manifests without launching the plugin.
- Is installed either from a catalog or by uploading a trusted binary to a Silo server.
For a minimal self-describing plugin, see examples/hello-scheduled-task. For a plugin that calls back into the host via RuntimeHost (publishing events, listing libraries), see examples/hello-runtime-host. For a stub overlay-network provider, see examples/hello-network-access.
Operator-facing presentation
PluginManifest.presentation gives the Silo admin UI typed, plugin-level copy
and canonical links. It is optional for backward compatibility, but cataloged
plugins should provide a complete block:
{
"presentation": {
"display_name": "Example Plugin",
"summary": "A one-sentence explanation for a homelab administrator.",
"description_markdown": "A longer description of what the plugin does and when to use it.",
"setup_markdown": "1. Install the plugin.\n2. Add the required connection.\n3. Enable it for the relevant library.",
"homepage_url": "https://example.com/plugin",
"source_url": "https://github.com/Silo-Server/example-plugin",
"support_url": "https://github.com/Silo-Server/example-plugin/issues",
"changelog_url": "https://github.com/Silo-Server/example-plugin/releases",
"publisher_name": "Silo",
"publisher_url": "https://github.com/Silo-Server",
"license_spdx": "AGPL-3.0-or-later"
}
}
display_name is limited to 120 characters and summary to 240 characters;
both are concise card copy without leading or trailing whitespace.
description_markdown and setup_markdown use CommonMark-style Markdown;
raw HTML is not part of the contract, and each field is limited to 32 KiB.
- All URLs must be absolute
http or https links and must not contain
embedded credentials; each URL is limited to 2048 bytes.
- Publisher and source fields are self-declared identity information. Catalog
provenance and approval are assigned by the host/catalog, never by this block.
publisher_name is limited to 120 characters.
- Use an SPDX license expression of at most 120 characters. Use
NOASSERTION
when the repository has not declared a license rather than guessing one.
Curated catalog tooling should call
manifest.ValidateCatalogPresentation(manifest, canonicalRepositoryURL) to
require the complete block and prevent a published source_url from drifting
away from the repository that produced the release.
Calling back into the host
Plugins talk to the host through the RuntimeHost service, accessed via pkg/pluginsdk/runtimehost.Client. The host invokes Runtime.BindHostBroker on startup so plugins can dial back over the shared broker; runtimedefault handles that step for you. Available RPCs:
PublishEvent / PublishEventTo / PublishEventToInstallation — fire events into the host's bus, broadcast, addressed to a stable plugin_id, or addressed to one specific installation.
GetHostInfo — read host URL metadata for callback URLs and external-facing plugin links.
ListLibraries — enumerate libraries the operator has configured.
CheckMediaPresence — ask whether a given external id is already in the catalog.
ListInstalledPlugins — discover sibling plugins (e.g. routers a request plugin can target).
ListLibraryMedia / GetCatalogStats — read public-safe catalog rows and aggregate counts.
ResolveCatalogImageURLs — resolve stored poster/backdrop image paths into host-generated browser URLs.
MintScopedStream — create short-lived, narrowly scoped stream grants for guest/public workflows.
CallPluginHTTP — invoke another installed plugin's http_routes.v1 handler through the host control plane.
SetGlobalConfigEntry — persist plugin-owned config that admins didn't set via the manifest form.
For plugin-to-plugin JSON calls, prefer the helper layer:
plugins, err := host.ListInstalledPluginsByCapability(ctx, capability.RequestRouter)
if err != nil || len(plugins) == 0 {
return err
}
var out struct {
Accepted bool `json:"accepted"`
}
err = host.CallPluginJSON(ctx, runtimehost.CallPluginJSONRequest{
InstallationID: int(plugins[0].GetInstallationId()),
Path: "/api/request",
Request: map[string]any{"title": "The Matrix"},
Response: &out,
})
The auth_provider.v1 capability also exposes OAuth-flow RPCs (InitAuthorize, ExchangeCode, RefreshSession) for plugins that wrap external identity providers.
Watch sync providers
watch_sync_provider.v1 lets external plugins participate in Silo's host-owned
watch-provider pipeline. The host owns encrypted per-profile credentials,
authorization-code and device-code flow state, durable desired-state events,
retries, ordering, and reconciliation. Plugins are stateless protocol adapters:
they receive secrets only for the duration of an RPC, map rich movie, episode,
and series identity to an upstream service, and return typed apply or retry
outcomes.
Watch-sync plugins must not persist or log credentials, authorization codes,
provider flow state, or secret configuration. ApplyEvents is an at-least-once
contract; plugins must treat event_id as stable across retries and implement
convergent desired-state updates rather than increments. That rule also applies
to scrobble stops: replaying the same event ID must not create another play.
For playback events, completed is the host's authoritative watched decision;
plugins must not infer completion from watch_history_id or percentage alone.
Metadata consumers must likewise check optional season_number presence: zero
means Specials, while absence means no season scope. See
compatibility guidance
for the request and record rules.
Authenticated RPCs receive the same host-owned capability, configuration, and
credential data through WatchSyncAuthenticatedContext. The context exists
only for one invocation and is never plugin configuration or plugin state.
Credentials returned by any RPC are complete authoritative replacements, not
patches. The host validates and persists them before consuming results, pages,
or faults—even when the response contains a fault. If credential persistence
fails, the host commits no other response data.
Device-code plugins register both WatchSyncProvider and the separate
WatchSyncDeviceAuthorizationService. Keeping device authorization in a
second service preserves source compatibility for v0.12 Go providers that
implemented WatchSyncProviderServer directly. Register it without changing
the released CapabilityServers shape:
runtime.ServeManifestWithOptions(manifestJSON, version, servers,
runtime.WithWatchSyncDeviceAuthorization(deviceAuthServer))
A pending poll may replace its opaque provider state, polling interval, and
expiry; the host encrypts and persists those values before the next poll.
Those updates remain part of the same user challenge, so the original user code
and verification URL must stay valid until expiry. An explicitly empty
provider_state clears the prior state; omitting it retains the prior state.
WatchSyncProviderConfig is keyed by manifest config key and field, for example
provider.client_id. Scalar values are sent as strings and structured values
as JSON. Fields marked secret in the manifest are sent through secret_values;
undeclared fields are treated as secret. Plugins must accept configuration from
the RPC context rather than relying on process-global state.
Descriptors and events use the shared WatchSyncMediaType enum so advertised
support and delivered media cannot drift between string conventions. Apply
results pair their delivery status with a typed fault: successful results omit
the fault, temporary retries use TEMPORARY, rate limits use RATE_LIMITED
with an optional delay, and rejected events use a non-retryable fault code.
Connection-wide faults such as invalid credentials belong on the RPC response.
A SERIES media item describes the show itself: external_ids, title, and
year identify the series, and the series_*, season, and episode fields are
unused.
Ratings are integers from 1 to 10 in every rating field; the host owns
conversion to its own display scale. Plugins convert between the provider's
native scale and 1–10 by rounding half up and clamping to the valid range.
import_ratings means ListRemoteState returns RATING states, and
export_ratings means ApplyEvents handles both SET_RATING and
REMOVE_RATING; there is no separate removal flag. SET_RATING carries the
value in the event's rating field, where zero is never valid. Both operations
are convergent desired-state writes: resending the same value, or removing a
rating that is already absent, must return APPLIED or NO_CHANGE, never a
fault. The host sends rating events only for media types listed in
supported_media_types, and manifest validation requires that list to include
MOVIE or SERIES when either ratings flag is set.
ListRemoteState returns provider-neutral typed subrecords. watched carries a
play count and last-watched time; progress carries a fractional percentage and
paused time; favorite and watchlist carry list membership; rating carries
a 1–10 rating and when it was set. An item may contain multiple state families.
The host requests only the state families a sync phase needs, keeps that phase's
cursor fixed while following ephemeral page tokens, commits each successful
page, and only then persists the final next_cursor. complete_snapshot=true
means the traversal is authoritative; when false, missing items are not
deletions. In a complete RATING traversal, an item absent from the snapshot is
unrated. An incremental favorite, watchlist, or rating removal is an item whose
corresponding state has removed=true; it may omit media when
provider_item_key identifies a record previously returned to the host. When
provides_watchlist_order=true, watchlist traversals must be complete snapshots
and the order of returned watchlist states is the remote list order. Event
list_position is presence-aware: an explicit zero means the first position,
while omission means no requested ordering.
Network access providers
network_access_provider.v1 lets a resident plugin give the deployment an
overlay-network identity (Tailscale, NetBird) and reverse-proxy overlay
traffic to the host's local listeners. The host starts these plugins at boot,
restarts them on crash, stores their per-instance state encrypted, and
aggregates status across the API server and proxy nodes. See
docs/network-access-provider.md for the
proxy contract, GetHostInfo fields, instance state, and enrollment rules.
Scan sources
The scan_source.v1 capability is for Autoscan providers. The host owns the
poll timer, marker persistence, path rewrites, validation, dedupe, and scan
enqueueing. The plugin only polls its upstream provider and returns changed
absolute paths in that provider's source namespace. The host applies autoscan
source rewrite rules before enqueueing scans.
The host resolves the configured upstream connection and passes it to
PollChanges for each poll. Plugins should treat request values such as API
keys as transient secrets and avoid logging them without redaction.
Self-describing binaries
Direct binary upload works best when the plugin embeds a manifest template and computes its own executable checksum at runtime before returning Runtime.GetManifest. That keeps the plugin installable without requiring a checked-out Silo repository or a sibling manifest.json file at upload time. The example plugin shows the pattern.
Compatibility
Compatibility and versioning expectations are documented in docs/compatibility.md.
Releases
SDK releases are cut from semver tags such as v0.1.0 and published through GitHub Actions.
- Additive public API changes belong in a new minor release.
- Compatible fixes and documentation updates belong in a patch release.
- Breaking public API, protobuf, or manifest contract changes require a new major version.
Before downstream repos stop using local workspace overrides, the required SDK commit must be pushed and tagged here first.
Build & test
make proto # regenerate protobuf code (installs tools under ./bin/ as needed)
go test ./...
Contributing
Read CONTRIBUTING.md and
docs/compatibility.md before opening a pull request.
Public Go, protobuf, runtime, and manifest changes should start as an issue and
identify affected downstream repositories.
Naming and branding
Give your plugin its own name and mention Silo in the summary, for example
"Trakt sync plugin for Silo". Repository and package names such as
silo-plugin-trakt are fine, since "silo" only describes what the code plugs
into. Avoid "Silo[word]" product names such as SiloTrakt, which read as
official, and do not use the Silo logo as your plugin's icon. Set
publisher_name to yourself, not "Silo", unless the plugin is published by the
project. The full guidance, including what needs no permission, is at
https://siloserver.org/brand.
License
Apache-2.0. See LICENSE.