@way_marks/server 0.8.0 → 1.0.0

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.
@@ -0,0 +1,442 @@
1
+ "use strict";
2
+ /**
3
+ * Risk Assessment Engine — Phase 4A
4
+ *
5
+ * Evaluates rollback safety based on:
6
+ * - Operation type risk (what tools were used)
7
+ * - Scale risk (how many actions)
8
+ * - Error pattern risk (what errors occurred)
9
+ * - Time risk (how old are the actions)
10
+ * - System state risk (current system load)
11
+ *
12
+ * Produces 0-10 risk score and safety level:
13
+ * - NONE (0)
14
+ * - LOW (1-2)
15
+ * - MEDIUM (3-4)
16
+ * - HIGH (5-7)
17
+ * - CRITICAL (8+)
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.assessRisk = assessRisk;
21
+ exports.calculateOperationTypeRisk = calculateOperationTypeRisk;
22
+ exports.calculateScaleRisk = calculateScaleRisk;
23
+ exports.calculateErrorPatternRisk = calculateErrorPatternRisk;
24
+ exports.calculateTimeRisk = calculateTimeRisk;
25
+ exports.calculateSystemStateRisk = calculateSystemStateRisk;
26
+ exports.getRiskLevel = getRiskLevel;
27
+ exports.generateRecommendations = generateRecommendations;
28
+ exports.shouldAutoBlock = shouldAutoBlock;
29
+ exports.getRiskSummary = getRiskSummary;
30
+ /**
31
+ * Assess overall risk of a session rollback
32
+ */
33
+ function assessRisk(session, actions, systemState) {
34
+ const timestamp = new Date().toISOString();
35
+ const factors = [];
36
+ // 1. Operation type risk
37
+ const operationRisk = calculateOperationTypeRisk(actions);
38
+ factors.push({
39
+ category: 'operation_type',
40
+ weight: operationRisk.weight,
41
+ reason: operationRisk.reason,
42
+ sub_factors: operationRisk.sub_factors,
43
+ });
44
+ // 2. Scale risk
45
+ const scaleRisk = calculateScaleRisk(actions);
46
+ factors.push({
47
+ category: 'scale',
48
+ weight: scaleRisk.weight,
49
+ reason: scaleRisk.reason,
50
+ });
51
+ // 3. Error pattern risk
52
+ const errorRisk = calculateErrorPatternRisk(actions);
53
+ factors.push({
54
+ category: 'error_pattern',
55
+ weight: errorRisk.weight,
56
+ reason: errorRisk.reason,
57
+ sub_factors: errorRisk.sub_factors,
58
+ });
59
+ // 4. Time risk
60
+ const timeRisk = calculateTimeRisk(session);
61
+ factors.push({
62
+ category: 'time',
63
+ weight: timeRisk.weight,
64
+ reason: timeRisk.reason,
65
+ });
66
+ // 5. System state risk
67
+ const systemRisk = calculateSystemStateRisk(systemState);
68
+ factors.push({
69
+ category: 'system_state',
70
+ weight: systemRisk.weight,
71
+ reason: systemRisk.reason,
72
+ });
73
+ // Calculate overall score (average of weights)
74
+ const totalWeight = factors.reduce((sum, f) => sum + f.weight, 0);
75
+ const score = Math.min(10, Math.round((totalWeight / 5) * 10) / 10);
76
+ // Determine risk level
77
+ const level = getRiskLevel(score);
78
+ // Generate recommendations
79
+ const recommendations = generateRecommendations(score, level, factors, actions);
80
+ return {
81
+ score,
82
+ level,
83
+ factors,
84
+ recommendations,
85
+ timestamp,
86
+ };
87
+ }
88
+ /**
89
+ * Calculate operation type risk
90
+ * High-risk tools: write_file, delete_file, bash, api_call with mutations
91
+ * Medium-risk: read operations that require consistency
92
+ * Low-risk: information gathering, metadata operations
93
+ */
94
+ function calculateOperationTypeRisk(actions) {
95
+ const toolRisks = {
96
+ // HIGH RISK (2.5 points) - data modification/deletion
97
+ delete_file: 2.5,
98
+ write_file: 2.0,
99
+ bash: 2.0,
100
+ api_call: 1.5, // Depends on mutation type
101
+ // MEDIUM RISK (1.0-1.5 points) - consistency-dependent
102
+ mkdir: 1.5,
103
+ rmdir: 2.0,
104
+ // LOW RISK (0 points) - read-only
105
+ read_file: 0,
106
+ find_files: 0,
107
+ grep: 0,
108
+ };
109
+ const sub_factors = [];
110
+ let totalRisk = 0;
111
+ let highRiskCount = 0;
112
+ let deleteCount = 0;
113
+ let writeCount = 0;
114
+ let bashCount = 0;
115
+ for (const action of actions) {
116
+ const risk = toolRisks[action.tool_name] || 0.5; // Default medium for unknown
117
+ totalRisk += risk;
118
+ if (risk >= 2.0)
119
+ highRiskCount++;
120
+ if (action.tool_name === 'delete_file')
121
+ deleteCount++;
122
+ if (action.tool_name === 'write_file')
123
+ writeCount++;
124
+ if (action.tool_name === 'bash')
125
+ bashCount++;
126
+ sub_factors.push({
127
+ category: action.tool_name,
128
+ weight: risk,
129
+ reason: `Tool: ${action.tool_name}`,
130
+ });
131
+ }
132
+ const avgToolRisk = actions.length > 0 ? totalRisk / actions.length : 0;
133
+ const weight = Math.min(3, avgToolRisk);
134
+ let reason = `Average tool risk: ${avgToolRisk.toFixed(1)}/3.0`;
135
+ if (deleteCount > 0)
136
+ reason += ` (${deleteCount} delete operations)`;
137
+ if (writeCount > 0)
138
+ reason += ` (${writeCount} write operations)`;
139
+ if (bashCount > 0)
140
+ reason += ` (${bashCount} bash commands)`;
141
+ if (highRiskCount > actions.length / 2)
142
+ reason += ' - majority high-risk operations';
143
+ return { weight, reason, sub_factors };
144
+ }
145
+ /**
146
+ * Calculate scale risk based on number of actions
147
+ * 1 file = 0 risk
148
+ * 2-5 files = 0.5 risk
149
+ * 6-20 files = 1.5 risk
150
+ * 20+ files = 3 risk
151
+ */
152
+ function calculateScaleRisk(actions) {
153
+ const count = actions.length;
154
+ let weight = 0;
155
+ if (count <= 1) {
156
+ weight = 0;
157
+ }
158
+ else if (count <= 5) {
159
+ weight = 0.5;
160
+ }
161
+ else if (count <= 20) {
162
+ weight = 1.5;
163
+ }
164
+ else {
165
+ weight = 3.0;
166
+ }
167
+ return {
168
+ weight,
169
+ reason: `${count} action(s) - ${count <= 1
170
+ ? 'single operation'
171
+ : count <= 5
172
+ ? 'small batch'
173
+ : count <= 20
174
+ ? 'large batch'
175
+ : 'very large batch'}`,
176
+ };
177
+ }
178
+ /**
179
+ * Calculate error pattern risk
180
+ * No errors = 0
181
+ * Transient errors (timeout, network) = 0.5
182
+ * Permission errors = 1.5
183
+ * Data errors (validation, format) = 2.0
184
+ * System errors (crash, critical) = 3.0
185
+ */
186
+ function calculateErrorPatternRisk(actions) {
187
+ const sub_factors = [];
188
+ const errorActions = actions.filter(a => a.status === 'error');
189
+ if (errorActions.length === 0) {
190
+ return {
191
+ weight: 0,
192
+ reason: 'No errors - all operations successful',
193
+ sub_factors: [],
194
+ };
195
+ }
196
+ let totalErrorRisk = 0;
197
+ let transientCount = 0;
198
+ let permissionCount = 0;
199
+ let dataCount = 0;
200
+ let systemCount = 0;
201
+ for (const action of errorActions) {
202
+ const errorMsg = (action.error_message || '').toLowerCase();
203
+ let errorRisk = 0.5; // Default: transient
204
+ if (errorMsg.includes('permission') ||
205
+ errorMsg.includes('denied') ||
206
+ errorMsg.includes('unauthorized')) {
207
+ errorRisk = 1.5;
208
+ permissionCount++;
209
+ }
210
+ else if (errorMsg.includes('validation') ||
211
+ errorMsg.includes('format') ||
212
+ errorMsg.includes('invalid')) {
213
+ errorRisk = 2.0;
214
+ dataCount++;
215
+ }
216
+ else if (errorMsg.includes('crash') ||
217
+ errorMsg.includes('segfault') ||
218
+ errorMsg.includes('fatal') ||
219
+ errorMsg.includes('panic')) {
220
+ errorRisk = 3.0;
221
+ systemCount++;
222
+ }
223
+ else if (errorMsg.includes('timeout') || errorMsg.includes('network')) {
224
+ errorRisk = 0.5;
225
+ transientCount++;
226
+ }
227
+ totalErrorRisk += errorRisk;
228
+ sub_factors.push({
229
+ category: `error_${action.action_id}`,
230
+ weight: errorRisk,
231
+ reason: `${action.tool_name}: ${action.error_message?.substring(0, 50) || 'unknown error'}`,
232
+ });
233
+ }
234
+ const avgErrorRisk = totalErrorRisk / errorActions.length;
235
+ const weight = Math.min(3, avgErrorRisk);
236
+ let reason = `${errorActions.length} error(s) - average risk: ${avgErrorRisk.toFixed(1)}/3.0`;
237
+ if (systemCount > 0)
238
+ reason += ` (${systemCount} system errors)`;
239
+ if (dataCount > 0)
240
+ reason += ` (${dataCount} data errors)`;
241
+ if (permissionCount > 0)
242
+ reason += ` (${permissionCount} permission errors)`;
243
+ if (transientCount > 0)
244
+ reason += ` (${transientCount} transient errors)`;
245
+ return { weight, reason, sub_factors };
246
+ }
247
+ /**
248
+ * Calculate time risk based on how old actions are
249
+ * <5 minutes = 0
250
+ * 5-60 minutes = 0.5
251
+ * 1-6 hours = 1.5
252
+ * 6-24 hours = 2.0
253
+ * 24+ hours = 3.0
254
+ */
255
+ function calculateTimeRisk(session) {
256
+ const createdAt = new Date(session.created_at);
257
+ const now = new Date();
258
+ const ageMs = now.getTime() - createdAt.getTime();
259
+ const ageMinutes = ageMs / (1000 * 60);
260
+ const ageHours = ageMinutes / 60;
261
+ const ageDays = ageHours / 24;
262
+ let weight = 0;
263
+ let ageStr = '';
264
+ if (ageMinutes < 5) {
265
+ weight = 0;
266
+ ageStr = `${Math.round(ageMinutes)}m ago - very fresh`;
267
+ }
268
+ else if (ageMinutes < 60) {
269
+ weight = 0.5;
270
+ ageStr = `${Math.round(ageMinutes)}m ago - recent`;
271
+ }
272
+ else if (ageHours < 6) {
273
+ weight = 1.5;
274
+ ageStr = `${Math.round(ageHours)}h ago - older`;
275
+ }
276
+ else if (ageHours < 24) {
277
+ weight = 2.0;
278
+ ageStr = `${Math.round(ageHours)}h ago - very old`;
279
+ }
280
+ else {
281
+ weight = 3.0;
282
+ ageStr = `${Math.round(ageDays)}d ago - ancient`;
283
+ }
284
+ return {
285
+ weight,
286
+ reason: ageStr,
287
+ };
288
+ }
289
+ /**
290
+ * Calculate system state risk
291
+ * Low load (cpu <50%, memory <50%) = 0
292
+ * Moderate (cpu 50-75%, memory 50-75%) = 1.0
293
+ * High (cpu 75-90%, memory 75-90%) = 2.0
294
+ * Critical (cpu >90%, memory >90%) = 3.0
295
+ */
296
+ function calculateSystemStateRisk(systemState) {
297
+ // Default: assume moderate load if no system state provided
298
+ if (!systemState) {
299
+ return {
300
+ weight: 1.0,
301
+ reason: 'System state unknown - assuming moderate load',
302
+ };
303
+ }
304
+ const avgLoad = (systemState.cpu_usage + systemState.memory_usage) / 2;
305
+ let weight = 0;
306
+ let description = '';
307
+ if (avgLoad < 50) {
308
+ weight = 0;
309
+ description = `Idle system (CPU: ${systemState.cpu_usage}%, Memory: ${systemState.memory_usage}%)`;
310
+ }
311
+ else if (avgLoad < 75) {
312
+ weight = 1.0;
313
+ description = `Moderate load (CPU: ${systemState.cpu_usage}%, Memory: ${systemState.memory_usage}%)`;
314
+ }
315
+ else if (avgLoad < 90) {
316
+ weight = 2.0;
317
+ description = `High load (CPU: ${systemState.cpu_usage}%, Memory: ${systemState.memory_usage}%)`;
318
+ }
319
+ else {
320
+ weight = 3.0;
321
+ description = `Critical load (CPU: ${systemState.cpu_usage}%, Memory: ${systemState.memory_usage}%)`;
322
+ }
323
+ if (systemState.active_users > 50) {
324
+ description += ` - ${systemState.active_users} active users`;
325
+ }
326
+ if (systemState.request_rate > 1000) {
327
+ weight = Math.min(3, weight + 0.5);
328
+ description += ` - high request rate (${systemState.request_rate} req/s)`;
329
+ }
330
+ return {
331
+ weight,
332
+ reason: description,
333
+ };
334
+ }
335
+ /**
336
+ * Determine risk level from score
337
+ */
338
+ function getRiskLevel(score) {
339
+ if (score < 1)
340
+ return 'none';
341
+ if (score < 3)
342
+ return 'low';
343
+ if (score < 5)
344
+ return 'medium';
345
+ if (score < 8)
346
+ return 'high';
347
+ return 'critical';
348
+ }
349
+ /**
350
+ * Generate recommendations based on risk assessment
351
+ */
352
+ function generateRecommendations(score, level, factors, actions) {
353
+ const recommendations = [];
354
+ // Score-based recommendations
355
+ if (score < 2) {
356
+ recommendations.push('Low risk: Safe to proceed with rollback');
357
+ }
358
+ else if (score < 4) {
359
+ recommendations.push('Moderate risk: Recommend approval from team lead');
360
+ }
361
+ else if (score < 7) {
362
+ recommendations.push('High risk: Requires escalation to manager');
363
+ recommendations.push('Consider partial rollback of safe operations only');
364
+ }
365
+ else {
366
+ recommendations.push('Critical risk: Requires executive approval');
367
+ recommendations.push('Recommend staged rollback with verification');
368
+ recommendations.push('Consider manual remediation instead of full rollback');
369
+ }
370
+ // Factor-specific recommendations
371
+ const operationFactor = factors.find(f => f.category === 'operation_type');
372
+ if (operationFactor && operationFactor.weight > 1.5) {
373
+ recommendations.push('Identify safe read-only operations for selective rollback');
374
+ }
375
+ const scaleFactor = factors.find(f => f.category === 'scale');
376
+ if (scaleFactor && scaleFactor.weight > 1.5) {
377
+ recommendations.push('Consider staged rollback due to large action count');
378
+ }
379
+ const errorFactor = factors.find(f => f.category === 'error_pattern');
380
+ if (errorFactor && errorFactor.weight > 1.5) {
381
+ recommendations.push('Multiple errors detected: review each error before rollback');
382
+ }
383
+ const timeFactor = factors.find(f => f.category === 'time');
384
+ if (timeFactor && timeFactor.weight > 1.5) {
385
+ recommendations.push('Operations are old: ensure no dependencies have changed');
386
+ }
387
+ const systemFactor = factors.find(f => f.category === 'system_state');
388
+ if (systemFactor && systemFactor.weight > 1.5) {
389
+ recommendations.push('System under high load: consider scheduling rollback during off-peak');
390
+ }
391
+ // Check for specific patterns
392
+ const deleteCount = actions.filter(a => a.tool_name === 'delete_file').length;
393
+ if (deleteCount > 0) {
394
+ recommendations.push(`${deleteCount} delete operation(s): verify backups before proceeding`);
395
+ }
396
+ const writeCount = actions.filter(a => a.tool_name === 'write_file').length;
397
+ if (writeCount > 5) {
398
+ recommendations.push('Multiple write operations: consider data consistency checks');
399
+ }
400
+ return recommendations;
401
+ }
402
+ /**
403
+ * Check if risk assessment should auto-block rollback
404
+ * Auto-block threshold: configurable, default 7.0
405
+ */
406
+ function shouldAutoBlock(score, threshold = 7.0) {
407
+ return score >= threshold;
408
+ }
409
+ /**
410
+ * Get human-readable risk summary
411
+ */
412
+ function getRiskSummary(assessment) {
413
+ const score = assessment.score.toFixed(1);
414
+ const level = assessment.level.toUpperCase();
415
+ let summary = `Risk Level: ${level} (${score}/10)\n`;
416
+ summary += '\nRisk Factors:\n';
417
+ for (const factor of assessment.factors) {
418
+ summary += ` • ${factor.category}: ${factor.weight.toFixed(1)}/3.0 - ${factor.reason}\n`;
419
+ }
420
+ if (assessment.blocked_reason) {
421
+ summary += `\n⚠️ AUTO-BLOCKED: ${assessment.blocked_reason}\n`;
422
+ }
423
+ if (assessment.recommendations.length > 0) {
424
+ summary += '\nRecommendations:\n';
425
+ for (const rec of assessment.recommendations) {
426
+ summary += ` → ${rec}\n`;
427
+ }
428
+ }
429
+ return summary;
430
+ }
431
+ exports.default = {
432
+ assessRisk,
433
+ calculateOperationTypeRisk,
434
+ calculateScaleRisk,
435
+ calculateErrorPatternRisk,
436
+ calculateTimeRisk,
437
+ calculateSystemStateRisk,
438
+ getRiskLevel,
439
+ generateRecommendations,
440
+ shouldAutoBlock,
441
+ getRiskSummary,
442
+ };