Skip to content

Commit debf10c

Browse files
authored
Merge pull request #552 from objectstack-ai/copilot/assess-plugin-system-architecture
2 parents 6028495 + ba6ae64 commit debf10c

7 files changed

Lines changed: 1013 additions & 6 deletions

File tree

packages/spec/PLUGIN_STANDARDS.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,66 @@ To help AI understand the "intent" of a file, use a standard JSDoc header.
116116
*/
117117
export const LeadObject = ...
118118
```
119+
120+
---
121+
122+
## 5. Plugin Runtime Capabilities
123+
124+
The microkernel architecture provides the following runtime capabilities for plugins. The Zod schemas governing each capability live in `src/kernel/`.
125+
126+
### 5.1 Hot Reload (`plugin-loading.zod.ts``PluginHotReloadSchema`)
127+
128+
Hot reload supports **development, staging, and production** environments. The `environment` field controls the safety level:
129+
130+
| Environment | Behavior |
131+
| :--- | :--- |
132+
| `development` | Fast reload with file watchers, no health validation required |
133+
| `staging` | Production-like reload with validation but relaxed rollback |
134+
| `production` | Full safety: health validation, auto-rollback, connection draining |
135+
136+
Production safety features (`productionSafety`):
137+
- **Health validation** — run health checks after reload before accepting traffic
138+
- **Rollback on failure** — auto-rollback if reloaded plugin fails health check
139+
- **Connection draining** — gracefully drain active requests before reloading
140+
- **Concurrency control** — limit concurrent reloads (`maxConcurrentReloads`)
141+
- **Reload cooldown** — minimum interval between reloads of the same plugin (≥1s)
142+
143+
### 5.2 Plugin Isolation (`plugin-loading.zod.ts``PluginSandboxingSchema`)
144+
145+
Sandboxing supports configurable **scope** and **isolation level**:
146+
147+
| Scope | Description |
148+
| :--- | :--- |
149+
| `automation-only` | Sandbox automation/scripting plugins only (default) |
150+
| `untrusted-only` | Sandbox plugins below a trust threshold |
151+
| `all-plugins` | Sandbox all plugins for maximum isolation |
152+
153+
Isolation levels: `none`, `process`, `vm`, `iframe`, `web-worker`.
154+
155+
**Inter-Plugin Communication (IPC):** Isolated plugins communicate with the kernel and other plugins via configurable IPC:
156+
- Transports: `message-port`, `unix-socket`, `tcp`, `memory`
157+
- Configurable message size limit, timeout, and service ACL (`allowedServices`)
158+
159+
### 5.3 Dynamic Loading (`plugin-runtime.zod.ts`)
160+
161+
Plugins can be loaded and unloaded at runtime **without restarting the kernel**:
162+
163+
- **`DynamicLoadRequestSchema`** — Load a plugin from `npm`, `local`, `url`, `registry`, or `git` sources with optional integrity verification
164+
- **`DynamicUnloadRequestSchema`** — Graceful/forceful/drain unload with dependency awareness (`cascade`, `warn`, or `block` dependents)
165+
- **`ActivationEventSchema`** — Lazy activation triggers: `onCommand`, `onRoute`, `onObject`, `onEvent`, `onService`, `onSchedule`, `onStartup`
166+
- **`PluginDiscoveryConfigSchema`** — Runtime discovery from registries and local directories with polling and trust filtering
167+
- **`DynamicLoadingConfigSchema`** — Subsystem configuration: max dynamic plugins, default sandbox policy, allowed sources, integrity requirements
168+
169+
### 5.4 Plugin System Assessment Summary
170+
171+
| Capability | Status | Schema / Details |
172+
| :--- | :--- | :--- |
173+
| Plugin Registration || `manifest.zod.ts``objectstack.config.ts` plugin array, ordered initialization |
174+
| Lifecycle Hooks || `plugin.zod.ts``init()``start()``healthCheck()``destroy()` |
175+
| Service Registry || `service-registry.zod.ts` — 17 services across 13 plugins via `ctx.registerService()` |
176+
| Event Bus || `events.zod.ts` — Pub/sub with pattern matching |
177+
| Dependency Resolution || `plugin-loading.zod.ts` — Declared dependencies with conflict resolution |
178+
| Health Checks || `plugin-lifecycle-advanced.zod.ts` — Per-plugin health + system aggregation |
179+
| Hot Reload || `plugin-loading.zod.ts` — Dev + production-safe with rollback and draining |
180+
| Plugin Isolation || `plugin-loading.zod.ts` — Configurable scope + IPC for process boundaries |
181+
| Dynamic Loading || `plugin-runtime.zod.ts` — Runtime load/unload with activation events and discovery |

packages/spec/PROTOCOL_MAP.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,13 @@ This document serves as the **Grand Map** of the ObjectStack specification. It l
208208
| [`feature.zod.ts`](src/kernel/feature.zod.ts) | | **Feature Flags**. Toggleable system features. |
209209
| [`service-registry.zod.ts`](src/kernel/service-registry.zod.ts) | | **Service Registry**. Internal dependency injection. |
210210
| [`metadata-loader.zod.ts`](src/kernel/metadata-loader.zod.ts) | | **Loader**. Logic for loading definitions from disk/DB. |
211-
| [`plugin-loading.zod.ts`](src/kernel/plugin-loading.zod.ts) | | **Plugin Loading**. Phases of plugin initialization. |
211+
| [`plugin-loading.zod.ts`](src/kernel/plugin-loading.zod.ts) || **Plugin Loading**. Loading strategies, production-safe hot reload (`environment`, `productionSafety`), full plugin sandboxing (`scope`, `ipc`), code splitting, caching, and performance monitoring. |
212+
| [`plugin-runtime.zod.ts`](src/kernel/plugin-runtime.zod.ts) || **Dynamic Loading**. Runtime load/unload of plugins (`DynamicLoadRequest`, `DynamicUnloadRequest`), activation events, plugin discovery from registries/directories, and source resolution (npm/local/url/registry/git). |
212213
| [`plugin-versioning.zod.ts`](src/kernel/plugin-versioning.zod.ts) | | **Versioning**. Semantic versioning rules for plugins. |
213214
| [`plugin-validator.zod.ts`](src/kernel/plugin-validator.zod.ts) | | **Validation**. Integrity checks for plugins. |
214215
| [`plugin-structure.zod.ts`](src/kernel/plugin-structure.zod.ts) | | **Structure**. Zod rules for folder layout and file naming. |
215216
| [`plugin-capability.zod.ts`](src/kernel/plugin-capability.zod.ts) | | **Capabilities**. What a plugin can do. |
216217
| [`plugin-lifecycle-events.zod.ts`](src/kernel/plugin-lifecycle-events.zod.ts) | | **Lifecycle Events**. Hooks for plugin state changes. |
217-
| [`plugin-lifecycle-advanced.zod.ts`](src/kernel/plugin-lifecycle-advanced.zod.ts) | | **Advanced Lifecycle**. Deep lifecycle hooks. |
218-
| [`plugin-security-advanced.zod.ts`](src/kernel/plugin-security-advanced.zod.ts) | | **Advanced Security**. Sandboxing and isolation. |
218+
| [`plugin-lifecycle-advanced.zod.ts`](src/kernel/plugin-lifecycle-advanced.zod.ts) | | **Advanced Lifecycle**. Health monitoring, hot reload state management, graceful degradation, and update strategies. |
219+
| [`plugin-security-advanced.zod.ts`](src/kernel/plugin-security-advanced.zod.ts) | | **Advanced Security**. Permission system, sandbox configuration (V8/WASM/container/process), security scanning, and trust levels. |
219220
| [`startup-orchestrator.zod.ts`](src/kernel/startup-orchestrator.zod.ts) | | **Startup**. Boot sequence orchestration. |

packages/spec/src/kernel/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export * from './plugin-capability.zod';
88
export * from './plugin-lifecycle-advanced.zod';
99
export * from './plugin-lifecycle-events.zod';
1010
export * from './plugin-loading.zod';
11+
export * from './plugin-runtime.zod';
1112
export * from './plugin-security-advanced.zod';
1213
export * from './plugin-structure.zod';
1314
export * from './plugin-validator.zod';

packages/spec/src/kernel/plugin-loading.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,51 @@ describe('Plugin Loading Protocol', () => {
209209
expect(result.enabled).toBe(false);
210210
expect(result.strategy).toBe('full');
211211
expect(result.debounceMs).toBe(300);
212+
expect(result.environment).toBe('development');
213+
});
214+
215+
it('should accept production environment with safety config', () => {
216+
const config = {
217+
enabled: true,
218+
environment: 'production' as const,
219+
strategy: 'state-preserve' as const,
220+
productionSafety: {
221+
healthValidation: true,
222+
rollbackOnFailure: true,
223+
healthTimeout: 60000,
224+
drainConnections: true,
225+
drainTimeout: 30000,
226+
maxConcurrentReloads: 2,
227+
minReloadInterval: 10000,
228+
},
229+
};
230+
const result = PluginHotReloadSchema.parse(config);
231+
expect(result.environment).toBe('production');
232+
expect(result.productionSafety?.healthValidation).toBe(true);
233+
expect(result.productionSafety?.rollbackOnFailure).toBe(true);
234+
expect(result.productionSafety?.maxConcurrentReloads).toBe(2);
235+
});
236+
237+
it('should apply production safety defaults', () => {
238+
const config = {
239+
enabled: true,
240+
environment: 'production' as const,
241+
productionSafety: {},
242+
};
243+
const result = PluginHotReloadSchema.parse(config);
244+
expect(result.productionSafety?.healthValidation).toBe(true);
245+
expect(result.productionSafety?.rollbackOnFailure).toBe(true);
246+
expect(result.productionSafety?.drainConnections).toBe(true);
247+
expect(result.productionSafety?.maxConcurrentReloads).toBe(1);
248+
expect(result.productionSafety?.minReloadInterval).toBe(5000);
249+
});
250+
251+
it('should accept all environment values', () => {
252+
const envs = ['development', 'staging', 'production'];
253+
envs.forEach((env) => {
254+
const result = PluginHotReloadSchema.parse({ environment: env });
255+
expect(result.environment).toBe(env);
256+
});
212257
});
213258
});
214259

@@ -269,6 +314,49 @@ describe('Plugin Loading Protocol', () => {
269314
const result = PluginSandboxingSchema.parse({});
270315
expect(result.enabled).toBe(false);
271316
expect(result.isolationLevel).toBe('none');
317+
expect(result.scope).toBe('automation-only');
318+
});
319+
320+
it('should accept all isolation scope values', () => {
321+
const scopes = ['automation-only', 'untrusted-only', 'all-plugins'];
322+
scopes.forEach((scope) => {
323+
const result = PluginSandboxingSchema.parse({ scope });
324+
expect(result.scope).toBe(scope);
325+
});
326+
});
327+
328+
it('should accept full plugin isolation with IPC', () => {
329+
const config = {
330+
enabled: true,
331+
scope: 'all-plugins' as const,
332+
isolationLevel: 'process' as const,
333+
ipc: {
334+
enabled: true,
335+
transport: 'unix-socket' as const,
336+
maxMessageSize: 2097152,
337+
timeout: 15000,
338+
allowedServices: ['metadata', 'data', 'auth'],
339+
},
340+
};
341+
const result = PluginSandboxingSchema.parse(config);
342+
expect(result.scope).toBe('all-plugins');
343+
expect(result.ipc?.enabled).toBe(true);
344+
expect(result.ipc?.transport).toBe('unix-socket');
345+
expect(result.ipc?.allowedServices).toHaveLength(3);
346+
});
347+
348+
it('should apply IPC defaults', () => {
349+
const config = {
350+
enabled: true,
351+
scope: 'all-plugins' as const,
352+
isolationLevel: 'process' as const,
353+
ipc: {},
354+
};
355+
const result = PluginSandboxingSchema.parse(config);
356+
expect(result.ipc?.enabled).toBe(true);
357+
expect(result.ipc?.transport).toBe('message-port');
358+
expect(result.ipc?.maxMessageSize).toBe(1048576);
359+
expect(result.ipc?.timeout).toBe(30000);
272360
});
273361
});
274362

@@ -401,6 +489,9 @@ describe('Plugin Loading Protocol', () => {
401489
'cache-hit',
402490
'cache-miss',
403491
'hot-reload',
492+
'dynamic-load',
493+
'dynamic-unload',
494+
'dynamic-discover',
404495
];
405496

406497
types.forEach((type) => {
@@ -438,6 +529,8 @@ describe('Plugin Loading Protocol', () => {
438529
'ready',
439530
'failed',
440531
'reloading',
532+
'unloading',
533+
'unloaded',
441534
];
442535

443536
states.forEach((stateValue) => {

packages/spec/src/kernel/plugin-loading.zod.ts

Lines changed: 121 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,14 +291,26 @@ export const PluginDependencyResolutionSchema = z.object({
291291

292292
/**
293293
* Plugin Hot Reload Configuration
294-
* Enables hot module replacement for development
294+
* Enables hot module replacement for development and production environments.
295+
*
296+
* Production mode adds safety features: health validation, rollback on failure,
297+
* connection draining, and concurrency control for zero-downtime reloads.
295298
*/
296299
export const PluginHotReloadSchema = z.object({
297300
/**
298301
* Enable hot reload
299302
*/
300303
enabled: z.boolean().default(false),
301304

305+
/**
306+
* Target environment for hot reload behavior
307+
*/
308+
environment: z.enum([
309+
'development', // Fast reload with relaxed safety (file watchers, no health validation)
310+
'staging', // Production-like reload with validation but relaxed rollback
311+
'production', // Full safety: health validation, rollback, connection draining
312+
]).default('development').describe('Target environment controlling safety level'),
313+
302314
/**
303315
* Hot reload strategy
304316
*/
@@ -347,6 +359,54 @@ export const PluginHotReloadSchema = z.object({
347359
afterReload: z.string().optional().describe('Function to call after reload'),
348360
onError: z.string().optional().describe('Function to call on reload error'),
349361
}).optional(),
362+
363+
/**
364+
* Production safety configuration
365+
* Applied when environment is 'staging' or 'production'
366+
*/
367+
productionSafety: z.object({
368+
/**
369+
* Validate plugin health before completing reload
370+
*/
371+
healthValidation: z.boolean().default(true)
372+
.describe('Run health checks after reload before accepting traffic'),
373+
374+
/**
375+
* Automatically rollback to previous version on reload failure
376+
*/
377+
rollbackOnFailure: z.boolean().default(true)
378+
.describe('Auto-rollback if reloaded plugin fails health check'),
379+
380+
/**
381+
* Maximum time to wait for health validation after reload (ms)
382+
*/
383+
healthTimeout: z.number().int().min(1000).default(30000)
384+
.describe('Health check timeout after reload in ms'),
385+
386+
/**
387+
* Drain active connections before reload
388+
*/
389+
drainConnections: z.boolean().default(true)
390+
.describe('Gracefully drain active requests before reloading'),
391+
392+
/**
393+
* Maximum time to wait for connection draining (ms)
394+
*/
395+
drainTimeout: z.number().int().min(0).default(15000)
396+
.describe('Max wait time for connection draining in ms'),
397+
398+
/**
399+
* Maximum number of concurrent plugin reloads
400+
*/
401+
maxConcurrentReloads: z.number().int().min(1).default(1)
402+
.describe('Limit concurrent reloads to prevent system instability'),
403+
404+
/**
405+
* Minimum interval between reloads of the same plugin (ms)
406+
*/
407+
minReloadInterval: z.number().int().min(1000).default(5000)
408+
.describe('Cooldown period between reloads of the same plugin'),
409+
}).optional(),
350410
}).describe('Plugin hot reload configuration');
351411

352412
/**
@@ -409,14 +469,26 @@ export const PluginCachingSchema = z.object({
409469

410470
/**
411471
* Plugin Sandboxing Configuration
412-
* Security isolation for untrusted plugins
472+
* Security isolation for plugins with configurable scope.
473+
*
474+
* Supports isolation beyond automation scripts: any plugin can be sandboxed
475+
* with process-level isolation and inter-plugin communication (IPC).
413476
*/
414477
export const PluginSandboxingSchema = z.object({
415478
/**
416479
* Enable sandboxing
417480
*/
418481
enabled: z.boolean().default(false),
419482

483+
/**
484+
* Isolation scope - which plugins are subject to sandboxing
485+
*/
486+
scope: z.enum([
487+
'automation-only', // Sandbox automation/scripting plugins only (current behavior)
488+
'untrusted-only', // Sandbox plugins below a trust threshold
489+
'all-plugins', // Sandbox all plugins (maximum isolation)
490+
]).default('automation-only').describe('Which plugins are subject to isolation'),
491+
420492
/**
421493
* Sandbox isolation level
422494
*/
@@ -482,6 +554,47 @@ export const PluginSandboxingSchema = z.object({
482554
*/
483555
allowedEnvVars: z.array(z.string()).optional(),
484556
}).optional(),
557+
558+
/**
559+
* Inter-Plugin Communication (IPC) configuration
560+
* Enables isolated plugins to communicate with the kernel and other plugins
561+
*/
562+
ipc: z.object({
563+
/**
564+
* Enable IPC for sandboxed plugins
565+
*/
566+
enabled: z.boolean().default(true)
567+
.describe('Allow sandboxed plugins to communicate via IPC'),
568+
569+
/**
570+
* IPC transport mechanism
571+
*/
572+
transport: z.enum([
573+
'message-port', // MessagePort (worker threads / Web Workers)
574+
'unix-socket', // Unix domain sockets (process isolation)
575+
'tcp', // TCP sockets (container isolation)
576+
'memory', // Shared memory channel (in-process VM)
577+
]).default('message-port')
578+
.describe('IPC transport for cross-boundary communication'),
579+
580+
/**
581+
* Maximum message size in bytes
582+
*/
583+
maxMessageSize: z.number().int().min(1024).default(1048576)
584+
.describe('Maximum IPC message size in bytes (default 1MB)'),
585+
586+
/**
587+
* Message timeout in milliseconds
588+
*/
589+
timeout: z.number().int().min(100).default(30000)
590+
.describe('IPC message response timeout in ms'),
591+
592+
/**
593+
* Allowed service calls through IPC
594+
*/
595+
allowedServices: z.array(z.string()).optional()
596+
.describe('Service names the sandboxed plugin may invoke via IPC'),
597+
}).optional(),
485598
}).describe('Plugin sandboxing configuration');
486599

487600
/**
@@ -579,7 +692,7 @@ export const PluginLoadingConfigSchema = z.object({
579692
dependencyResolution: PluginDependencyResolutionSchema.optional(),
580693

581694
/**
582-
* Hot reload configuration (development only)
695+
* Hot reload configuration (development and production)
583696
*/
584697
hotReload: PluginHotReloadSchema.optional(),
585698

@@ -619,6 +732,9 @@ export const PluginLoadingEventSchema = z.object({
619732
'cache-hit',
620733
'cache-miss',
621734
'hot-reload',
735+
'dynamic-load', // Plugin loaded at runtime
736+
'dynamic-unload', // Plugin unloaded at runtime
737+
'dynamic-discover', // Plugin discovered via registry
622738
]),
623739

624740
/**
@@ -672,6 +788,8 @@ export const PluginLoadingStateSchema = z.object({
672788
'ready', // Fully initialized and ready
673789
'failed', // Failed to load or initialize
674790
'reloading', // Hot reloading in progress
791+
'unloading', // Being unloaded at runtime
792+
'unloaded', // Successfully unloaded (dynamic loading)
675793
]),
676794

677795
/**

0 commit comments

Comments
 (0)