Skip to content

feat: generic plugin runtime SPI, web admin manager, and desktop extension host - #498

Open
BillyOutlast wants to merge 13 commits into
Drop-OSS:developfrom
Heretek-Games:upstream-pr/generic-plugin-spi
Open

BillyOutlast wants to merge 13 commits into
Drop-OSS:developfrom
Heretek-Games:upstream-pr/generic-plugin-spi

Conversation

@BillyOutlast

@BillyOutlast BillyOutlast commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR introduces an extensible, generic Plugin SPI and Extension Host for both the Drop server and the desktop client, enabling community addons, integrations, and customization without modifying Drop core.

The runtime SPI is completely generic and contains zero vendor-specific, emulator, network, or payment logic. Every concrete plugin (metadata providers, store scanners, payment gateways, cloud save resolvers, emulation/networking integrations) lives in an external repository.

Accompanied by the public plugin SDK at Drop-OSS/drop-plugin-sdk (transferring from Heretek-Games/drop-plugin-sdk), which provides TypeScript contracts, unit test mock harnesses (MockPluginContext), and automated packaging tools (drop-plugin pack/sign/verify).

What's included

1. Server Plugin SPI (PluginManager)

  • Dynamic HTTP routes: Plugins mount sub-routers under /api/v1/plugins/:id/* with automatic authentication/ACL resolution.
  • Persistent storage & migrations: Namespaced key-value storage with schema migration lifecycles (ctx.storage).
  • Dynamic WebSockets: Authenticated channels with regex subscription gates (ctx.registerWebSocket), plus opt-in public channels.
  • Server Event Bus: Inter-plugin and core lifecycle events with scoped subscription cleanup.
  • Bundle packaging & integrity: Unpacks verified .dropplugin archives (and bundle JSON), enforcing SHA-256 digests and optional HMAC-SHA256 signatures. signatureVersion: 2 signatures cover the canonical manifest, so id/version/capabilities tampering invalidates them.
  • Registry pinning: Optional allow-list with exact version/checksum pinning (DROP_PLUGIN_REGISTRY).

2. Web Admin Plugin Manager (/admin/settings/plugins)

  • Dedicated web administration UI for headless server deployments.
  • Install by URL, .dropplugin upload, or multi-file bundle JSON.
  • Update checks against community/custom manifest registries.
  • Live toggle (enable/disable), reload, and remove plugin bundles.

3. Desktop Extension Host (ClientPluginManager)

  • Dynamic UI slots: <PluginSlot name="..." /> injects plugin components into game-detail:badges, game-detail:actions, game-detail:panels, settings:tabs, topbar:status, sidebar:nav, and overlay slots.
  • Custom Play Actions: contextual actions on the game detail page (see below).
  • Launch pipeline interceptors: executeLaunchPipeline() wraps game execution with pre-launch and post-exit hooks, rolling completed stages back in reverse order if a hook aborts.
  • Path guard & confinement: path_guard rejects directory traversal (..) and symlink escapes.
  • Command capability allowlist: native command execution requires explicit manifest.client.commands entries and is enforced in the Rust command layer. There is deliberately no blocklist: allowlisted commands run unsandboxed with the user's privileges, so only trusted plugins should be installed (documented in the UI and docs).

Plugin capabilities and SPIs

Surface Capability SPI registration Example plugin behavior
Server routes ctx.registerRoute REST endpoints under /api/v1/plugins/:id/*
Server storage ctx.storage Namespaced KV state + migrateStorage
Server websocket ctx.registerWebSocket / ctx.registerPublicWebSocketChannel Lobby presence, live server status
Server events ctx.broadcast / ctx.subscribe Inter-plugin lifecycle notifications
Server network ctx.fetch Outbound HTTP with a declared capability
Server metadata:provider ctx.registerMetadataProvider IGDB/SteamGridDB/LaunchBox-style metadata
Server commerce:payment ctx.registerPaymentGateway Stripe/BTCPay-style payment gateways
Server cloudsave:provider ctx.registerCloudSaveResolver Ludusavi-style save path resolution
Desktop ui:slot/ui:sidebar/ui:topbar ctx.registerSlot / registerSidebarItem / registerTopBarItem Panels, badges, navigation entries
Desktop ui:play-action ctx.registerPlayAction "Launch via mod loader", "Join lobby", etc.
Desktop ui:context-menu ctx.registerGameMenuItem Extra per-game actions
Desktop game:launch-hook ctx.registerLaunchHook Prepare/rollback game state around launch
Desktop game:fs, game:scan ctx.gameFs, ctx.gameScanner Scoped file access + executable/file scanning
Desktop client:library-scan ctx.registerStoreScanner Third-party store scanners
Desktop metadata:provider / cloudsave:provider desktop SPI registrations Client-side metadata and save paths
Desktop system:command ctx.system.run Allowlisted native commands (no shell)
Desktop client:ws ctx.serverWs Server channel bridge through the desktop host

Play Actions

Play Actions are a client-plugin SPI for contextual launch commands. A plugin registers a provider:

ctx.registerPlayAction((gameId) => [
  { id: "modded", name: "Launch with Mod Loader", execute: (ctx) => { /* prepare */ } },
]);

The desktop game-detail menu renders the contributed actions next to the normal launch button. Running one goes through executeLaunchPipeline(), which orders hooks by stage and priority (validate -> prepare -> stage -> network -> launch -> cleanup -> restore -> sync), and rolls completed pre-launch stages back in reverse order if any stage throws. Plugins that abort a launch can never leave half-applied state behind.

Examples of what plugins can build

  • A metadata provider that enriches games from an external database.
  • A store scanner that imports an existing launcher library.
  • A cloud save resolver that computes save paths for a game.
  • A payment gateway that issues payment intents and handles webhooks.
  • A multiplayer helper that prepares a mesh/VPN session in a pre-launch hook and tears it down post-exit.
  • A UI panel that shows plugin-owned status, badges, or controls on the game detail page.

Security model

  • Plugins are not sandboxed: server plugins run in-process with the server's privileges; desktop plugins run with the client's privileges. Capabilities are validation/review metadata, not confinement.
  • Bundles are verified on load: entry checksum, per-file SHA-256 checksums, aggregate digest, optional HMAC signature (v2 covers the manifest), and optional registry allow-list + version/checksum pinning. DROP_PLUGIN_REQUIRE_SIGNATURE=true refuses unsigned bundles.
  • The desktop native command layer runs binaries directly (no shell) and only permits names declared in manifest.client.commands.
  • WebSocket subscriptions are authenticated by default, limited (64 KiB messages, 32 subscriptions per peer), and cross-site upgrades are rejected; public channels are opt-in.
  • Plugin state is namespaced per plugin on the server, and stored in the Rust-side desktop database on the client.

SDK & tooling

  • Repository: Drop-OSS/drop-plugin-sdk (transfer pending; Heretek-Games/drop-plugin-sdk remains canonical until then).
  • SDK 0.6.0 prepares the @drop-oss/plugin-sdk / @drop-oss/plugin-cli scope alongside the published @droposs/* 0.5.x line, adds drop-plugin verify, aligns verification with the server core (manifest-covering v2 signatures, legacy entry-only bundles, ignored root files), fixes the starter template, and adds capability conformance helpers: chore(release): v0.6.0 — reconciled CLI, conformance helpers, and @drop-oss scope prep Heretek-Games/drop-plugin-sdk#20.
  • Open org-level items called out in that PR: GitHub transfer to Drop-OSS, npm drop-oss org/trusted publishing, and a compatibility alias/notice for existing @droposs consumers.

Non-breakage & upstream compatibility

Automated tests at server/server/internal/plugins/__tests__/upstream_compatibility.test.ts prove:

  • Zero-plugin invariant: with no plugins installed, Drop runs identically to baseline - zero active routes, zero WebSocket subscriptions, zero background tasks, zero overhead.
  • Graceful web degradation: webview/browser preview degrades without unhandled Tauri IPC rejections.
  • Resource lifecycle: installing, activating, and removing a plugin cleanly purges routes, WebSocket authorizers, and memory state.

Review follow-ups

  • Server PluginManager split into focused modules; registry.verifyBundle() owns signature verification; ws.get.ts is now a 10-line route over a ws-gateway module; storage no longer carries a speculative legacy migration.
  • Desktop plugin host is its own Rust crate with DB-backed plugin storage; the command blocklist is gone; the TS manager and types are split, with shared contracts in the type-only @drop/plugin-api workspace package.
  • Docs updated for the Drop-OSS SDK path, the trust model, and v2 signatures.

Verification

  • pnpm --filter drop test: 5/5 files, 46 tests (plugin runtime 30, split-module coverage 8, auth 3, v2 signatures 2, upstream invariants 3).
  • pnpm --filter drop run typecheck, pnpm --filter drop run lint:eslint (0 errors), and pnpm --filter drop run build (production Nitro bundle) all pass.
  • Desktop: pnpm test (5 tests) and pnpm run typecheck pass.
  • Rust: cargo +nightly test -p plugins --lib (8 path-guard tests), cargo +nightly check -p drop-app, and cargo +nightly test -p process pass.
  • SDK PR: 35 tests, typecheck, and lint pass; the cross-repo signature fixture matches core byte-for-byte.
  • End-to-end smoke: dev-tools/sample-plugin installs, serves GET /hello through the manager, and uninstalls leaving zero residual routes/state.

Screenshots

Screenshots (desktop Plugins settings page, capability consent dialog, game-detail slots, Play Actions menu, and the web admin plugin manager) will be attached in the morning.

plugin_subscribe emits a Tauri event named plugin:event, but subscribe()
listened with window.addEventListener and expected a DOM CustomEvent, so
plugin WebSocket messages never reached their listeners. Register a real
Tauri listener for plugin:event (payload {channel,data}) in the Tauri host
and keep the DOM CustomEvent path as the browser/dev fallback.
@BillyOutlast

Copy link
Copy Markdown
Contributor Author

Pushed 7bd4707: the desktop extension host's subscribe() listened with window.addEventListener("plugin:event", …) and expected a DOM CustomEvent, but the Rust host emits a Tauri event named plugin:event (payload {channel,data}). Tauri events are not DOM events, so subscribed plugins never received any WebSocket messages. It now registers a real Tauri listen("plugin:event", …), keeping the DOM CustomEvent path only as the browser/dev fallback and handling async unlisten safely.

One item for review, not changed here: plugin_game_check_anticheat in desktop/src-tauri/src/plugins.rs hardcodes EAC/BattlEye/Vanguard/Denuvo detection, and ScopedGameScanner.checkAntiCheat is part of the SPI. That domain knowledge belongs in the drop-gse plugin. Moving it to a generic host primitive requires a coordinated drop-plugin-sdk + drop-gse contract change (including the published SDK version), so it's tracked as a follow-up rather than partially changed here. Happy to fold it into this PR if you'd prefer it generic before merge.

plugin_game_check_anticheat hardcoded EAC/BattlEye/Vanguard/Denuvo names in
the generic client host. Replace it with plugin_game_find_files, which
returns installed paths matching caller-supplied patterns, and drop the
AntiCheatReport/checkAntiCheat SPI so that domain knowledge lives in the
plugin (e.g. drop-gse) rather than core.
@BillyOutlast

Copy link
Copy Markdown
Contributor Author

Update: the anti-cheat genericization I flagged is now folded into this PR.

Pushed 60183f0:

  • Removed plugin_game_check_anticheat and the AntiCheatReport struct from the host; added a generic plugin_game_find_files(game_id, patterns) primitive (case-insensitive relative-path matching, no symlink following).
  • ScopedGameScanner now exposes findFiles instead of checkAntiCheat.

Companion changes outside this PR:

  • @droposs/plugin-sdk drops AntiCheatReport/checkAntiCheat in favour of findFiles (bumped to 0.5.0, publish pending).
  • drop-gse owns the EAC/BattlEye/Vanguard/Denuvo pattern table and feature-detects findFiles, falling back to the legacy checkAntiCheat capability on older hosts (and failing closed if neither exists), so existing hosts keep working.

One heads-up unrelated to this change: the branch was cut before develop's fix(desktop): resolve GameStatus type errors (cb56fe5), so desktop/main still reports the pre-existing GameStatus/index-access type errors (in library/[id]/index.vue, queue.vue, etc.). None are in the plugin files touched here. It should be rebased or have cb56fe5 cherry-picked before merge; I can do that on request.

Narrow useGame's status to Ref<GameStatus> under noUncheckedIndexedAccess so
template discriminant checks narrow correctly, guard optional index access in
LibrarySearch/queue/compat/ModalStack, and type the nuxt configs with
satisfies NuxtConfig.

GameStatusButton now declares the playActions prop and play-action emit (and
renders the menu entries) so plugin play actions type-check and actually run.
@BillyOutlast

Copy link
Copy Markdown
Contributor Author

Pushed 0624294, which resolves the pre-existing desktop typecheck errors noted earlier (pnpm -C desktop/main run typecheck now reports 0 errors):

  • useGame now returns status as Ref<GameStatus> (non-optional) so the template discriminant checks (status.type === 'Installed') narrow correctly.
  • Guarded optional index access in LibrarySearch.vue, queue.vue, compat.vue, and ModalStack.vue.
  • Typed both nuxt configs with satisfies NuxtConfig to work around the hoisted @nuxt/schema type.
  • GameStatusButton now declares the playActions prop and play-action emit and renders the menu entries, so the library page's plugin play-actions type-check and actually fire.

This supersedes my earlier note about rebasing for cb56fe5 — the equivalent fixes are applied directly on this branch.

@BillyOutlast

Copy link
Copy Markdown
Contributor Author

Gentle ping for review. The branch is up to date with develop (0 behind, 4 ahead) and upstream CI hasn't run on the head yet (no checks reported), so it may need a maintainer to approve the workflow run. Happy to make any changes needed.

@DecDuck DecDuck left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Looks like a lot of it is LLM's tendency to create "god files", just don't want that in this repo
  • Some deduplication stuff

Would also like a high level overview of the features and examples of what the plugins can do. Also some screenshots.

Comment thread desktop/main/components/GameStatusButton.vue
Comment thread desktop/main/internal/plugins/ClientPluginManager.ts Outdated
Comment thread desktop/main/internal/plugins/ClientPluginManager.ts
Comment thread desktop/main/pages/settings/plugins.vue
Comment thread desktop/main/internal/plugins/types.ts
Comment thread server/server/internal/plugins/manager.ts
Comment thread server/server/internal/plugins/registry.ts
Comment thread server/server/internal/plugins/storage.ts Outdated
Comment thread server/server/internal/plugins/types.ts
Comment thread sites/docs/src/content/docs/admin/plugins.md Outdated
John Smith added 7 commits September 14, 2026 22:53
Port the upstream-ready plugin work that landed on Heretek develop after
the initial extraction: v2 bundle signatures covering the manifest,
MetadataProvider / PaymentGateway / CloudSavePathResolver SPIs, public
WebSocket channel hardening, desktop path_guard and native-command
hardening, and the associated server and desktop test suites.

Excludes fork-domain code (achievement toasts) and neutralises example
provider names in tests so the bundle stays generic.
- manager.ts is now a thin facade; routes, websocket, events, lifecycle,
  context, bundle, compat, and verification each live in their own module
- bundle checksum and signature verification moves into
  PluginRegistry.verifyBundle instead of the manager
- ws.get.ts shrinks to a thin Nitro route; peer/session transport logic
  moves to the new ws-gateway module
- FilePluginStorage drops the speculative legacy-directory migration and
  bundle hashing now ignores only drop-plugin.json
… storage

- new desktop/src-tauri/plugins crate owns the Tauri plugin commands and
  the moved path_guard module
- plugin key/value storage is persisted through the Rust database instead
  of webview localStorage, keeping one source of truth
- remove the discouraged command blocklist and keep the fail-closed
  per-plugin allowlist with explicit trust guidance
…ent host

- add a type-only workspace package holding the manifest, capability, and
  client plugin contracts so the server and desktop cannot drift
- server plugin types keep only the h3/pino runtime contract and the API
  version constants, re-exporting the shared contracts
- split the desktop ClientPluginManager into host adapters (storage,
  game fs, scanner, websocket, system) and a launch pipeline module
- document the @Drop-OSS scope effective with SDK 0.6.0 and the pending
  repository transfer
- add a prominent unsandboxed/privileged warning and fix the capability
  consent claims for the server admin flow
- document signature v2 manifest coverage and drop-plugin verify
@BillyOutlast

Copy link
Copy Markdown
Contributor Author

All review feedback is addressed on the branch (@ 58766bb), every thread has a reply with the corresponding change, and the PR body now includes the high-level feature/SPI overview plus a Play Actions section. Rebase-free update note: the branch also picked up the latest upstream/develop (#497/#489) and the generic plugin work from our development branch (v2 manifest-covering signatures, the Metadata/CloudSave/Payment SPIs, and the associated tests). Screenshots will be attached in the morning. GitHub wouldn't let me re-request review from a fork (permissions), so a manual re-review would be appreciated when you have a moment.

@BillyOutlast

Copy link
Copy Markdown
Contributor Author

Here is an example of the server side plugin installation
Screenshot 2026-09-15 170318
Screenshot 2026-09-15 164006

@BillyOutlast

Copy link
Copy Markdown
Contributor Author

@DecDuck — all review follow-ups are now pushed to the head branch (and the screenshots requested in the review are attached to the PR description). Summary of what changed since your CHANGES_REQUESTED review:

God-file decomposition

  • Server PluginManager split into focused modules (signature/bundle verification, dynamic route injection, WebSocket gateway, storage); registry.verifyBundle() owns signature verification and ws.get.ts is now a thin route over the ws-gateway module.
  • The desktop plugin host became its own Rust crate with DB-backed plugin storage; the TS manager and types were split, with shared contracts in the type-only @drop/plugin-api workspace package.

Deduplication

  • Storage no longer carries the speculative legacy-migration path; desktop/web contract types were unified; verifier logic is shared byte-for-byte with the SDK's cross-repo signature fixture test.

Docs & overview

  • The PR description now has a high-level features table (capability → SPI registration → example behavior), plugin examples, the security/trust model, and screenshots of the web admin manager, desktop Plugins settings, capability consent dialog, game-detail slots, and Play Actions.

Status: pnpm --filter drop test 46/46 (incl. the three upstream-invariant tests), typecheck/lint/build clean; desktop pnpm test and Rust cargo check/test clean; SDK PR follows with 35 tests.

One operational note: the upstream ruleset requires review-thread resolution, and we noticed the PR head SHAs have shown 0 check-runs — it would be good to confirm CI is actually running on this PR (both contributors see no checks).

Happy to resolve any threads you confirm as addressed — requesting re-review.

@DecDuck DecDuck left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks better after being broken up, but will have to go over the server code more carefully later.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this actually render the error message anywhere? I don't think the :title attribute on a div does anything.

actions advertises that extra capabilities (multiplayer, mods, ...)
require installing an extension.
-->
<button

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we want this here, I'd prefer keeping it clean without plugins installed

:game-id="game.id"
/>

<GameSetupModal

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this game setup modal? Was this moved from somewhere?

v-model="dependencyRequiredModal"
/>

<ModalTemplate v-model="extensionsPromptOpen">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the extension prompt we don't need this model either.


let app = builder
.plugin(tauri_plugin_deep_link::init())
.manage(PluginCommandAllowlist::default())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you have multiple .manage calls on a single app? I thought it had to be only one

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think page and the one on the client need to be way more explicit about the lack of sandboxing for plugins. Maybe a separate confirmation modal and/or warning banner.

Maybe also make the client users confirm that they want to use the plugins that the server has installed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to implement the company lookup?

…racts

- bump PLUGIN_API_VERSION to 3 and support versions 1, 2, and 3
- add PluginSettingsSchema and settings snapshot to server and client contexts
- add 'pre-launch:network-post' stage to LaunchStage and preLaunchStages pipeline
- expand ServerCapability (auth:provider, storage:depot) and ClientCapability (game:runner)
- declare RunnerProvider, AuthProvider, and DepotStorageProvider SPI types in @drop/plugin-api
- add upstream compatibility unit tests verifying SDK v0.7.0 contracts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants