Skip to content

Commit e046ed7

Browse files
Copilothotlong
andcommitted
feat(kernel): add production hot reload, full plugin isolation, and dynamic loading protocol
- Enhanced PluginHotReloadSchema with environment modes (dev/staging/production) and production safety features (health validation, rollback, connection draining) - Enhanced PluginSandboxingSchema with isolation scope (automation-only/untrusted-only/all-plugins) and inter-plugin communication (IPC) mechanism - Created plugin-runtime.zod.ts with Dynamic Loading protocol: runtime load/unload, plugin discovery, activation events, and source resolution - Extended PluginLoadingEventSchema with dynamic-load/unload/discover events - Extended PluginLoadingStateSchema with unloading/unloaded states - Added comprehensive tests for all new schemas (324 kernel tests pass) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 3aab3e9 commit e046ed7

5 files changed

Lines changed: 946 additions & 3 deletions

File tree

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 gates)
310+
'staging', // Production-like reload with validation but relaxed rollback
311+
'production', // Full safety: health gates, 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(0).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)