openpetswithchatandmcp/docs/plugins.md

271 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# FamiliarOS Plugin Platform (SDK v3 / SuperPlugins)
This is the reference for the current plugin platform. The forward-looking
spec that drove this implementation is `docs/superplugins.md`; this document
describes what is actually built and how to work with it.
## Architecture
```text
plugin package (manifest + entry + assets + panels)
-> plugin-service.ts install/seed/local-load, state, config
-> plugin-runtime.ts per-plugin lifecycle, declarative timers
-> plugin-js-host.ts sandboxed BrowserWindow per JS plugin + IPC dispatch table
-> plugin-sdk-preload.cjs builds the plugin-facing ctx (handles, subscriptions)
-> plugin-sdk-bridge.ts validation, permissions, quotas (the security boundary)
-> plugin-host-capabilities.ts Electron side effects (one instance, injected at startup)
bubbles -> plugin-bubble-arbiter.ts -> default-familiar-controller / plugin-familiar-registry -> familiar-window.ts
audio/tts -> familiar window renderer (WebAudio recipes, data-URL playback, speechSynthesis)
events -> plugin-events-source.ts (familiar windows, powerMonitor, screen, pollers)
familiars -> plugin-familiar-registry.ts + familiar-motion-engine.ts (spawn, moveTo, followCursor, physics, onTick)
panels -> plugin-panels.ts (+ panel-preload.cjs)
ai -> plugin-ai-gateway.ts (Anthropic / OpenAI / Ollama)
secrets -> plugin-secrets.ts (safeStorage-encrypted, per plugin)
auth -> plugin-oauth.ts (PKCE loopback, system browser)
voice -> plugin-voice.ts (TTS via renderer; one-shot STT via capture window + provider)
toast -> plugin-toast.ts; notify -> Electron Notification
settings -> plugin-platform-settings.ts (sound/AI-speech/voice/mic toggles + quiet hours + AI provider)
```
The single governing rule: **plugins describe; the host renders.** Bubbles are
descriptors validated in the bridge and rendered by `familiar-window.ts`. Plugin
HTML runs only inside the sandboxed *panel* window (`ui:panel`), never in a familiar
window.
Bridge naming note: the desktop sandbox now exposes FamiliarOS-first globals
(`window.familiarOSPanel`, `window.familiarOSCommandForm`,
`window.familiarOSPromptWindow`, `globalThis.__familiarOSSdk`,
`globalThis.__familiarOSRunCallback`) while retaining the historical
OpenPets names as compatibility aliases during the rebrand transition.
## Manifest
`familiaros.plugin.json`, validated by `apps/desktop/src/plugin-manifest.ts`.
A JSON Schema ships with the CLI (`packages/cli/schemas/familiaros.plugin.schema.json`);
manifests may carry `$schema` for editor validation.
- `manifestVersion: 1` — declarative timer plugins (legacy).
- `manifestVersion: 2` — JavaScript SDK v2 surface (legacy; keeps working unchanged).
- `manifestVersion: 3` — SDK v3. Requires `sdkVersion: "3.x.y"`. Adds:
- `assets`: `{ icons | images | svgs | sprites | sounds: { name: relativePath } }`
up to 32 entries per kind. Validated at install: path containment, format,
per-kind size caps (`pluginAssetMaxBytes`), and SVG sanitization
(`plugin-assets.ts` strips script/foreignObject/event handlers/external hrefs).
- `panels`: `{ name: relative .html path }` (max 8). Panel HTML gets a strict
CSP injected at install.
- New config field types `date`, `sound`, and `secret` (masked input; no defaults allowed).
Minimum unblock note: Control Center `sound` config fields now support importing
`.ogg`, `.mp3`, and `.wav` files through the host picker. Imported sounds are
stored as opaque `{ kind: "user-sound", id, name }` refs; raw filesystem paths
are rejected by config validation. Near-term debt remains to split
`plugin-sdk-bridge.ts` by namespace, centralize preload/host route contracts,
extract the user-sound store from host capabilities, and add parity tests for
renderer preload routes.
## Permissions (v3)
Existing: `familiar:speak`, `familiar:reaction`, `familiar:move`, `schedule`, `storage`,
`status`, `commands`, `network` (+ declarative-only `timer`).
New in v3: `familiar:interact` (bubble buttons/inputs), `familiar:pin` (pinned slot),
`familiar:animate` (custom sprites/scale), `familiar:speak:dynamic` (AI-generated speech),
`familiar:drop` (drag-and-drop onto the familiar), `familiars:read`, `familiars:manage` (spawn/close),
`audio`, `events`, `ui:toast`, `ui:panel`, `notify`, `bus`, `ai`, `secrets`,
`voice:speak`, `voice:listen`, `auth`, `files`, `system:openExternal`,
`system:metrics`, `clipboard`, `network:write`.
Flagged **sensitive** (louder consent, gated by global settings, default off):
`voice:listen`, `clipboard`, `familiar:speak:dynamic`
(`sensitivePluginPermissions` in `plugin-manifest.ts`).
Trust model: declared permissions + user approval at install + catalog review.
No signing tier (deliberate — see `docs/superplugins.md` §15).
## The SDK surface
Types: `packages/sdk/src/index.ts` (`@familiaros/plugin-sdk`, v3). The
namespaces on `ctx`: `familiars`, `familiar` (alias of `familiars.default`), `ui` (bubbles,
alert, toast, panel, dynamic menu), `audio`, `events`, `assets`, `bus`, `schedule`
(`once`/`every`/`daily`/`cron`/`at`/`list`), `storage` (now with `keys` +
`subscribe`, ~5 MB quota), `config`, `net` (`fetch` with non-GET +
`stream`), `notify`, `ai`, `secrets`, `voice`, `auth`, `files`, `system`,
`commands`, `status`, `http` (v2 GET-only alias), `log`.
Hard lines kept regardless of permissions:
- The render rule above (no raw markup into familiar windows).
- The privacy line (§3.1): no keystrokes, no screen contents, no other apps'
- `ctx.ui.alert(...)` is the must-not-miss delivery helper: it renders a sticky,
high-priority familiar bubble and can optionally request `sound`, `notify`, actions,
`dismissOn`, an `indicator`, and rich bubble content (`text`, limited
`markdown`, `icon`, `svg`, `image`, `tone`). `indicator` renders the top header
row used by familiar status messages, but is alert-owned instead of a familiar reaction: it
accepts a named host icon or a manifest-declared asset via `ctx.assets.icon(...)`
/ `ctx.assets.svg(...)` / `ctx.assets.image(...)`, plus safe `color`,
`background`/`backgroundColor`, and `borderColor` values. Raw SVG strings are
not accepted at runtime; bundle SVGs in `assets` so the host can validate and
sanitize them at install. Bubble body media (`icon`, `svg`, `image`) is
icon-only and cannot be combined with `text`/`markdown`; use `indicator` for
icon + message alerts. Alerts require `familiar:speak`; `familiar:interact` is only
needed for actions/input, `audio` only when `sound` is set, and `notify` only
when `notify` is set. The returned handle behaves like a bubble handle and adds
`acknowledge()`.
- Config schemas may use `type: "sound"` for host-managed plugin sound
preferences. The saved value is a named host sound, an opaque user sound ref,
or empty; plugins never receive raw filesystem paths.
- The privacy line (§3.1): no keystrokes, no screen contents, no other apps'
window titles, no ambient clipboard/microphone/filesystem. Clipboard read is
allowed only *inside a user-invoked command handler*; STT is one-shot
push-to-talk behind a default-off toggle; drops fire only on explicit drags.
- Network: HTTPS-only, manifest-declared + user-approved exact hosts, manual
redirects, response caps, DNS/private-IP SSRF guard (`assertPublicHost`).
Quotas live in `pluginSdkQuotas` (`plugin-sdk-bridge.ts`).
Reactions require `familiar:reaction`: `ctx.familiar.react(reaction)` preserves the built-in
reaction/status bubble, while `ctx.familiar.react(reaction, { showMessage: false })`
plays the animation without generating that default text.
## Plugin i18n
A plugin ships its translations as `locales/<locale>.json` — one file per
supported locale, the same convention as the host catalog: a flat map of dotted
keys to strings, with `{var}` interpolation. `locales/en.json` is the source and
the fallback; missing locales (or missing keys within a locale) fall back to
`en`, then to the raw key. The host packages and loads any present `locales/`;
no file is required.
Two ways to use those keys:
- **`$t:key` references in manifest static fields** — wherever the host renders
a plugin-authored string at display time: `name`, `description`, `configSchema`
labels/descriptions/option labels, command titles/descriptions, and dynamic
menu item titles. Write the value as `"$t:plugin.name"`; the host resolves it
against the plugin's catalog for the active locale (→ plugin `en` → raw key) at
display time, so labels re-render translated when the user switches language.
- **`ctx.t(key, vars?)` + `ctx.locale`** — for strings the plugin composes at
runtime (bubble / notify / status bodies with interpolation). `ctx.t` reads the
active locale live and interpolates `{var}` placeholders; `ctx.locale` is the
current locale string. Example: `ctx.t("reminder.due", { message })`.
Keep placeholders intact across locales and leave brand names untranslated. The
reference implementations include `familiaros.reminders` ("Quick Reminders") and
`familiaros.launch-buddy` ("Launch Buddy"), with the reminder flow mirrored by the
CLI `reminder` template.
## Bubbles & the arbiter
`ctx.ui.bubble(spec)` / `familiar.speak(spec)` accept a string or a descriptor
(text, limited markdown, icon/svg/image refs, tone, accent token, duration,
sticky, pin, dismissOn, priority, actions, input, hud) and return a live handle
(`update`, `dismiss`, `pin`, `unpin`, `onAction`, `onSubmit`, `onDismiss`).
A **pinned mini HUD bubble** can be rendered using the `hud` descriptor (requires `pin: true`). The `hud` property takes an `items` array (14 items), where each item has `icon` (named host icon or asset ref), `value` (0100), optional `label`, and optional `tone` ("amber", "blue", "green", "pink", "slate", "red"). When `hud` is present, it must not be combined with text, markdown, body media, or indicator. Pinned bubbles with `hud` render as a compact, polished 2x2 grid with CSS progress bars, avoiding emoji alignment issues.
`plugin-bubble-arbiter.ts` (one per familiar surface) arbitrates: priority queue,
do-not-interrupt for sticky/urgent, coalescing of identical back-to-back
messages, and a single **pinned slot** above the transient slot with
priority-aware replace semantics. Non-dynamic transient text goes through the
static content filter and stays single-line; pinned text may use a few safe lines
for compact status HUDs. `dynamic: true` content needs `familiar:speak:dynamic` plus
the global toggle and gets the relaxed screen (2,000 chars, secret redaction).
## Multi-familiar & liveness
`ctx.familiars.spawn({ petId })` opens an ephemeral window for an installed familiar
(max 4 per plugin), addressable via handles. `onTick` is the host-driven brain
loop (~10 fps, paused while hidden/dragging); `getState` gives self-perception;
`moveTo`/`followCursor`/`physics` run in `familiar-motion-engine.ts`. Spawned familiars
are torn down with their plugin.
## Host integrations
- **AI gateway** — one user-configured provider (Settings → Plugin Platform):
Anthropic, OpenAI, or Ollama. Keys are safeStorage-encrypted and never reach
plugin code. `complete` supports tools (function calling); `stream` streams
tokens. BYO-provider plugins can use `net.stream` + `secrets` instead.
- **OAuth** — `ctx.auth.oauth` runs PKCE against a loopback listener in the
system browser; tokens persist in the plugin's secrets and are returned to
the plugin. `refresh`/`signOut` manage the stored session.
- **Files** — OS dialogs only (`pick`/`save`); reads are size-capped, one-shot
handles. Dropped files (`familiar:drop`) are readable through the same accessor.
- **System** — `info()` (platform/locale/timezone/theme/version/online) is
always available; `metrics()` (aggregate CPU/mem only) needs
`system:metrics`; `openExternal` is HTTPS-only.
- **Commands** — `ctx.commands.register(...)` accepts `icon` as either one of
the host's named icon strings (for example `"info"`, `"check"`, `"timer"`) or
a manifest-declared bundled icon reference from `ctx.assets.icon(name)`. Raw SVG
strings are rejected; put custom SVG/PNG icons under `assets.icons` so the host
can validate and sanitize them before the command reaches runtime/UI state.
- **Quiet hours** are a host primitive (Settings → Plugin Platform) gating
speech audio, plugin sound, voice, and notification sound together.
## Developer workflow
```bash
# scaffold (templates: blank | reminder | ambient | ai-chat | tamagotchi | calendar)
npx @familiaros/cli plugin new "My Plugin" --template tamagotchi
# author-time validation: manifest, config schema, permissions, declared files
npx @familiaros/cli plugin validate ./my-plugin
# deterministic tests without the app (fake clock, event injection, mocks)
npm test # runs test.js against @familiaros/plugin-sdk/testing
# live with hot reload — saving a file re-snapshots and reloads just that
# plugin, preserving enabled state, storage, and approved permissions
FAMILIAROS_DEV_PLUGIN_PATHS=$(pwd)/my-plugin pnpm dev:desktop
# or for a folder of plugins (this is what pnpm dev:desktop:plugins uses)
FAMILIAROS_DEV_PLUGIN_ROOTS=$(pwd)/plugins/official pnpm dev:desktop
```
The test kit is `@familiaros/plugin-sdk/testing` (`createTestHarness`): fake
clock (`clock.advance("90m")` drives `once`/`every`/`daily`/`cron`/`at`),
curated event injection (`emit`), bubble interaction
(`fireBubbleAction`/`fireBubbleSubmit`), command runs, permission simulation
(unapproved namespaces throw), and mocks for `net`, `ai`, `secrets`, `files`,
`auth`, `voice`, and `system`. Assertions: `expectSpoke`, `expectReacted`,
`expectScheduled`, `expectBubble` (matches descriptors, not pixels),
`expectStored`, `expectNetCall`, `expectNotified`, `expectNoErrors`.
Runtime introspection: `familiaros:plugins-inspector` IPC
(`runtime.getInspectorState(id)`) returns schedules + next runs, registered
commands/menu items, active bubbles/panels, subscription counts, quota
counters, and the last error.
## Publishing
Unchanged flow: `pnpm plugins:check` validates and packages `plugins/official`
into ZIPs + catalog (dry-run); `plugins:publish` uploads. v3 ZIPs may contain
the manifest, the entry, and every declared asset/panel file — nothing else.
Catalog min/max-FamiliarOS-version metadata gates what loads where; v2 plugins
keep loading through the same runtime (the v3 context is a superset).
## Drift guards & CI
- `apps/desktop/src/check-plugin-sdk-conformance.ts` — compile-time guard that
the bridge surface and permission union match the published SDK types.
- `packages/sdk/src/check-plugin-sdk.ts` — runtime contract test of the mock
context (used by `pnpm --filter @familiaros/plugin-sdk test`).
- `apps/desktop/tests/plugin-bridge-fuzz.test.ts` — property/fuzz tests over
the bridge validators (cron, markdown, dynamic-text redaction, SVG/panel
sanitizers, private-IP guard, form values, arbiter invariants).
- `pnpm check` runs all of the above.
## Troubleshooting
- *Bubble never shows*: check the inspector for quota counters and the
arbiter state; sticky/urgent bubbles block lower-priority ones.
- *`Plugin permission is not approved`*: the manifest must declare it AND the
user must have approved it (re-load local plugins after permission changes).
- *Audio/voice silent*: check Settings → Plugin Platform toggles and quiet
hours.
- *`ai.*` throws*: configure a provider + key in Settings → Plugin Platform.
- *Sprite override not rendering*: sprites must be a horizontal strip with
square frames (frame size = image height); fps 130.
- *Renderer-visible URL schemes*: per `AGENTS.md`, any new scheme needs CSP
updates in `apps/desktop/vite.config.ts` and
`apps/desktop/src/renderer/index.html`. The v3 features deliberately reuse
`file:` (already allowed in familiar windows) and add no new schemes.