fez
Extension API

GUI surface

Panels, cards, views, and themes in the desktop app.

Fez renders standard settings, themes and dialogs from data. Custom extension UI runs in a native child webview inside the app. Select the runtime in the installed package's fez.guiRuntime; fez.parts.gui points to its JSON or JavaScript file.

RuntimeGUI partHost behavior
declarativeJSONFez renders validated settings and theme palettes
isolated-settingsIIFE bundleOne custom settings panel in a child webview
isolated-pageIIFE bundle + manifest contributionsA document view with scoped saves and comments
isolatedIIFE bundle + manifest contributionsCustom navigation, settings, thread, message and profile views in child webviews

An absent runtime retains the legacy shared-webview loader. Unknown declarations fail visibly; isolated parts never fall back to that loader. Use a desktop version supporting the declaration: older releases may ignore it.

Start with the section for your interface:

The contract below describes current source. Test a packaged desktop build and set fez.minFezVersion to the oldest version you verified before publishing.

For a custom bundle, the child supplies its own React instance. Use h() calls or JSX with a bound factory:

import type { GuiExtensionApi } from "@fezchat/extension-api/gui";

let h: GuiExtensionApi["React"]["createElement"];

export default function activate(api: GuiExtensionApi) {
  h = api.React.createElement;
  api.registerMessageDecorator(
    (content) => content.startsWith("📊 poll: "),
    ({ content, msgId, channelId }) => h(PollCard, { content, msgId, channelId }),
  );
}

Build with esbuild --format=iife --global-name=__fezExt --platform=browser --jsx=transform --jsx-factory=h. api.React carries createElement plus useState, useEffect, useCallback, useRef, useMemo.

Registration seams

These are the broader GUI API's registration seams. Each isolated runtime supports only its documented subset; declaring a runtime does not grant all methods in GuiExtensionApi.

MethodWhat it addsPermission
registerSettingsPanel(name, render, opts?)a card in Settings → Extensions. The panel is filed under your extension's name regardless of the name argument — one extension can't impersonate another. opts.source links it to a channel source so the rail offers a settings buttonui
registerAgentProfileSection(label, render)extra facts in agent reputation views. render({ pubkey, persona? }, host?) supports elements or mount/dispose. persona is present only for a local agent; never use a foreign display name to select a local account. Sections show their extension source and disappear on unloadui
registerGuiCommand(name, run)a composer slash command; return "" to emit nothingcommands
registerMessageDecorator(match, render)a card mounted under matching message bodies (how the poll card enters)ui
registerThreadView(name, match, render)a lens on a whole thread, keyed on the root message's content; rendered above the replies — an index of the conversation, not a substituteui
fez.modelProvider (manifest, not a method)a model in Agents → model, served by your own bin through the processes broker; the only route from an isolated runtime — see Model providersui + processes
registerPageView(name, match, render)own a whole document. match returning true adds a toggle; "default" opens your view instead of markdown. Markdown stays one click away — the document is the truth, a view is a lensui
registerBlockRenderer(lang, render, menu?)own a fenced code block by language tag. Pass menu (label + insert template) or your block type stays undiscoverableui
watchAgent(name)open the live activity pane for an agentread:agents
openThread(channelId, rootId) / openUrl(url)navigationui
clienta restricted client adapter; absent without the grantread:channels
notify / toasthost notifications / in-app feedback; absent without their grantsnotifications / ui
personaslist/read/update/create personas and roster their keys — the power to reprogram every agent on the machinepersonas (sensitive)
secretsset(key, value) and has(key)write-only by design. A panel can store a token it just obtained and check one exists; it can never read it back. The value lands in the OS keychain and is read host-sideui

Host-only (no published types yet): registerTheme (a CSS-variable pack, light + dark), registerMarkdownPlugin (a remark plugin, runs after gfm), registerArtifactViewer (render a typed kind-40300 artifact), parseQuery.

Client contract

Check api.client before using it. Its read methods return snapshots: changing a returned map or message cannot change the host's cache. The adapter exposes an explicit set of methods, not the raw FezClient or its wire. New core methods do not automatically become extension capabilities.

OperationAdditional grant
ensureChannel, sendChannelMessage, toggleReaction, publishDoc, publishDocComment, publishArtifact, saveExtensionConfigpublish
httpAuthHeadersign or publish, matching the headless signing rule
decryptFromsign or read:dms
extensionConfigsign
agents, workingAgents, runQuery with source runsread:agents

Denied synchronous calls throw; denied asynchronous calls reject. Errors name the extension, operation, and missing grant. Handle these errors in buttons and effects. An empty result does not stand in for denied access. on supports channelsChanged and paymentReceipt; other events are rejected.

Existing installations keep their recorded grants. If an extension adds a required permission, reinstall it to review and grant that request.

Page and block props

registerPageView's render receives { content, save, comment, title, channelId, slug?, versionId?, editable }. save(next) is the whole write path — it publishes a new version; your view never learns whether it's a wiki page or a channel doc. comment(text, anchor, mentions) anchors a thread to a line — mention an agent in it and the agent answers inside the document. editable is false when an old version is on screen or the extension lacks publish. Both callbacks reject without publish, even if a view ignores editable.

Block renderers receive { info, body, raw, channelId, slug? }info is everything after the language tag on the fence (agent=researcher refresh=daily), and raw is the verbatim block, the anchor for comments.

Network access

Settings and page children block direct fetch, WebSocket and media requests through CSP. Use api.fetch for their bounded native HTTPS transport and api.openUrl for browser links. These require recorded hostname grants; exact hosts, .example.com subdomains and * are recognized. network:relay is not expanded by this transport.

Custom isolated children additionally allow direct HTTPS/WSS connections, images and media to their recorded network: hosts. A leading dot covers the domain and its subdomains; network:relay permits the configured relay origins and their corresponding HTTP(S) origins. The native host constructs this CSP for each child. A changed grant closes the custom view on its next broker check, including the periodic snapshot check; reopen it to use the new grants.

This isolates native authority and browser storage, but does not contain all network traffic: the macOS probe found that WebRTC can still send STUN traffic. Separate webviews also do not guarantee separate OS processes. Legacy GUI parts still share the main webview; their shadowed network APIs are only checks on supplied APIs, not a security boundary.

The processes grant remains broad authority to run the package's own executables. The native broker binds an isolated caller to its installed package and rejects other packages' binaries; it does not sandbox that executable's filesystem, keychain, network or spending authority. Wallet's existing setup intentionally returns a newly created wallet mnemonic to its view for backup. Its own CLI can also export remote hotkeys. These capabilities are distinct from Fez's native custody of the Nostr identity key.

Declarative settings and themes

Declare "guiRuntime": "declarative" and "parts": { "gui": "dist/gui.json" }. Fez validates the entire bounded JSON payload before registering anything; extension JavaScript and CSS are not evaluated in the main window.

The JSON can contain settings and themes:

  • A process section declares an owned binary, bounded status arguments, labeled phases, and fixed run/spawn actions. It requires ui and processes. Browser uses it for setup, progress and testing.
  • An agent-select section declares an own preference key, options, stable identity-based defaults and optional HTTPS preview URLs. It requires ui and read:agents; previews also require their recorded network grant. ElevenLabs uses it for voice selection and previews, preserving overrides.
  • themes maps names to light and dark palettes. Only known Fez tokens and validated color values are accepted; fonts and layout remain fixed. The Themes package supplies these palettes for Appearance settings.

The source schemas are declarative-gui.ts and the shipped Browser, ElevenLabs and Themes JSON files. Current ElevenLabs permissions include read:agents, network:api.elevenlabs.io, network:storage.googleapis.com, network:relay, publish, read:channels and ui. Its older installed manifest and recorded grants may lack read:agents; updating a manifest alone does not grant it.

Model providers (manifest)

An extension that runs a model can add it to Agents → edit agent → model without any code in the main page — which is what makes this work from the isolated runtimes. Declare the provider in package.json and ship the bin that answers for it:

{
  "fez": {
    "type": "extension",
    "parts": { "gui": "dist/gui.js" },
    "guiRuntime": "isolated",
    "permissions": ["ui", "processes"],
    "bin": { "fez-mesh": "dist/cli.mjs" },
    "modelProvider": {
      "id": "mini",
      "label": "Shared Models",
      "bin": "fez-mesh",
      "list": ["models", "--json"],
      "prepare": ["prepare", "--name", "{persona}", "--model", "{model}"]
    }
  }
}

The desktop registers it as ext-<extension>-<id> (ext-mesh-mini above) and drives it through the processes broker, so the same processes grant and bin claim that gate api.processes.run gate this. Both ui and processes must be recorded grants; a missing grant or a malformed declaration shows in the extension's status and does not stop its GUI part from loading.

  • list runs the bin with those args and must print a JSON array of { id, label, status, detail? } with status one of ready, offline, busy. A nonzero exit reports stderr in the picker.
  • prepare runs when the user saves that model on an agent, with {persona} and {model} substituted into the args (both must appear at least once). Exit nonzero with the reason on stderr to refuse — the message is shown as the save error. Exit zero once the agent may use the model.

api.registerModelProvider remains for parts on the legacy host, but a manifest declaration works in every runtime and needs no host-page code.

Isolated settings runner (macOS)

Compatible settings-only GUI parts select a separate native webview in their package manifest:

{
  "fez": {
    "type": "extension",
    "parts": { "gui": "dist/gui.js" },
    "guiRuntime": "isolated-settings",
    "permissions": ["ui"]
  }
}

GitHub and Slack use this runtime. Install or relink the updated package so its installed manifest includes the declaration, then open its settings normally. The panel opens inline. The main loader registers its embedded host without evaluating the extension bundle or injecting its CSS. The separate entry supports both legacy React elements and the mount/dispose callback; mounting React alone in the main page does not provide this boundary.

Each declared extension gets its own child webview, native session and preference namespace inside the main window. It resizes with Settings and closes when its page is left; host dialogs temporarily hide it. Main-page reloads revoke and close all child panels. All require a recorded ui grant. An absent guiRuntime keeps the legacy GUI host. Unknown or malformed runtime declarations fail visibly without evaluating the bundle or injecting its CSS. A declared isolated part never falls back to execution in the main page if loading fails, including on platforms where the isolated runner is unsupported.

Use a desktop build that supports this field: older releases ignore unknown manifest fields. The declaration does not retrofit isolation into an older host. The previous VITE_FEZ_ISOLATED_PANEL selector has been removed.

Build compatible settings parts against the explicit, type-only contract:

import type { IsolatedPanelApi } from "@fezchat/extension-api/gui";

export default function activate(api: IsolatedPanelApi) {
  api.registerSettingsPanel("My settings", (host) => {
    if (!host) throw new Error("Mount node required");
    host.textContent = "Settings are ready";
    return () => { host.replaceChildren(); };
  });
}

IsolatedPanelApi contains local React, one settings registration, preferences, write-only secrets, browser links, HTTPS fetch, host details/confirmation dialogs, and a limited optional client. Rust binds each request to the actual webview, checks current recorded grants, and refuses general native commands. Preferences stay in the installed extension's own file and preserve other state; corrupt files fail closed. The published contract and GitHub's mirror are compiled against the real host.

The client requires read:channels. Its agents() method is an initial snapshot requiring read:agents. Config reads require sign; config writes also require publish. Config operations reuse FezClient in the main window, including its existing encryption and relay publication. Extensions receive neither the raw client nor arbitrary signing/decryption operations.

client.listChannels() returns current active channels as copied records (id, name, optional source and meta). Store the chosen ID as an integration destination; channel names can change or collide. client.createChannel(name) creates an ordinary channel and returns its new ID. It requires publish and workspace ownership, and never adopts or retags an existing channel with the same name. Both methods also work in the legacy GUI host.

Config and secret namespaces use the installed directory with fez- added when absent: both github and a locally linked fez-github use fez-github. If both aliases are installed, config and secret access fail until the duplicate is removed. An extension cannot request another namespace. Secrets expose only set and has, use the existing keychain slots, and require ui. A keychain access error is reported rather than interpreted as disconnected.

GitHub settings

Update the GitHub package and approve its declared permissions, including network:github.com and network:api.github.com. Existing recorded grants are not silently expanded by the runner. Its installed manifest selects "guiRuntime": "isolated-settings"; both github and the locally linked fez-github directory work without a launch flag. The GitHub panel keeps device login, repository discovery, watch/triage settings and GitHub browser links. Each repository attaches to a selected ordinary channel, or the user can create one. Existing watches migrate using their exact repository metadata and retain their history. GitHub and ElevenLabs omit settingsSource, so their panels appear in global Settings. Extensions that need a dedicated source group can still declare settingsSource in their manifest; isolated registration options do not override that metadata.

Use api.fetch explicitly, including when configuring HTTP libraries. The native transport permits UTF-8 GET/HEAD/POST requests only, over HTTPS port 443 with a matching recorded network: hostname grant. It rejects URL credentials, non-public resolved addresses, cookies and privileged headers, redirects, bodies above 64 KiB, and responses above 2 MiB. Browser links use the same HTTPS/hostname grant check. network:relay is not expanded by this transport. There are at most 16 in-flight requests per panel; host replies time out after 30 seconds. HTTP uses a 20-second transport timeout; system DNS resolution can outlast that timeout.

Main-window operations are delivered by a channel carrying request IDs only; a main-only command returns the authorized operation. Large Tauri channel messages are delivered directly to their bound webview, avoiding Tauri 2's unscoped fetch queue. Replies are withheld after revocation or close. Operations already dispatched may finish; closing a panel does not roll back a write.

This is native privilege isolation, not a complete extension sandbox. CSP blocks direct fetch, WebSocket and media requests in this settings runner; the broker's HTTPS transport is separate. Declarative ElevenLabs previews use Fez's own media control. This runner currently refuses non-macOS hosts: Tauri's large invoke-response path needs separate hardening there. Installed executable parts and other GUI extensions retain their existing execution model. Agent snapshots refresh when reopened; concurrent CLI and GUI preference writes still need a shared cross-process lock. Use the custom runtime below for its supported process and live-view capabilities. Isolation remains selected per installed manifest.

The manual native probe uses temporary state and simulated credentials; it never starts agents or accesses the user's keychain. Build the desktop, then run cargo run --example isolated-panel-probe --features tauri/custom-protocol from the desktop's src-tauri directory. It checks the real bundled webview, config and secret routing, large JSON/binary channel delivery and privilege denials. Its success marker is WKWebView probe: PASS. GitHub's browser test loads the real bundle with simulated GitHub/keychain responses; it does not connect the user's account. The regular eval gate checks native authorization.

Isolated document views (macOS)

Kanban uses the same child-webview boundary for its whole document view. Declare "guiRuntime": "isolated-page" and a guiContributions.page in the installed manifest:

{
  "guiRuntime": "isolated-page",
  "guiContributions": {
    "page": {
      "name": "▦ board",
      "match": { "fence": "fez:board", "checklistSections": 2 }
    }
  }
}

The matching fence selects the view by default; checklistSections optionally adds a toggle for documents with that many headings and a task list outside code fences. The host validates these bounded declarations before registering anything. Optional messages provide text-only summaries selected by line prefixes, with the original message in a disclosure. Optional blocks declare fence disclosures and insert-menu templates. These features never execute extension JavaScript or CSS in the main webview.

Build the GUI against IsolatedPageApi from @fezchat/extension-api/gui, then call registerPageView once with the declared name. Render a React element; the child loads the bundle and its companion gui.css. The host owns matching. Use the supplied page's save(next) and comment(text, anchor, mentions); the broker accepts no channel or document target. Both callbacks require a current editable version and a live publish grant, checked again after asynchronous version reads and immediately before publication. Closed panels, stale versions and revoked grants reject. An already published write is not rolled back by closing its panel.

Page reads require ui and read:channels. Updates refresh the snapshot without remounting the page, so open forms retain their drafts. Agent discovery also refreshes with read:agents; unavailable agents must not prevent reading cards. pkByName returns undefined when unavailable, while agents() rejects access without its grant. Review schedules use the existing scoped config API. The runtime's macOS and network limitations above still apply, and executable extension parts keep their existing execution model.

Build Kanban and the desktop, then run cargo run --example isolated-page-probe --features tauri/custom-protocol from src-tauri. This uses temporary fixture documents to verify the real macOS view, card details, scoped saves/comments, stale-write rejection and native privilege denials without touching a user's board or agents.

Custom isolated views (packaged macOS builds)

fez create my-extension --gui creates an isolated React navigation view with matching manifest contributions, bundled React, and only the ui permission. Follow the dev loop to build and link it.

Use "guiRuntime": "isolated" for custom UI that needs more than the settings or document contract. The manifest supplies bounded, data-only contributions; only the child evaluates the IIFE and its styles. For example:

{
  "fez": {
    "type": "extension",
    "parts": { "gui": "dist/gui.js" },
    "guiRuntime": "isolated",
    "permissions": ["ui", "read:channels"],
    "guiContributions": {
      "settings": true,
      "messages": [{ "label": "Open poll", "match": { "linePrefix": "📊 poll: " } }]
    }
  }
}

Contributions can declare nav entries, channel-bound tabs and summaries, threads, messages, profiles, and one settings panel. Match rules use bounded text or base58-token criteria, never executable callbacks or arbitrary regular expressions. Names identify navigation and thread callbacks; message and profile array positions identify the corresponding registration order in the child. See IsolatedCustomContributions in the published manifest types.

Fez renders the surrounding navigation and labeled message/thread/profile launchers. A selected custom view opens inside the app with its own native session. Its original registration callbacks run only there. A copied public channel snapshot supplies the bound message's actual content, author and time, names, permitted agent information, signed receipts and the current viewer's reaction timestamps. No raw client, wire, DMs or arbitrary signing is exposed. Reaction writes require publish and can target only that opened message. Snapshots refresh without resetting mounted React component state.

The adapter also supports own storage and preferences, owned processes and agents, permitted persona operations, bounded navigation, feedback, and channelsChanged/paymentReceipt subscriptions. Unsupported capabilities fail; this is not the entire legacy GUI API. Bazaar, Mining and Wallet source manifests declare this runtime. Wallet keeps its existing author checks, expiry rules, receipt labels and backup ceremony. Its explorer links require network:taostats.io, network:basescan.org and network:sepolia.basescan.org, in addition to its existing grants.

The native custom runner currently requires a packaged macOS build. It deliberately refuses tauri dev: the development server bypasses the per-webview asset response hook that installs its network policy. Browser fixtures can exercise the adapter with mocked IPC, but do not establish the native boundary.

Host details and confirmation dialogs

Isolated settings, page and custom APIs expose showDetails and confirm:

await api.showDetails({ title: "Card details", body: card.details, context: "Backlog" });
const accepted = await api.confirm({ title: "Apply this change?", body: explanation });
if (accepted) await saveChange();

Fez opens a transparent child covering the app and renders its standard dialog with the installed extension's fixed source label. Title, body and context are plain text; the overlay never loads extension code or styles. Content scrolls within the viewport. showDetails resolves when opened. confirm waits for Confirm or Cancel; dismissal cancels. Callers should handle rejected requests without applying the proposed change. The legacy GuiExtensionApi.confirm member is optional; check it when supporting older hosts.

Closing, Escape and backdrop clicks close only the dialog and restore its parent's focus. Parent closure or reload also removes its dialogs. Native validation limits title to 4 KiB, body to 64 KiB and context to 256 bytes and checks the caller's recorded ui grant. Kanban uses showDetails for card text. Wallet awaits confirm before switching x402 payments to Base mainnet, including the effective per-call and daily limits in the prompt.

Reload behavior

Gui parts reload live when an extension is installed, updated, or removed from the gallery. Headless and relay parts need their host restarted.

On this page