feat(plugins): add backend subprocess support via RPC bridge

Extend the plugin system so plugins can optionally declare a
entry in their manifest. The host spawns a Node.js subprocess for that
script, assigns it a random local port, and exposes an RPC proxy route
() that the sandboxed iframe can call via
postMessage — avoiding the need for the iframe to hold auth tokens.

Changes:
- Add plugin process manager to spawn/stop server subprocesses
- Wire subprocess lifecycle into enable/disable and uninstall routes
- Add RPC proxy route on the host server
- Extend PluginsContext and PluginTabContent to handle ccui:rpc and
  ccui:rpc-response postMessage events
- Add hello-world server.js as a reference subprocess implementation
- Update manifest.json and README with server field documentation
This commit is contained in:
simosmik
2026-03-05 11:58:04 +00:00
parent 6517ec4ecd
commit 2588851746
10 changed files with 590 additions and 54 deletions

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef } from 'react';
import { useTheme } from '../../contexts/ThemeContext';
import { authenticatedFetch } from '../../utils/api';
import type { Project, ProjectSession } from '../../types/app';
type PluginTabContentProps = {
@@ -82,6 +83,34 @@ export default function PluginTabContent({
);
break;
}
case 'ccui:rpc': {
// Plugin is making an RPC call to its server subprocess.
// We bridge this because the sandboxed iframe has no auth token.
const { requestId, method, path: rpcPath, body } = event.data;
if (!requestId || !rpcPath) break;
const cleanPath = String(rpcPath).replace(/^\//, '');
const url = `/api/plugins/${encodeURIComponent(pluginName)}/rpc/${cleanPath}`;
authenticatedFetch(url, {
method: method || 'GET',
...(body ? { body: JSON.stringify(body) } : {}),
})
.then(async (res) => {
const data = await res.json().catch(() => null);
iframeRef.current?.contentWindow?.postMessage(
{ type: 'ccui:rpc-response', requestId, status: res.status, data },
'*',
);
})
.catch((err) => {
iframeRef.current?.contentWindow?.postMessage(
{ type: 'ccui:rpc-response', requestId, status: 500, error: (err as Error).message },
'*',
);
});
break;
}
default:
break;
}

View File

@@ -12,8 +12,10 @@ export type Plugin = {
type: 'iframe' | 'react';
slot: 'tab';
entry: string;
server: string | null;
permissions: string[];
enabled: boolean;
serverRunning: boolean;
dirName: string;
};