How Extensions Run
kkrpc, the HostAPI proxy, boot info, and the worker-view render cycle.
When you call Clipboard.readText() from an extension, nothing happens in your sandbox — the call is forwarded to the host, executed there under a permission check, and the result is returned. This page explains the machinery.
kkrpc: typed bidirectional RPC
Kunkun uses kkrpc, a type-safe RPC layer, for host↔extension communication. It replaces stringly-typed channels with compile-time-checked method names, arguments, and return types, and it works over any transport (Electron IPC, WebSocket, stdio, Worker postMessage).
Every SDK method is an async proxy call:
If the permission check fails, the promise rejects with a typed error instead of returning a value — you never silently get partial access.
The HostAPI proxy: getHostAPI()
The SDK resolves the host connection through a single global singleton:
import { getHostAPI } from "@kunkunsh/sdk/runtime";
const host = getHostAPI(); // typed KunkunHostAPI proxy; throws if not connected yetTwo design points make this robust:
- Single instance across bundles. The connection is stored on
globalThis.__kunkun_host_api__, so even if several copies of@kunkunsh/sdkend up bundled together (host + plugin), they share one host connection. - Errors survive transport. Typed errors like
DbErrorare re-instantiated on the plugin side after crossing kkrpc, soerror instanceof DbErroranderror.codestill work.
You rarely call getHostAPI() directly — the friendly wrappers (Clipboard, fs, db, showToast, …) do it for you.
Boot info: window.__kunkun__
Before your code runs, the host injects runtime metadata:
interface KunkunPluginBoot {
pluginId: string
installationId: string
extensionIdentifier: string
commandName: string
runtimeKind: "custom-view" | "worker-view" | "node-view"
apiVersion: string
mode: "development" | "production"
permissionScope: PermissionScope
// ...
}window.__kunkun__ is information, not authorization. The host doesn't trust it to grant access — it constructs your HostAPI from its own trusted state and enforces permissions there. It's safe to read for your own metadata (e.g. commandName), but you can't escalate by editing it.
Custom-view: static SPA over a custom protocol
Custom-view extensions load into an isolated BrowserWindow via the kunkun-ext:// protocol:
- Origin isolation. Each extension gets a unique origin (
kunkun-ext://{pluginId}), so Chromium enforces same-origin separation automatically. - Path safety. The protocol handler rejects symlink escapes and path traversal, and falls back to
index.htmlfor SPA routes. - Preload channel allowlist. Plugin windows use a preload that permits only the exact kkrpc channel injected for that window — the trusted app's main channel is blocked. Even if that filter were bypassed, kkrpc's sender check drops cross-window messages.
Worker-view: React in a Web Worker, host renders the UI
Worker-view (and node-view) extensions don't touch the DOM. You write React; a custom reconciler (Uniview) serializes your component tree to JSON; the host maps each node to a native Svelte component and renders it. Event handlers become ids that are called back into the worker.
This is why worker-view components import primitives from @kunkunsh/sdk/ui (Button, List, Form, …) instead of using raw HTML: those primitives are what the host knows how to render. It's also why your build must bundle to a single file with dedupeReact() — two React copies would break the reconciler.
Node-view uses the exact same UI model, but the worker is a Node.js/Deno process instead of a Web Worker, so you additionally get fs, shell, native modules, and full network access (subject to permissions).
Where each mode runs
| Mode | Runtime | DOM? | Node APIs? |
|---|---|---|---|
custom-view | BrowserWindow (kunkun-ext://) | Full | No |
worker-view | Web Worker | No | No |
node-view | Node.js / Deno process | No | Yes |
worker-headless | Web Worker | No | No |
node-headless | Node.js / Deno process | No | Yes |
See Extension Types to choose, and Permissions for how each runtime is sandboxed.