qagent-desktop-extensions

Extension Entry Contract

This document defines the repository-side shape for extension entries in this library. It complements the WebUI-side loading contract documented in the main Hermes WebUI repository.

Required Files

Each extension entry should include:

extensions/<extension-id>/
  README.md
  extension.json
  manifest.json
  assets/

Optional files:

extensions/<extension-id>/
  screenshots/
  docs/
  scripts/

Manifest Shape

extension.json is the author-facing metadata source for registry, gallery, trust, capability, and lifecycle information. manifest.json is the current runtime loader manifest consumed by Hermes WebUI.

The current WebUI loader reads a manifest bundle with an extensions array. Extension entries should keep paths local to the extension directory.

{
  "extensions": [
    {
      "id": "example-extension",
      "scripts": ["assets/example-extension.js"],
      "stylesheets": ["assets/example-extension.css"]
    }
  ]
}

Use stable, lowercase extension IDs. Prefer letters, numbers, and hyphens.

Capabilities And Best Practices

Extensions run as trusted local code in the WebUI origin against the authenticated session, so they can lean on core-provided capabilities instead of re-implementing them, and they must follow the patterns that keep that trust safe. The current capability spectrum:

User settings — settings_schema (preferred over ad-hoc localStorage panels)

If an extension has user-configurable options, declare them so they render natively in Settings → Extensions → [extension] rather than building a bespoke panel:

"permissions": { "storage": { "owned": true } },
"settings_schema": [
  { "key": "enabled", "type": "boolean", "label": "Enable", "default": true },
  { "key": "mode", "type": "enum", "label": "Mode",
    "options": [ {"value":"compact","label":"Compact"}, {"value":"full","label":"Full"} ],
    "default": "compact" }
]

Use settings_schema for small user preferences and scalar configuration: toggles, mode selectors, labels, URLs, numeric limits, and simple color strings. Do not force user content or collections into settings fields. Message pins, model favorites, pinned MCP tools, custom theme collections, uploaded/avatar image blobs, and generated artifacts belong in extension-owned storage (or future sanctioned storage APIs), not in the native settings form.

When retrofitting existing entries, keep the slice reviewable: one extension per PR, preserve the legacy localStorage / owned-storage fallback when practical, softly migrate existing values instead of dropping user config, use permissions.storage.owned === true whenever settings_schema is present, and update the README trust / compatibility notes with the new storage behavior.

Skins — registerHermesSkin with a base scheme

Skin extensions call window.registerHermesSkin({ name, value, tokens, ... }). A skin tuned for one base mode must declare scheme: 'light' | 'dark' so core forces the matching base theme while the skin is active — this keeps code/chat tokens (--strong, --code-inline-bg, --pre-text, --input-bg, which are NOT on the registerHermesSkin allowlist) readable. Do not ship per-token CSS workarounds for this; use scheme. See e-ink-skin (scheme:'light') and skin-pack (scheme:'dark').

TTS engines — registerHermesTtsEngine

An extension can register a speech engine via window.registerHermesTtsEngine({ id, label, synthesize }) that appears in Settings → TTS Engine and drives both the Listen button and voice mode. See voicevox-tts. Normalize/chunk long input before synthesis where the backend has length limits.

Best practices (enforced in review + by the safety scan)

Sidecar Metadata

Extensions that depend on a local helper process declare the proxy contract and runtime ownership in extension.json. This lets WebUI report health without hardcoding extension-specific behavior and lets repository CI decide whether a runtime must carry the canonical scaffold.

Example shape:

{
  "sidecar": {
    "type": "loopback",
    "origin": "http://127.0.0.1:17787",
    "health_path": "/health",
    "proxy_auth": "legacy",
    "runtime": {
      "kind": "external",
      "repository": "https://github.com/franksong2702/hermes-webui-desktop-companion"
    }
  }
}

The sidecar object declares both the WebUI proxy contract and who owns the runtime. The runtime manifest.json repeats type, origin, health_path, and proxy_auth, but omits the library-only runtime object. See SIDECAR_CONTRACT.md before adding one.

Suggested fields:

README Shape

Each extension README should cover:

Sidecar And Native Host Notes

Some extensions may need a local process outside the browser, such as a desktop helper, native window, model bridge, or OS integration. Those entries should document:

Sidecars should bind to localhost by default and avoid public network exposure.

Post-Install Guidance

Extensions that need a local app, sidecar, or native host should include post_install so gallery UIs can tell users what to do after clicking Install. This is user-facing guidance; lifecycle remains the machine-readable source for what must start.

Example:

{
  "post_install": {
    "summary": "Install enables the WebUI bridge. In the Desktop Companion repo, run npm run start:pet to launch the desktop pet.",
    "docs_url": "https://github.com/franksong2702/hermes-webui-desktop-companion#after-gallery-install",
    "requires_local_app": true,
    "local_app_label": "Desktop Companion app"
  }
}

Suggested fields:

Compatibility Notes

Because the WebUI extension API is still evolving, extension READMEs should name the WebUI version, PR, or API surface they were tested against whenever possible.

Compatibility notes should prefer capability names over exact versions when possible. For example, say an extension needs manifest bundles and sidecar metadata rather than only naming the first release where those features worked.

Validation And Registry

Run the repo-wide validator before opening or updating an extension PR:

node scripts/validate-extensions.mjs
node scripts/scan-extension-safety.mjs

The validator scans every extensions/*/extension.json, checks required files, safe local asset paths, runtime manifest.json consistency, shipped capability names, lifecycle and permission shape, and selected permissions-vs-code drift such as WebUI API read/write disclosures.

The safety scan layers on high-risk checks for entry files before they can land: obvious secrets, symlinks or unsafe paths, blocked JavaScript execution patterns, undeclared external network literals, localStorage writes without owned-key declarations, and generated artifact hash/size consistency.

Treat the safety scan as a fast fail-closed heuristic, not as a full security proof. A green scan means the entry avoided the currently automated high-risk patterns; it does not clear adversarial JavaScript that hides behavior through split strings, aliased/computed Function or import(), XMLHttpRequest, or other semantic obfuscation. Those deeper malicious-code checks, broader browser-capability drift checks, and artifact-author provenance binding remain tracked under #8.

Generate the registry locally with:

node scripts/generate-registry.mjs --out dist/registry.json

The generated registry is the gallery/install index consumed by WebUI’s shipped Settings → Extensions flow. It includes the reviewed entry metadata plus Action-added fields such as entry_path, runtime_manifest_path, published_at, file_count, and per-file file_sha256 values.

The generator also writes deterministic per-extension zip artifacts under dist/artifacts/ and adds install-delivery fields to each registry entry:

Zip members are rooted under the extension id, for example desktop-companion/extension.json, so the core install client can extract into the extension root with a zip-slip-safe path check. The core WebUI install client still owns fetch, hash verification, extraction, installed-file tracking, rollback, and uninstall.