Backend Processes
Spawn a managed, long-lived child process and talk to it over typed RPC.
Sometimes a command isn't enough — you need a persistent process: a PTY, an SSH connection pool, a native scanner, a long-running server. spawnBackend launches a managed Node/Deno/Bun process from your extension and gives you a typed RPC handle to it. This is the seam that also lets extensions ship as both a CLI and a plugin.
The two halves
Frontend — spawnBackend
import { spawnBackend } from "@kunkunsh/sdk";
interface TerminalBackend {
open(cmd: string): Promise<string>; // returns a session id
write(id: string, data: string): Promise<void>;
}
const conn = await spawnBackend<TerminalBackend>({
scriptPath: "$EXTENSION/dist/backend.js",
runtime: "node", // "auto" | "node" | "bun" | "deno"
});
const sessionId = await conn.api.open("bash");
await conn.api.write(sessionId, "ls\n");
await conn.destroy(); // kill the processspawnBackend returns { api, backendId, destroy } — api is a typed proxy over kkrpc.
Backend — exposeBackend
// src/backend.ts (bundled to dist/backend.js)
import { exposeBackend } from "@kunkunsh/sdk/backend";
import { spawn } from "node:child_process";
const facade: TerminalBackend = {
async open(cmd) { /* start a pty, return id */ return "s1"; },
async write(id, data) { /* write to the pty */ },
};
exposeBackend<TerminalBackend>(facade);You can also pass a localAPI to spawnBackend so the backend can call back into the frontend (bidirectional kkrpc).
Manifest permission
Declare exactly which script may be spawned, and its runtime:
{
"permissions": [
{ "permission": "backend", "allow": [{ "script": "$EXTENSION/dist/backend.js", "runtime": "node" }] }
]
}Capabilities
Extra process powers are declared per backend scope under capabilities. Every one follows the same shape — omit = none, true/"all" = blanket, array = scoped list:
{
"permission": "backend",
"allow": [{
"script": "$EXTENSION/dist/backend.js",
"runtime": "auto",
"capabilities": {
"subprocess": "all", // or ["ps", "sysctl"]
"env": true, // or ["HTTP_PROXY"]
"sys": ["loadavg", "cpus", "systemMemoryInfo"],
"nativeAddons": ["node-pty/**/*.node"],
"ffi": ["native/libscanner.dylib"]
}
}]
}| Capability | Form | What it grants |
|---|---|---|
subprocess | "all" | string[] | Child processes. A program list scopes Deno's --allow-run; Node can't narrow it (list ⇒ Deno-only). |
env | true | string[] | Env access. true enumerates the (host-curated, secret-free) env; a list also copies those keys through from the host env. Needed by deps that spread process.env. |
sys | true | string[] | OS introspection (Deno --allow-sys kinds: loadavg, cpus, systemMemoryInfo, networkInterfaces, hostname, osUptime, …). |
workers | boolean | Worker threads. |
nativeAddons | string[] (globs) | Packaged .node artifacts the backend loads. |
ffi | string[] | Native libraries loaded through Deno.dlopen(). |
unsandboxed | boolean | Escape hatch — no process sandbox. Last resort. |
Runtimes & sandboxing
A runtime is eligible only if it can enforce every declared capability at least as narrowly as declared; otherwise it's rejected and auto moves to the next candidate.
| Runtime | Notes |
|---|---|
| Deno | Best sandbox — scoped --allow-read/write, per-domain --allow-net, --allow-run/--allow-env/--allow-sys (blanket or scoped). Preferred by auto. |
| Node v25+ | Node permission model. Blanket env: true/sys: true are implicitly satisfied (Node never gates them); scoped env/sys/subprocess, domain-scoped net, and FFI are rejected unless capabilities.unsandboxed. |
| Bun | No sandbox — allowed only with capabilities.unsandboxed. |
"auto" picks Deno → Node. Declaring a scoped sys/env/subprocess list therefore steers auto to Deno, since Node can't enforce the narrower grant. The process sandbox applies your manifest scopes as runtime flags.
Native addons don't inline
A .node addon can't be bundled into a JS file. Package the addon's loader + platform .node files alongside dist/backend.js, and allow them via capabilities.nativeAddons. Or lazy-load the native feature so the extension still works without it.
Building the backend
Bundle the backend entry as a standalone Node/Deno script:
await Bun.build({
entrypoints: ["./src/backend.ts"],
outdir: "./dist",
target: "node", format: "esm",
// keep native addon packages external and copy their files into dist/
});Real examples
wterm-terminal-demo— a custom view backed by a Node PTY overspawnBackendkkterminal/space-lens— production backends shared with a CLI; see CLI + Plugin