@kb-labs/plugin-runtime
v1.0.0
Published
Plugin System Runtime - Context factory, sandboxed shims, and APIs
Readme
@kb-labs/plugin-runtime-v3
V3 Plugin System Runtime - Context factory, sandboxed execution, and runtime APIs.
Features
- Context Factory: Creates
PluginContextV3with all services wired - Sandbox Execution: In-process and subprocess execution modes
- Runtime Shims: Sandboxed fs, fetch, env with permission checks
- API Stability: Integration tests prevent accidental API drift
Installation
pnpm add @kb-labs/plugin-runtime-v3Usage
In-Process Execution (Dev Mode)
import { runInProcess } from '@kb-labs/plugin-runtime-v3/sandbox';
import type { PluginContextDescriptor } from '@kb-labs/plugin-contracts-v3';
const descriptor: PluginContextDescriptor = {
host: 'cli',
pluginId: '@kb-labs/my-plugin',
pluginVersion: '1.0.0',
cwd: process.cwd(),
permissions: { fs: { read: ['./'] } },
hostContext: { host: 'cli', argv: [], flags: {} },
parentRequestId: undefined,
};
const result = await runInProcess({
descriptor,
platform: platformServices,
ui: uiFacade,
handlerPath: '/path/to/handler.js',
input: { data: 'test' },
});
console.log(result.exitCode); // 0
console.log(result.meta); // Auto-injected metadataHandler Return Values (V3)
Handlers should return a CommandResult<T> with optional custom metadata:
export default {
async execute(ctx, input) {
// Your handler logic
const data = { message: 'Hello, World!' };
// Return result with optional custom metadata
return {
exitCode: 0,
result: data,
meta: {
customField: 'value',
timing: [{ checkpoint: 'start', elapsed: 0 }],
},
};
}
};Automatic Metadata Injection: The runtime automatically adds standard metadata fields:
executedAt- ISO timestamp when execution startedduration- Execution duration in millisecondspluginId- Plugin identifierpluginVersion- Plugin versioncommandId- Command identifier (if available)host- Execution host ('cli' | 'rest' | 'workflow')tenantId- Tenant identifier (if available)requestId- Request tracking ID
These fields are merged with your custom metadata automatically. If your custom metadata has the same key, your value will be overwritten by the standard field.
Subprocess Execution (Production)
import { runInSubprocess } from '@kb-labs/plugin-runtime-v3/sandbox';
const result = await runInSubprocess({
descriptor,
socketPath: '/path/to/unix.sock', // IPC socket
handlerPath: '/path/to/handler.js',
input: { data: 'test' },
timeoutMs: 30000,
});Context Structure
The PluginContextV3 provided to handlers contains:
{
// Metadata
host: 'cli' | 'rest' | 'workflow',
requestId: string,
pluginId: string,
pluginVersion: string,
cwd: string,
// Signal
signal?: AbortSignal,
// Services
ui: UIFacade, // 13 methods
platform: PlatformServices, // 7 services
runtime: RuntimeAPI, // fs, fetch, env
api: PluginAPI, // lifecycle, state, etc.
// Tracing
trace: TraceContext,
}UI Facade (13 methods)
ctx.ui.info('message');
ctx.ui.success('message');
ctx.ui.warn('message');
ctx.ui.error('message');
ctx.ui.debug('message');
ctx.ui.spinner('loading...');
ctx.ui.table(data);
ctx.ui.json(data);
ctx.ui.newline();
ctx.ui.divider();
ctx.ui.box('content');
await ctx.ui.confirm('Are you sure?');
await ctx.ui.prompt('Enter value:');Runtime API
// Filesystem (17 methods)
await ctx.runtime.fs.readFile('/path');
await ctx.runtime.fs.writeFile('/path', 'content');
await ctx.runtime.fs.exists('/path');
await ctx.runtime.fs.readdir('/path');
await ctx.runtime.fs.mkdir('/path');
await ctx.runtime.fs.rm('/path');
await ctx.runtime.fs.copy('/src', '/dest');
await ctx.runtime.fs.move('/src', '/dest');
// Network
await ctx.runtime.fetch('https://api.example.com');
// Environment
const value = ctx.runtime.env('NODE_ENV');Plugin API
// Lifecycle
ctx.api.lifecycle.onCleanup(async () => {
// Cleanup logic
});
// State (in-memory cache)
await ctx.api.state.set('key', value, ttlMs);
const cached = await ctx.api.state.get('key');Platform Services
ctx.platform.logger.info('message');
ctx.platform.llm.generate({ prompt: 'Hello' });
ctx.platform.embeddings.embed('text');
ctx.platform.vectorStore.search('query');
ctx.platform.cache.get('key');
ctx.platform.storage.read('key');
ctx.platform.analytics.track('event');Testing
Run Tests
# All tests
pnpm test
# Watch mode
pnpm test:watchContext Structure Tests
Integration tests verify the actual runtime structure of PluginContextV3:
pnpm test src/__tests__/context-structure.test.tsThese tests:
- ✅ Prevent accidental API removals
- ✅ Ensure type definitions match runtime
- ✅ Document the exact API surface
- ✅ Protect against regressions
See src/tests/README.md for details.
Exports
// Main export
import { createPluginContextV3 } from '@kb-labs/plugin-runtime-v3';
// Sandbox runners
import { runInProcess, runInSubprocess } from '@kb-labs/plugin-runtime-v3/sandbox';
// Bootstrap (for subprocess execution)
import '@kb-labs/plugin-runtime-v3/sandbox/bootstrap';Architecture
┌─────────────────────────────────────────┐
│ CLI / REST / Workflow Host │
└───────────────┬─────────────────────────┘
│
├─ Dev Mode: runInProcess()
│ ├─ createPluginContextV3()
│ ├─ Import handler dynamically
│ └─ Execute in same process
│
└─ Production: runInSubprocess()
├─ fork(bootstrap.js)
├─ IPC communication
├─ Sandboxed execution
└─ Return result via IPCBootstrap.js
The bootstrap.js file is a standalone entry point for forked subprocesses:
- Bundled dependencies: All
@kb-labs/plugin-contracts-v3code is inlined - No external imports: Works without node_modules access
- Multi-location fallback: Tries production, development, and fallback paths
See ADR-0016: Standalone Bootstrap for details.
Development
# Build
pnpm build
# Watch mode
pnpm dev
# Type check
pnpm type-check
# Run tests
pnpm testLicense
MIT
