- Remove unused statSync import in starter switch-sdk-scope.mjs (fixes CI lint failure) - Enforce sidecar verification in packPlugin and add integrity tests - Implement fail-closed capability proxies in MockClientPluginContext - Clean up any types, unused arguments, and @ts-ignore warnings across SDK/CLI/templates - Add pre-publish staging smoke tests and provenance flag in publish-legacy-scope.mjs - Pin GitHub Actions to commit SHAs in release workflows and bump doc/scope versions
9.8 KiB
Desktop Client Plugins Architecture & Guide
Client plugins in Drop run inside the desktop host (Tauri v2 + Vue 3). They allow community developers to extend the user interface, register alternative game launch actions, perform scoped file staging with reverse rollbacks, and execute allowlisted native binaries.
The ClientPlugin Interface
import type { ClientPlugin, ClientPluginContext } from "@drop-oss/plugin-sdk";
export default class ExampleClientPlugin implements ClientPlugin {
metadata = {
id: "example-client",
name: "Example Client Plugin",
version: "1.0.0",
apiVersion: 2,
targets: ["client" as const],
capabilities: [
"ui:slot" as const,
"ui:play-action" as const,
"ui:context-menu" as const,
"game:launch-hook" as const,
"game:fs" as const,
"game:scan" as const,
"system:command" as const,
],
};
async init(ctx: ClientPluginContext): Promise<void> {
// Register UI slots, play actions, launch hooks, and context menus
}
async teardown?(): Promise<void> {
// Cleanup handlers
}
}
1. UI Slots (ui:slot Capability)
Client plugins can inject custom Vue components into designated UI slots across the application.
Available Slots
| Slot Name | Location | Description |
|---|---|---|
"game-detail:actions" |
Game details hero | Custom action buttons alongside the Play button |
"game-detail:panels" |
Game details body | Injected cards, status views, and widgets |
"game-detail:badges" |
Game details header | Badges (e.g. "Multiplayer Ready", "Modded") |
"settings:tabs" |
Settings view | Custom plugin configuration tabs |
"topbar:status" |
Top navigation bar | Global indicators (e.g. VPN status, peer count) |
"sidebar:nav" |
Left sidebar | Additional primary navigation links |
"overlay:panel" |
In-game overlay | Dedicated overlay panel for the active title |
"overlay:quick-access" |
In-game overlay | Quick-action buttons within the in-game overlay |
Registering a Slot
ctx.registerSlot(
"game-detail:panels",
{
render() {
const h = (globalThis.window as any)?.Vue?.h;
return h(
"div",
{ class: "rounded-xl border border-zinc-800 bg-zinc-900/60 p-4" },
[
h(
"h4",
{ class: "text-xs font-semibold text-zinc-200" },
"Custom Extension",
),
h("p", { class: "text-xs text-zinc-400 mt-1" }, "Status: Active"),
],
);
},
},
{ label: "My Panel", order: 10 },
);
Important
Do not register a
template:string. Drop's production Vue build does not include the runtime template compiler (vue/dist/vue.esm-bundler.js), so a component created from a raw template string throwsComponent provided template option but runtime compilation is not supportedand is caught by the plugin error boundary. Use arender()function (returningh(...)vnodes) or a precompiled component instead.
Installed-Location Caveats for system.run
ctx.system.run executes allowlisted bare executable names only (never
paths). Resolution happens on the desktop host PATH. GUI sessions often omit
common daemon directories, so the host retries the following well-known
directories when the binary is not on PATH:
| Platform | Additional directories searched |
|---|---|
| Linux | /usr/sbin, /usr/local/sbin, /opt/ZeroTier/One |
| macOS | /Library/Application Support/ZeroTier/One, /usr/local/bin, /usr/local/sbin, /opt/homebrew/bin |
| Windows | Program Files (x86)\ZeroTier\One, Program Files\ZeroTier\One, ProgramData\ZeroTier\One |
Windows batch wrappers (e.g. zerotier-cli.bat) are never executed — batch
files require a shell and are rejected by the no-shell execution model. Plugin
authors should invoke the underlying executable directly (e.g.
zerotier-one.exe -q ...) in their manifest.client.commands.
2. Play Actions (ui:play-action Capability)
Inspired by Playnite, Drop allows plugins to register dynamic startup actions for games. When a user clicks the dropdown next to "Play", your custom action appears as an option:
ctx.registerPlayAction((gameId: string) => [
{
id: "play-lan-multiplayer",
name: "Play LAN Multiplayer",
icon: "heroicons:users",
isDefault: false,
execute: async (context: LaunchContext) => {
ctx.logger.info(`Starting LAN multiplayer for ${context.gameTitle}...`);
// Trigger game launch through Drop's pipeline
},
},
]);
3. Game Launch Pipeline & Reverse Rollbacks (game:launch-hook)
The launch pipeline enables plugins to inspect, prepare, and clean up game launches in coordinated stages.
Pipeline Stages
flowchart LR
V["1. validate"] --> P["2. prepare"]
P --> S["3. stage"]
S --> N["4. network"]
N --> L["[GAME RUNNING]"]
L --> C["5. cleanup"]
C --> R["6. restore"]
R --> Y["7. sync"]
style L fill:#9333ea,stroke:#c084fc,stroke-width:2px,color:#fff
pre-launch:validate: Integrity checks, anti-cheat scans. (Cancelable)pre-launch:prepare: Downloading staged resources or configs. (Cancelable)pre-launch:stage: Backing up and swapping executables or DLLs. (Cancelable)pre-launch:network: Connecting to virtual mesh VPNs or lobbies. (Cancelable)post-exit:cleanup: Removing transient scratch files.post-exit:restore: Restoring original backups.post-exit:sync: Cloud save synchronization.
Fail-Closed Reverse Rollback
If any pre-launch hook throws an error, the launch is immediately aborted, and all previously completed pre-launch stages are rolled back in reverse order, ensuring the user's game directory is never left in a corrupted state.
ctx.registerLaunchHook({
stage: "pre-launch:stage",
order: 10,
execute: async (launchCtx: LaunchContext) => {
// 1. Backup original DLL
await ctx.gameFs.backupFile(launchCtx.gameId, "steam_api64.dll");
// 2. Write custom wrapper
await ctx.gameFs.writeFile(
launchCtx.gameId,
"steam_api64.dll",
customBytes,
);
},
});
ctx.registerLaunchHook({
stage: "post-exit:restore",
order: 10,
execute: async (launchCtx: LaunchContext) => {
// Restore original DLL
await ctx.gameFs.restoreFile(launchCtx.gameId, "steam_api64.dll");
},
});
4. Scoped Game Filesystem (game:fs Capability)
Client plugins have safe, confined access to the active game's installation directory. The host rejects any path traversal (..) or symlink escaping the game's root directory:
// Read, write, backup, restore, delete
const exists = await ctx.gameFs.fileExists(gameId, "config.ini");
await ctx.gameFs.writeFile(gameId, "custom.ini", "setting=1");
const backupPath = await ctx.gameFs.backupFile(gameId, "steam_api64.dll");
await ctx.gameFs.restoreFile(gameId, "steam_api64.dll");
5. Executable Scanning & File Search (game:scan Capability)
// Scan all executables and compute SHA-256 hashes
const executables = await ctx.gameScanner.scanExecutables(gameId);
for (const exe of executables) {
ctx.logger.info(
`Found ${exe.relativePath} (hash: ${exe.sha256}, size: ${exe.size})`,
);
}
// Search for files by path fragment. The host has no domain knowledge, so the
// plugin owns the meaning of the patterns (e.g. anti-cheat detection).
const matches = await ctx.gameScanner.findFiles(gameId, [
"easyanticheat",
"battleye",
]);
if (matches.length > 0) {
ctx.logger.warn(`Anti-cheat indicators present: ${matches.join(", ")}`);
}
6. Allowlisted Native System Commands (system:command Capability)
Plugins that require native helper binaries (e.g. zerotier-cli) must declare them explicitly in manifest.client.commands:
{
"client": {
"capabilities": ["system:command"],
"commands": ["zerotier-cli"]
}
}
At runtime, the host executes the binary directly without a shell, preventing shell injection:
const result = await ctx.system.run("zerotier-cli", ["status"], {
timeoutMs: 5000,
});
if (result.code === 0) {
ctx.logger.info("ZeroTier status:", result.stdout);
}
Attempting to run a binary not declared in manifest.client.commands is blocked immediately.
Bundled Native Sidecars (system:sidecar Capability)
Client plugins can also bundle platform-specific native binaries (e.g. gse-engine, compatibility helpers) inside the .dropplugin archive using the client.sidecars declaration:
{
"client": {
"capabilities": ["system:command", "system:sidecar"],
"commands": ["my-tool"],
"sidecars": [
{
"name": "my-tool",
"targets": [
{
"os": "linux",
"arch": "x64",
"path": "sidecars/linux-x64/my-tool",
"sha256": "abcdef..."
},
{
"os": "windows",
"arch": "x64",
"path": "sidecars/windows-x64/my-tool.exe",
"sha256": "123456..."
}
]
}
]
}
}
When the client plugin activates, the desktop host matches the current OS and CPU architecture, verifies the SHA-256 digest, stages the binary into ~/.local/share/drop/plugins/<pluginId>/sidecars/, and routes calls to ctx.system.run("my-tool", ...) to the verified staged binary.