@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.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/package.json +70 -0
- package/src/hooks/builtin-hooks.mjs +296 -0
- package/src/hooks/condition-cache.mjs +109 -0
- package/src/hooks/condition-evaluator.mjs +722 -0
- package/src/hooks/define-hook.mjs +211 -0
- package/src/hooks/effect-sandbox-worker.mjs +170 -0
- package/src/hooks/effect-sandbox.mjs +517 -0
- package/src/hooks/file-resolver.mjs +387 -0
- package/src/hooks/hook-chain-compiler.mjs +236 -0
- package/src/hooks/hook-executor-batching.mjs +277 -0
- package/src/hooks/hook-executor.mjs +465 -0
- package/src/hooks/hook-management.mjs +202 -0
- package/src/hooks/hook-scheduler.mjs +413 -0
- package/src/hooks/knowledge-hook-engine.mjs +358 -0
- package/src/hooks/knowledge-hook-manager.mjs +269 -0
- package/src/hooks/observability.mjs +531 -0
- package/src/hooks/policy-pack.mjs +572 -0
- package/src/hooks/quad-pool.mjs +249 -0
- package/src/hooks/quality-metrics.mjs +544 -0
- package/src/hooks/security/error-sanitizer.mjs +257 -0
- package/src/hooks/security/path-validator.mjs +194 -0
- package/src/hooks/security/sandbox-restrictions.mjs +331 -0
- package/src/hooks/telemetry.mjs +167 -0
- package/src/index.mjs +101 -0
- package/src/security/sandbox/browser-executor.mjs +220 -0
- package/src/security/sandbox/detector.mjs +342 -0
- package/src/security/sandbox/isolated-vm-executor.mjs +373 -0
- package/src/security/sandbox/vm2-executor.mjs +217 -0
- package/src/security/sandbox/worker-executor-runtime.mjs +74 -0
- package/src/security/sandbox/worker-executor.mjs +212 -0
- 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
|
+
};
|