waybetter-for-x/apps/desktop/plugins/README.md
2026-10-01 06:01:13 +07:00

5.3 KiB

Declarative plugin SDK v1

X Workspace alpha plugins are JSON manifests. They contribute a theme, local draft templates, or both. They do not execute JavaScript, read the network or cookies, access drafts, send posts, install packages, or change X's remote page. The native observer is a separate, built-in experimental component.

Copy focus.theme.json or developer-templates.json as a starting point. If the UI exposes JSON import, it must call validateManifest(parsedJSON) and display the returned manifest's permissions before enabling contributions. Bundle loading uses the same validator. A theme import is not evidence that arbitrary plugin code is supported.

Contribution Required permission Behavior
contributions.theme theme:write Sets the local workspace's known CSS tokens.
contributions.templates drafts:templates Offers plain text templates that a user can turn into a local draft.

Permissions must match contributions exactly. Unknown fields and permissions are rejected, including script, entrypoint, url, css, and additional theme properties. Turning on a plugin is not permission to act on X. Use a namespaced template key such as ${manifest.id}/${template.id} in the host because template IDs are unique only inside their plugin.

Manifest contract

Required top-level fields are id, name, version, sdkVersion, description, permissions, and contributions.

  • id: 1–64 characters, starts with a lowercase letter, then lowercase letters and digits with single dots/hyphens between segments. No constructor, prototype, or __proto__ segments. Example: yourname.release-notes.
  • name: 1–80 characters; description: 1–500 characters. Both must be nonblank and free of control characters.
  • version: SemVer 2.0.0, at most 64 characters. Examples: 1.0.0, 0.2.0-beta.1.
  • sdkVersion: exactly 1.
  • permissions: unique entries from the table, with no unused permissions.
  • contributions: at least one supported contribution.
  • A validated manifest must fit within 128 KiB of UTF-8 JSON. A host should also limit the source file to 128 KiB before JSON parsing. Importing an archive is not supported.

Theme fields are exactly background, panel, elevated, text, muted, accent, border, radius, and density. Colors accept #RGB or #RRGGBB and normalize to lowercase six-digit hex (for example, #aBc becomes #aabbcc) for the native bridge. Expressions, URLs, CSS declarations, alpha channels, and custom properties are not supported. radius is an integer in 0–24 pixels. density is comfortable or compact.

A plugin can supply 1–24 templates. Each has exactly id, name, and text. IDs follow the plugin ID grammar and must be unique inside the plugin. Names have an 80-character limit. Text is nonblank, at most 20,000 UTF-16 code units, and can contain tabs/newlines and ordinary URLs as literal draft text. Text is never HTML and must not be passed to innerHTML.

Host integration

import { applyTheme, validateManifest } from '../src/sdk/index.ts';

const plugin = validateManifest(JSON.parse(source));
// Show the name, description, version and exact permissions to the user.
// After user activation:
if (plugin.contributions.theme) applyTheme(plugin.contributions.theme);

The validator returns a fresh data structure. Keep the validated copy, not the original input. On startup, revalidate any manifests loaded from saved settings. Revalidate again after editing imported data. Never merge manifests into application state with unrestricted recursive merging.

Host lifecycle requirements:

  1. Reject duplicate plugin IDs or offer an explicit replacement flow; do not silently overwrite.
  2. Enabling a theme applies only the known theme tokens. It does not inject a stylesheet.
  3. Creating from a template goes through the host's draft save API; imported templates cannot select account identities.
  4. Disabling or uninstalling a plugin removes its available template contributions. Existing user-created drafts remain independent.
  5. If the active theme belongs to that plugin, switch to a built-in preset when it is disabled or uninstalled.
  6. Use local preferences for manifest enablement; never copy session tokens or remote X payloads into plugin state.

applyTheme(tokens, target?) validates all tokens before its first DOM write. Without a target it uses document.documentElement. It writes --color-bg, --color-panel, --color-elevated, --color-text, --color-muted, --color-accent, --color-border, --radius, and data-density only. Presets are Graphite, Ivory, Midnight, and Forest. The built-in Focus plugin is a fifth optional theme.

Intentional alpha limits

There is no general-purpose executable plugin API, live reload protocol, permission escalation, marketplace, network API, metrics-hiding behavior, notification hook, MCP access, or sandbox. These need separate threat models and host implementation. A JSON manifest claiming one of them will fail validation.

Validation is for JSON-like data in the trusted host. It rejects accessors, exotic object/array prototypes, symbols, unknown keys, sparse arrays, and prototype-pollution keys. JavaScript Proxy objects are outside the threat model: only parse JSON from import files. Host rendering must still treat all names, descriptions and template text as untrusted plain text.