Skip to content

Runtime, sandbox, and RPC

The runtime loads with the injected Fluxer application and reconciles installed plugin records into active sandbox instances. The injector does not need to remain open.

Plugin code is copied into per-user data, checked against its manifest hash, and disabled until the user confirms the exact bundle and capabilities. Importing changed code resets enabled/trust state.

Each enabled plugin gets an opaque-origin frame with scripts allowed but no same-origin privilege. The frame has no parent DOM, Node/Electron access, unrestricted networking, or remote script execution.

The SDK’s definePlugin registers a definition inside this sandbox. Identity and API compatibility are checked before startup.

SDK calls become serializable requests. Both the sandbox client and host check the required capability. Host handlers validate data and context before invoking generic services. Privileged operations repeat appropriate checks at the main-process boundary.

Credentials stay in core. Attachments expose owned opaque handles rather than arbitrary channel/message lookup; external navigation requires a host-held user-interaction authorization.

Bootstrap creates one HostServices per window and hands it to the plugin manager and the Electron host. It holds a RouteWatcher (it polls the path, since Fluxer’s router runs in the page’s own world), a GuildDataService (community info, roles and channels, fetched with the user’s credential, cached for 10 minutes and shared by every plugin and the badge host), an ActivityWatcher (passive capture listeners on Fluxer’s window; it keeps only the time of the last input and hands out focused, visible and idle flags per subscription), a VoiceWatcher (the adapter’s reading of the voice panel, observed only while a plugin listens, keeping the last known state while the panel is hidden), the SettingsPageRegistry of plugins’ settings pages, the ScopedPatcher with its typed hooks (patch-hooks.ts), the message box tracker and, on first use, the DecorationHost. Each plugin instance reaches them through PagePluginServices, which checks inputs, keeps per-plugin limits and registers everything through the instance’s PluginServices, so the 64-registration cap and cleanup apply.

A new page-level primitive usually belongs in PagePluginServices plus a host service, not in PluginServices or the RuntimeHost interface. Transports that run in the main process for one instance, images and network, live in PluginServices instead: it counts that instance’s requests, cancels them when the instance stops and forwards them over IPC.

DecorationHost follows the badge pattern: the adapter asks it what to draw at each place on screen, and it answers from a per-plugin cache, asking providers about visible places in batches and validating every entry on its own. Block rendering (decoration-render.ts) uses fluxer-style.ts classes, sets text as text only, and sends button and link clicks back through the user-action checks.

The management UI lists the registry’s pages after FluxPlugs’ own in the settings surface and, when one is selected, draws an empty area filling the content area, like Fluxer’s own scroller. The plugin’s frame can’t move into that area, because moving an iframe reloads it, so PluginServices.embed keeps the frame where it is and positions it as a fixed overlay over the area, re-aligned every animation frame and hidden while another dialog covers settings. The adapter reports when the page is left, which unembeds the frame and sends onHide. While embedded, ui.openView refuses with view_busy, and ui.theme adds --settings-page-width and --settings-page-gutter: the adapter measures Fluxer’s own content column by laying out an empty element with the column’s classes, so the page can match Fluxer’s width at any window size.

For plugins with network, main-helper.cjs handles fluxplugs:network:fetch and fluxplugs:network:cancel. It checks the capability, the code hash and the hosts the user confirmed (PluginRecord.trustedNetworkHosts), then network-resource.ts sends the request. That transport shares its DNS pinning with image loading (public-address.ts): it resolves the host to public IPv4 addresses, connects to the checked address and re-checks every redirect. The request and host rules live in packages/schemas/src/network.ts, and injector-core applies the same host rule to manifests.

Role badges follow the same split. BadgeHost gives providers a community’s role list (from the shared GuildDataService) and never member data; MemberRoleResolver looks up the roles of people on screen with the user’s credential, one request at a time within per-community budgets, and backs off on 429, 401 and 403. When lookups are off or fail, it falls back to what the page shows.

Reconciliation starts, stops, or restarts plugin instances when trust, enablement, code, or settings change. Custom views show the same plugin frame and instance.

On shutdown, cancel pending work and release dialogs, subscriptions, styles, wrappers, image handles, and other instance-owned resources. Plugin cleanup remains necessary even with core’s safety net.

Use runtime unit tests for grant enforcement, malformed requests, ownership, cleanup, cancellation, and stale asynchronous completions. Use the external-plugin fixture to prove a plugin can run using the public package boundary without private imports.