Plugin SDK v3
Build OpenPets SDK v3 plugins with the host-provided context object, voice and delivery APIs, testing harness, and scaffolded plugin packages.
Plugin SDK v3
@open-pets/plugin-sdk (packages/sdk/) is the public, author-facing
contract for OpenPets plugins. It is a types-first package: it ships
TypeScript declarations describing the OpenPetsContext a plugin receives, plus
a deterministic, no-Electron test harness. The real behavior is injected at
runtime by the desktop host - the SDK is the shape both sides agree on.
For the platform that implements this contract (sandbox, permissions, install),
see Plugin platform. Source map: packages/sdk/src/codemap.md.
Current line: SDK 3.x, paired with plugin manifestVersion: 3. The exact
published package version is owned by packages/sdk/package.json.
How the contract is enforced
There are three copies of “the SDK” and they must stay in lockstep:
- The published types -
packages/sdk/src/index.ts. What authors program against. - The host implementation -
apps/desktop/src/plugin-sdk-bridge.tsand itsplugin-sdk-*namespace modules. What actually runs. - The test harness -
packages/sdk/src/testing.ts. A mockctxfor unit tests.
A conformance check (packages/sdk/src/check-plugin-sdk.ts) compiles/runs a
representative plugin against the harness to detect drift. Rule: a change to
the SDK touches index.ts, testing.ts, the desktop bridge, and the
conformance check together. Updating one without the others is how the
contract silently breaks.
The context object
A plugin exports a registration hook; at runtime the host calls it with a
host-backed ctx (OpenPetsContext). Capabilities are grouped into namespaces,
each gated by a permission (Plugin platform):
| Namespace | What it does | Rough permission |
|---|---|---|
ctx.pet |
Drive the default pet: speak, react | pet:* |
ctx.pets |
Spawn/target multiple pets, motion | pets:* |
ctx.ui |
Host-rendered bubbles, alerts, menu items, panels | ui:* |
ctx.schedule |
Recurring / one-shot timers | schedule |
ctx.storage |
Quota-bound persistent plugin data + subscriptions | storage |
ctx.config |
Read config + listen for changes | (config schema) |
ctx.events |
Curated host events: clicks, drag/drop, display, power, idle | events |
ctx.bus |
Inter-plugin publish/subscribe | bus |
ctx.audio |
Play plugin/user sounds | audio |
ctx.voice |
TTS + one-shot listen | voice:* |
ctx.notify |
OS-style notifications/toasts | notify |
ctx.ai |
Host-mediated AI gateway | ai |
ctx.secrets |
Encrypted plugin-scoped secrets | secrets |
ctx.auth |
Host-mediated OAuth/PKCE | auth |
ctx.net |
Declared∩approved hosts; public HTTPS + optional local HTTP via network:local; non-GET via network:write |
network, network:write, network:local |
ctx.files |
Scoped file access | files |
ctx.system |
System info / clipboard | system:*, clipboard |
ctx.assets |
Resolve declared asset refs (icons/images/sprites/sounds) | (declared assets) |
ctx.commands |
Register right-click commands | commands |
ctx.status |
Publish status text | (status surface) |
ctx.t |
Localized strings via plugin locales | - |
ctx.log |
Plugin logging | - |
The exact signatures live in packages/sdk/src/index.ts - that file is the
contract, so program against it rather than any list copied into a doc.
OpenPetsPermission in the SDK mirrors manifest validation so authors get
autocomplete for exactly the capabilities they can request.
ctx.net is the canonical network surface. Executable permissions are
manifest∩approved. Hosts match the intersection of network.hosts and approved
hosts; a bare hostname covers only the scheme default port. network:local adds
declared loopback/private HTTP endpoints alongside public HTTPS (public hosts
still use public-host checks). network:write unlocks non-GET on ctx.net only.
Legacy ctx.http.fetch stays GET-only public HTTPS and never inherits local or
write access.
ctx.voice.listen
ctx.voice.listen() is a single, host-owned push-to-talk capture. It is visibly
indicated only after microphone acquisition succeeds, never listens ambiently,
and rejects concurrent requests. The host bounds acquisition at 15 seconds and
transcription at 30 seconds, trims the returned text, and rejects empty output
with Voice transcription returned no text. The host cancellation path is used
when a plugin is stopped or OpenPets shuts down; plugin code does not receive raw
audio, credentials, or a renderer handle for the privacy surface.
Commands time out after five seconds by default. A command that deliberately
waits for user interaction, such as host-mediated OAuth, may declare a bounded
timeoutMs between one second and five minutes.
OAuth installed-app credentials
ctx.auth.oauth accepts an optional clientSecret alongside the provider,
client ID, and approved scopes. An installed-app credential may require that
secret at its token endpoint even when the host uses PKCE and a loopback
redirect. The host keeps OAuth session data, including a supplied client secret,
in encrypted plugin-scoped secret storage; plugins should not log it.
ctx.ui.delivery
ctx.ui.delivery requests a generic, host-owned, display-level delivery and
requires the dedicated ui:delivery permission. Authors provide a stable
plugin-scoped key, a courier returned by ctx.assets.sprite() for a
manifest-declared sprite, plain-text title/detail, and a near-term expiry. The
host - not plugin code - selects the cursor display, renders the delivery, queues it
with other work, and controls its visual behavior. Coordinates, HTML, URLs,
arbitrary asset paths, and animation controls are intentionally outside this
contract.
The call returns an opaque handle. A plugin may dismiss its delivery or register
one dismissal handler; the reason is click, manual, expired, or
plugin-stopped. Re-registering the same key supersedes the prior handle.
Handlers are not invoked after the plugin host has stopped. Use the stable key
to make repeated sync or reminder work idempotent, and treat dismissal as a
host lifecycle signal rather than a durable acknowledgement.
Design principles authors should know
- Describe, don’t render. You hand the host descriptors (a bubble, an alert, a HUD, a command); the host validates, lays out, and owns lifecycle. You can’t draw into a pet window directly. This is what keeps plugins safe and consistent.
- Everything is permission-gated and quota-bound. A namespace call without
the declared+approved permission is denied; storage and other namespaces have
quotas (
plugin-sdk-quotas). Design for graceful denial. - State survives restarts.
ctx.storagepersists; schedules reconcile after restart/sleep. Stateful companions (reminders, virtual pet) rely on this. - Localize by reference. Use
$t:in the manifest andctx.t(key, vars)in code; shiplocales/en.json. See Internationalization. - Declare visual assets explicitly. A delivery courier must be a declared sprite asset, never an installed-pet ID or filesystem path. Sprite-grid config is a host-rendered select presentation whose previews must refer to declared sprites; the host honors reduced-motion preferences in that picker.
The test harness - @open-pets/plugin-sdk/testing
Plugin tests import createTestHarness(register, options). It builds a
deterministic mock ctx with fake time and runs the plugin’s startup
without Electron, then exposes controls and assertions:
- Drive:
clock.advance(...),emit(event),runCommand(...),fireBubbleAction(...). - Assert on recorded effects (descriptors, not pixels): helpers like
expectSpoke,expectBubble,expectScheduled, plus recorded storage/config/network/AI/sound/panel/pet actions and recorded deliveries.
This is why official plugins can have fast, deterministic test.js suites:
they assert that a scheduled job would fire and the pet would speak, by
advancing fake time - no rendering, no flake. See plugins/official/*/test.js
for real examples and Testing and validation for
how these run in CI.
Starting a plugin
openpets plugin new <name> --template <blank|reminder|ambient|ai-chat|tamagotchi|calendar>
scaffolds a working SDK v3 package wired to this contract and the testing
harness. The templates intentionally exercise current surfaces (ctx.ui.alert,
dynamic speech, events, schedules, storage, commands, AI, assets, and a
Tamagotchi-style state loop) so authors have a real starting point rather than an
empty file. See Plugin platform for the full authoring workflow.
