@unrdf/hooks 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/package.json +70 -0
  4. package/src/hooks/builtin-hooks.mjs +296 -0
  5. package/src/hooks/condition-cache.mjs +109 -0
  6. package/src/hooks/condition-evaluator.mjs +722 -0
  7. package/src/hooks/define-hook.mjs +211 -0
  8. package/src/hooks/effect-sandbox-worker.mjs +170 -0
  9. package/src/hooks/effect-sandbox.mjs +517 -0
  10. package/src/hooks/file-resolver.mjs +387 -0
  11. package/src/hooks/hook-chain-compiler.mjs +236 -0
  12. package/src/hooks/hook-executor-batching.mjs +277 -0
  13. package/src/hooks/hook-executor.mjs +465 -0
  14. package/src/hooks/hook-management.mjs +202 -0
  15. package/src/hooks/hook-scheduler.mjs +413 -0
  16. package/src/hooks/knowledge-hook-engine.mjs +358 -0
  17. package/src/hooks/knowledge-hook-manager.mjs +269 -0
  18. package/src/hooks/observability.mjs +531 -0
  19. package/src/hooks/policy-pack.mjs +572 -0
  20. package/src/hooks/quad-pool.mjs +249 -0
  21. package/src/hooks/quality-metrics.mjs +544 -0
  22. package/src/hooks/security/error-sanitizer.mjs +257 -0
  23. package/src/hooks/security/path-validator.mjs +194 -0
  24. package/src/hooks/security/sandbox-restrictions.mjs +331 -0
  25. package/src/hooks/telemetry.mjs +167 -0
  26. package/src/index.mjs +101 -0
  27. package/src/security/sandbox/browser-executor.mjs +220 -0
  28. package/src/security/sandbox/detector.mjs +342 -0
  29. package/src/security/sandbox/isolated-vm-executor.mjs +373 -0
  30. package/src/security/sandbox/vm2-executor.mjs +217 -0
  31. package/src/security/sandbox/worker-executor-runtime.mjs +74 -0
  32. package/src/security/sandbox/worker-executor.mjs +212 -0
  33. package/src/security/sandbox-adapter.mjs +141 -0
@@ -0,0 +1,413 @@
1
+ /**
2
+ * @file Hook Scheduler for Cron and Interval Triggers
3
+ * @module hooks/hook-scheduler
4
+ *
5
+ * @description
6
+ * Event-driven scheduler for cron/time-based hook triggers:
7
+ * - on-schedule: Cron-like scheduled execution
8
+ * - on-interval: Periodic execution at fixed intervals
9
+ * - on-idle: Execute during idle periods
10
+ * - on-startup: Execute once at system startup
11
+ */
12
+
13
+ import { z } from 'zod';
14
+
15
+ /**
16
+ * @typedef {Object} ScheduledHook
17
+ * @property {string} id - Unique scheduler entry ID
18
+ * @property {import('./define-hook.mjs').Hook} hook - The hook to execute
19
+ * @property {string} schedule - Cron expression or interval spec
20
+ * @property {number} [interval] - Interval in milliseconds
21
+ * @property {boolean} enabled - Whether the schedule is active
22
+ * @property {Date} [lastRun] - Last execution timestamp
23
+ * @property {Date} [nextRun] - Next scheduled execution
24
+ * @property {number} runCount - Total execution count
25
+ */
26
+
27
+ /**
28
+ * Schedule configuration schema
29
+ * POKA-YOKE: Interval bounds prevent CPU thrashing (RPN 168 → 0)
30
+ */
31
+ export const ScheduleConfigSchema = z.object({
32
+ id: z.string().min(1),
33
+ hookId: z.string().min(1),
34
+ type: z.enum(['cron', 'interval', 'idle', 'startup']),
35
+ expression: z.string().optional(), // Cron expression
36
+ // POKA-YOKE: Interval bounds validation (RPN 168 → 0)
37
+ // Min 10ms prevents CPU thrashing, max 24h prevents integer overflow
38
+ intervalMs: z
39
+ .number()
40
+ .positive()
41
+ .min(10, 'Interval must be at least 10ms to prevent CPU thrashing')
42
+ .max(86400000, 'Interval cannot exceed 24 hours (86400000ms)')
43
+ .optional(),
44
+ idleTimeoutMs: z
45
+ .number()
46
+ .positive()
47
+ .min(100, 'Idle timeout must be at least 100ms')
48
+ .max(3600000, 'Idle timeout cannot exceed 1 hour')
49
+ .optional(),
50
+ enabled: z.boolean().default(true),
51
+ maxRuns: z.number().positive().optional(), // Max executions
52
+ metadata: z.record(z.any()).optional(),
53
+ });
54
+
55
+ /**
56
+ * Hook Scheduler - Manages time-based hook execution
57
+ *
58
+ * @class HookScheduler
59
+ */
60
+ export class HookScheduler {
61
+ /**
62
+ * Create a new hook scheduler
63
+ *
64
+ * @param {object} options - Scheduler options
65
+ * @param {Function} options.executeHook - Hook execution function
66
+ * @param {number} options.tickInterval - Scheduler tick interval (default: 1000ms)
67
+ */
68
+ constructor(options = {}) {
69
+ /** @type {Map<string, ScheduledHook>} */
70
+ this.schedules = new Map();
71
+
72
+ /** @type {Function} */
73
+ this.executeHook = options.executeHook || (async () => {});
74
+
75
+ /** @type {number} */
76
+ this.tickInterval = options.tickInterval || 1000;
77
+
78
+ /** @type {NodeJS.Timer|null} */
79
+ this.ticker = null;
80
+
81
+ /** @type {boolean} */
82
+ this.running = false;
83
+
84
+ /** @type {Date|null} */
85
+ this.lastTick = null;
86
+
87
+ /** @type {number} */
88
+ this.idleThreshold = options.idleThreshold || 5000;
89
+
90
+ /** @type {number} */
91
+ this.idleStart = Date.now();
92
+
93
+ // Startup hooks queue
94
+ /** @type {Array<ScheduledHook>} */
95
+ this.startupQueue = [];
96
+ }
97
+
98
+ /**
99
+ * Register a scheduled hook
100
+ *
101
+ * @param {import('./define-hook.mjs').Hook} hook - Hook to schedule
102
+ * @param {object} config - Schedule configuration
103
+ * @returns {ScheduledHook} - Registered scheduled hook
104
+ */
105
+ register(hook, config) {
106
+ const validConfig = ScheduleConfigSchema.parse(config);
107
+
108
+ const scheduled = {
109
+ id: validConfig.id,
110
+ hook,
111
+ type: validConfig.type,
112
+ expression: validConfig.expression,
113
+ interval: validConfig.intervalMs,
114
+ idleTimeout: validConfig.idleTimeoutMs,
115
+ enabled: validConfig.enabled,
116
+ lastRun: null,
117
+ nextRun: this._calculateNextRun(validConfig),
118
+ runCount: 0,
119
+ maxRuns: validConfig.maxRuns,
120
+ metadata: validConfig.metadata || {},
121
+ };
122
+
123
+ this.schedules.set(validConfig.id, scheduled);
124
+
125
+ // Queue startup hooks
126
+ if (validConfig.type === 'startup') {
127
+ this.startupQueue.push(scheduled);
128
+ }
129
+
130
+ return scheduled;
131
+ }
132
+
133
+ /**
134
+ * Unregister a scheduled hook
135
+ *
136
+ * @param {string} id - Schedule ID to remove
137
+ */
138
+ unregister(id) {
139
+ this.schedules.delete(id);
140
+ }
141
+
142
+ /**
143
+ * Start the scheduler
144
+ */
145
+ start() {
146
+ if (this.running) return;
147
+
148
+ this.running = true;
149
+ this.lastTick = new Date();
150
+
151
+ // Execute startup hooks immediately
152
+ this._executeStartupHooks();
153
+
154
+ // Start periodic ticker
155
+ this.ticker = setInterval(() => this._tick(), this.tickInterval);
156
+ }
157
+
158
+ /**
159
+ * Stop the scheduler
160
+ */
161
+ stop() {
162
+ if (!this.running) return;
163
+
164
+ this.running = false;
165
+ if (this.ticker) {
166
+ clearInterval(this.ticker);
167
+ this.ticker = null;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Notify scheduler of activity (resets idle timer)
173
+ */
174
+ notifyActivity() {
175
+ this.idleStart = Date.now();
176
+ }
177
+
178
+ /**
179
+ * Get scheduler statistics
180
+ *
181
+ * @returns {object} - Scheduler stats
182
+ */
183
+ getStats() {
184
+ const schedules = Array.from(this.schedules.values());
185
+ return {
186
+ totalSchedules: schedules.length,
187
+ enabledSchedules: schedules.filter(s => s.enabled).length,
188
+ totalRuns: schedules.reduce((sum, s) => sum + s.runCount, 0),
189
+ byType: {
190
+ cron: schedules.filter(s => s.type === 'cron').length,
191
+ interval: schedules.filter(s => s.type === 'interval').length,
192
+ idle: schedules.filter(s => s.type === 'idle').length,
193
+ startup: schedules.filter(s => s.type === 'startup').length,
194
+ },
195
+ running: this.running,
196
+ idleSince: this.idleStart,
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Execute startup hooks
202
+ * @private
203
+ */
204
+ async _executeStartupHooks() {
205
+ for (const scheduled of this.startupQueue) {
206
+ if (scheduled.enabled && scheduled.runCount === 0) {
207
+ await this._executeScheduled(scheduled);
208
+ }
209
+ }
210
+ this.startupQueue = [];
211
+ }
212
+
213
+ /**
214
+ * Scheduler tick - check and execute due hooks
215
+ * @private
216
+ */
217
+ async _tick() {
218
+ const now = new Date();
219
+ const isIdle = Date.now() - this.idleStart > this.idleThreshold;
220
+
221
+ for (const scheduled of this.schedules.values()) {
222
+ if (!scheduled.enabled) continue;
223
+ if (scheduled.maxRuns && scheduled.runCount >= scheduled.maxRuns) continue;
224
+
225
+ let shouldRun = false;
226
+
227
+ switch (scheduled.type) {
228
+ case 'interval':
229
+ shouldRun = scheduled.nextRun && now >= scheduled.nextRun;
230
+ break;
231
+ case 'idle':
232
+ shouldRun =
233
+ isIdle &&
234
+ (!scheduled.lastRun ||
235
+ Date.now() - scheduled.lastRun.getTime() >
236
+ (scheduled.idleTimeout || this.idleThreshold));
237
+ break;
238
+ case 'cron':
239
+ shouldRun = scheduled.nextRun && now >= scheduled.nextRun;
240
+ break;
241
+ default:
242
+ // startup handled separately
243
+ break;
244
+ }
245
+
246
+ if (shouldRun) {
247
+ await this._executeScheduled(scheduled);
248
+ }
249
+ }
250
+
251
+ this.lastTick = now;
252
+ }
253
+
254
+ /**
255
+ * Execute a scheduled hook
256
+ * POKA-YOKE: Circuit breaker disables after 3 consecutive failures (RPN 432 → 43)
257
+ * @private
258
+ */
259
+ async _executeScheduled(scheduled) {
260
+ try {
261
+ scheduled.lastRun = new Date();
262
+ scheduled.runCount++;
263
+
264
+ // Calculate next run time
265
+ scheduled.nextRun = this._calculateNextRun(scheduled);
266
+
267
+ // Execute the hook
268
+ await this.executeHook(scheduled.hook, {
269
+ scheduledId: scheduled.id,
270
+ runCount: scheduled.runCount,
271
+ scheduledTime: scheduled.lastRun,
272
+ });
273
+
274
+ // POKA-YOKE: Reset error count on success
275
+ scheduled.errorCount = 0;
276
+ scheduled.lastError = null;
277
+ } catch (error) {
278
+ // POKA-YOKE: Track consecutive errors (RPN 432 → 43)
279
+ scheduled.lastError = error instanceof Error ? error : new Error(String(error));
280
+ scheduled.errorCount = (scheduled.errorCount || 0) + 1;
281
+
282
+ // Emit error event for observability
283
+ if (this.onError) {
284
+ this.onError({
285
+ scheduledId: scheduled.id,
286
+ hookName: scheduled.hook?.name || 'unknown',
287
+ error: scheduled.lastError,
288
+ errorCount: scheduled.errorCount,
289
+ timestamp: new Date(),
290
+ });
291
+ }
292
+
293
+ console.error(
294
+ `[POKA-YOKE] Scheduled hook "${scheduled.id}" failed (attempt ${scheduled.errorCount}/3):`,
295
+ scheduled.lastError.message
296
+ );
297
+
298
+ // POKA-YOKE: Circuit breaker - disable after 3 consecutive failures
299
+ if (scheduled.errorCount >= 3) {
300
+ scheduled.enabled = false;
301
+ console.warn(
302
+ `[POKA-YOKE] Scheduled hook "${scheduled.id}" disabled after 3 consecutive failures. ` +
303
+ `Last error: ${scheduled.lastError.message}. ` +
304
+ `Re-enable with scheduler.enable("${scheduled.id}") after fixing the issue.`
305
+ );
306
+
307
+ // Emit circuit-open event
308
+ if (this.onCircuitOpen) {
309
+ this.onCircuitOpen({
310
+ scheduledId: scheduled.id,
311
+ hookName: scheduled.hook?.name || 'unknown',
312
+ lastError: scheduled.lastError,
313
+ totalErrors: scheduled.errorCount,
314
+ timestamp: new Date(),
315
+ });
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Re-enable a disabled scheduled hook
323
+ *
324
+ * @param {string} id - Schedule ID to enable
325
+ * @returns {boolean} - True if enabled, false if not found
326
+ */
327
+ enable(id) {
328
+ const scheduled = this.schedules.get(id);
329
+ if (!scheduled) return false;
330
+
331
+ scheduled.enabled = true;
332
+ scheduled.errorCount = 0;
333
+ scheduled.lastError = null;
334
+ return true;
335
+ }
336
+
337
+ /**
338
+ * Calculate next run time for a schedule
339
+ * @private
340
+ */
341
+ _calculateNextRun(config) {
342
+ const now = new Date();
343
+
344
+ switch (config.type) {
345
+ case 'interval':
346
+ return new Date(now.getTime() + (config.intervalMs || config.interval || 60000));
347
+ case 'cron':
348
+ // Simple cron parser (supports: */n for "every n units")
349
+ return this._parseCronExpression(config.expression);
350
+ case 'idle':
351
+ case 'startup':
352
+ return null; // Event-driven, not time-driven
353
+ default:
354
+ return null;
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Simple cron expression parser
360
+ * POKA-YOKE: Strict validation instead of silent fallback (RPN 315 → 0)
361
+ * Supports intervals in the format star-slash-n (e.g., star-slash-5 = every 5 minutes)
362
+ * @private
363
+ */
364
+ _parseCronExpression(expression) {
365
+ // POKA-YOKE: Explicit null handling instead of silent fallback
366
+ if (!expression) {
367
+ throw new Error(
368
+ 'Cron expression is required for type "cron". ' +
369
+ 'Use "*/n" format for intervals (e.g., "*/5" for every 5 minutes).'
370
+ );
371
+ }
372
+
373
+ // Simple interval pattern: */n (every n minutes)
374
+ const intervalMatch = expression.match(/^\*\/(\d+)$/);
375
+ if (intervalMatch) {
376
+ const minutes = parseInt(intervalMatch[1], 10);
377
+
378
+ // POKA-YOKE: Validate interval range (RPN 315 → 0)
379
+ if (minutes < 1 || minutes > 1440) {
380
+ throw new Error(
381
+ `Invalid cron interval: */[${minutes}]. ` +
382
+ `Value must be between 1 and 1440 minutes (24 hours). ` +
383
+ `Example: "*/5" for every 5 minutes.`
384
+ );
385
+ }
386
+
387
+ return new Date(Date.now() + minutes * 60 * 1000);
388
+ }
389
+
390
+ // POKA-YOKE: Reject unrecognized patterns instead of silent fallback (RPN 315 → 0)
391
+ throw new Error(
392
+ `Invalid cron expression: "${expression}". ` +
393
+ `Supported format: "*/n" where n is minutes (1-1440). ` +
394
+ `Example: "*/5" for every 5 minutes, "*/60" for every hour.`
395
+ );
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Create a preconfigured scheduler instance
401
+ *
402
+ * @param {object} options - Scheduler options
403
+ * @returns {HookScheduler} - Scheduler instance
404
+ */
405
+ export function createHookScheduler(options = {}) {
406
+ return new HookScheduler(options);
407
+ }
408
+
409
+ export default {
410
+ HookScheduler,
411
+ createHookScheduler,
412
+ ScheduleConfigSchema,
413
+ };