@way_marks/server 0.9.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,222 @@
1
+ "use strict";
2
+ /**
3
+ * Auto-Block Rules Engine — Phase 4D
4
+ *
5
+ * Automatically blocks risky rollbacks based on:
6
+ * - Risk assessment score
7
+ * - Policy violations
8
+ * - Configured block rules
9
+ *
10
+ * Prevents rollback of:
11
+ * - High-risk operations (score > threshold)
12
+ * - Policy-violating operations
13
+ * - Manual block rules
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.evaluateAutoBlock = evaluateAutoBlock;
17
+ exports.createBlock = createBlock;
18
+ exports.unblockSession = unblockSession;
19
+ exports.getDefaultBlockRules = getDefaultBlockRules;
20
+ /**
21
+ * Evaluate if rollback should be auto-blocked
22
+ */
23
+ function evaluateAutoBlock(session, actions, riskAssessment, policyViolations, blockRules, riskThreshold = 7.0, userRole = 'user') {
24
+ const blockingRules = [];
25
+ const blockedAt = new Date().toISOString();
26
+ const blockId = generateBlockId();
27
+ // Check risk score threshold
28
+ const isHighRisk = riskAssessment.score >= riskThreshold;
29
+ // Check matching block rules
30
+ const matchingRules = blockRules.filter(rule => rule.enabled && evaluateBlockRuleCondition(rule.condition, actions, session));
31
+ blockingRules.push(...matchingRules);
32
+ // Check policy violations with block action
33
+ const blockingViolations = policyViolations.filter(v => v.action === 'block');
34
+ // Determine if blocked
35
+ const hasBlockingViolations = blockingViolations.length > 0;
36
+ const blocked = isHighRisk || blockingRules.length > 0 || hasBlockingViolations;
37
+ // Generate reason
38
+ let reason = '';
39
+ const reasons = [];
40
+ if (isHighRisk) {
41
+ reasons.push(`Risk score ${riskAssessment.score.toFixed(1)}/10 exceeds threshold ${riskThreshold}`);
42
+ }
43
+ if (blockingRules.length > 0) {
44
+ reasons.push(`${blockingRules.length} block rule(s) matched`);
45
+ }
46
+ if (hasBlockingViolations) {
47
+ reasons.push(`${blockingViolations.length} policy violation(s) require blocking`);
48
+ }
49
+ reason = reasons.join('; ');
50
+ // Determine override capability
51
+ const canOverride = userRole === 'admin' || userRole === 'super_admin';
52
+ const overrideRole = userRole === 'admin' || userRole === 'super_admin' ? 'admin' : 'admin';
53
+ const result = {
54
+ blocked,
55
+ reason: blocked ? reason : undefined,
56
+ blocking_rules: blockingRules,
57
+ policy_violations: blockingViolations,
58
+ risk_score: riskAssessment.score,
59
+ can_override: canOverride,
60
+ override_required_role: overrideRole,
61
+ };
62
+ if (blocked) {
63
+ result.block_id = blockId;
64
+ result.blocked_at = blockedAt;
65
+ }
66
+ return result;
67
+ }
68
+ /**
69
+ * Evaluate block rule condition
70
+ */
71
+ function evaluateBlockRuleCondition(condition, actions, session) {
72
+ // Use same logic as policy conditions
73
+ // Check if any action matches
74
+ for (const action of actions) {
75
+ if (matchCondition(condition, action, session)) {
76
+ return true;
77
+ }
78
+ }
79
+ return false;
80
+ }
81
+ /**
82
+ * Simple condition matcher
83
+ */
84
+ function matchCondition(condition, action, session) {
85
+ switch (condition.type) {
86
+ case 'operation_type':
87
+ case 'tool_name':
88
+ return matchString(action.tool_name, condition.value, condition.operator);
89
+ case 'action_count':
90
+ return matchNumeric(session.action_count, condition.value, condition.operator);
91
+ case 'file_pattern':
92
+ const target = action.target || '';
93
+ return matchString(target, condition.value, condition.operator);
94
+ case 'error_present':
95
+ const hasError = action.status === 'error';
96
+ return hasError === (condition.value === 'true');
97
+ default:
98
+ return false;
99
+ }
100
+ }
101
+ /**
102
+ * String comparison
103
+ */
104
+ function matchString(actual, expected, operator) {
105
+ if (operator === 'equals' || operator === 'in') {
106
+ if (Array.isArray(expected)) {
107
+ return expected.includes(actual);
108
+ }
109
+ return actual === String(expected);
110
+ }
111
+ if (operator === 'contains') {
112
+ if (Array.isArray(expected)) {
113
+ return expected.some(e => actual.includes(String(e)));
114
+ }
115
+ return actual.includes(String(expected));
116
+ }
117
+ if (operator === 'matches_regex') {
118
+ try {
119
+ const regex = new RegExp(String(expected));
120
+ return regex.test(actual);
121
+ }
122
+ catch {
123
+ return false;
124
+ }
125
+ }
126
+ return false;
127
+ }
128
+ /**
129
+ * Numeric comparison
130
+ */
131
+ function matchNumeric(actual, expected, operator) {
132
+ const expectedNum = typeof expected === 'number' ? expected : parseInt(String(expected), 10);
133
+ if (isNaN(expectedNum))
134
+ return false;
135
+ switch (operator) {
136
+ case 'equals':
137
+ return actual === expectedNum;
138
+ case 'greater_than':
139
+ return actual > expectedNum;
140
+ case 'less_than':
141
+ return actual < expectedNum;
142
+ default:
143
+ return false;
144
+ }
145
+ }
146
+ /**
147
+ * Create a block for a session
148
+ */
149
+ function createBlock(sessionId, riskScore, reason, ruleId, policyViolationCount = 0) {
150
+ return {
151
+ block_id: generateBlockId(),
152
+ session_id: sessionId,
153
+ rule_id: ruleId,
154
+ policy_violation_count: policyViolationCount,
155
+ risk_score: riskScore,
156
+ reason,
157
+ blocked_at: new Date().toISOString(),
158
+ };
159
+ }
160
+ /**
161
+ * Unblock a session (admin override)
162
+ */
163
+ function unblockSession(block, unblockedBy, reason, overrideToken) {
164
+ return {
165
+ ...block,
166
+ unblocked_at: new Date().toISOString(),
167
+ unblocked_by: unblockedBy,
168
+ unblock_reason: reason,
169
+ override_token: overrideToken,
170
+ };
171
+ }
172
+ /**
173
+ * Generate unique block ID
174
+ */
175
+ function generateBlockId() {
176
+ return `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
177
+ }
178
+ /**
179
+ * Get default block rules
180
+ */
181
+ function getDefaultBlockRules() {
182
+ return [
183
+ {
184
+ rule_id: 'block-production-delete-hours',
185
+ name: 'Production Delete During Off-Hours',
186
+ description: 'Block delete operations in production environment during off-hours',
187
+ condition: {
188
+ type: 'operation_type',
189
+ operator: 'equals',
190
+ value: 'delete_file',
191
+ },
192
+ action: 'block',
193
+ severity: 'error',
194
+ message: 'Cannot delete files during off-hours in production',
195
+ enabled: true,
196
+ created_at: new Date().toISOString(),
197
+ created_by: 'system',
198
+ },
199
+ {
200
+ rule_id: 'block-large-batch-delete',
201
+ name: 'Large Batch Delete Protection',
202
+ description: 'Block deletion of more than 20 files in single session',
203
+ condition: {
204
+ type: 'action_count',
205
+ operator: 'greater_than',
206
+ value: 20,
207
+ },
208
+ action: 'require_approval',
209
+ severity: 'warning',
210
+ message: 'Large batch deletion requires approval',
211
+ enabled: true,
212
+ created_at: new Date().toISOString(),
213
+ created_by: 'system',
214
+ },
215
+ ];
216
+ }
217
+ exports.default = {
218
+ evaluateAutoBlock,
219
+ createBlock,
220
+ unblockSession,
221
+ getDefaultBlockRules,
222
+ };
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ /**
3
+ * Rollback Manager
4
+ *
5
+ * Handles atomic, all-or-nothing rollback of entire agent sessions.
6
+ * Restores files from snapshots, records rollback operations, and maintains audit trail.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.validateRollbackable = validateRollbackable;
43
+ exports.createRollbackTransaction = createRollbackTransaction;
44
+ exports.executeRollbackTransaction = executeRollbackTransaction;
45
+ exports.rollbackSession = rollbackSession;
46
+ const fs = __importStar(require("fs"));
47
+ const path = __importStar(require("path"));
48
+ const database_1 = require("../db/database");
49
+ /**
50
+ * Parse a snapshot JSON to extract file information
51
+ */
52
+ function parseSnapshot(snapshotJson) {
53
+ if (!snapshotJson)
54
+ return null;
55
+ try {
56
+ return JSON.parse(snapshotJson);
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ /**
63
+ * Validate that all actions in session are rollbackable
64
+ */
65
+ function validateRollbackable(actions) {
66
+ const errors = [];
67
+ for (const action of actions) {
68
+ // Check if action is marked as reversible
69
+ if (action.is_reversible === 0) {
70
+ errors.push({
71
+ action_id: action.action_id,
72
+ reason: 'Action marked as non-reversible',
73
+ });
74
+ }
75
+ // For write_file actions, check if we have before_snapshot
76
+ if (action.tool_name === 'write_file' && !action.before_snapshot) {
77
+ errors.push({
78
+ action_id: action.action_id,
79
+ reason: 'Missing before_snapshot for write_file action',
80
+ });
81
+ }
82
+ // For bash actions, warn if they had side effects (e.g., database mutations)
83
+ if (action.tool_name === 'bash') {
84
+ const inputPayload = JSON.parse(action.input_payload || '{}');
85
+ const command = inputPayload.command || '';
86
+ // List of dangerous patterns that can't be rolled back
87
+ const irreversiblePatterns = [
88
+ /DROP\s+TABLE/i,
89
+ /DELETE\s+FROM/i,
90
+ /TRUNCATE/i,
91
+ /rm\s+-rf/,
92
+ /git\s+push/,
93
+ /npm\s+publish/,
94
+ ];
95
+ for (const pattern of irreversiblePatterns) {
96
+ if (pattern.test(command)) {
97
+ errors.push({
98
+ action_id: action.action_id,
99
+ reason: `Bash command contains irreversible operation: "${command.substring(0, 50)}"`,
100
+ });
101
+ }
102
+ }
103
+ }
104
+ }
105
+ return {
106
+ isValid: errors.length === 0,
107
+ errors,
108
+ warningCount: 0,
109
+ };
110
+ }
111
+ /**
112
+ * Create a rollback transaction (plan, don't execute)
113
+ */
114
+ function createRollbackTransaction(session_id, actions) {
115
+ const fileRestores = [];
116
+ for (const action of actions) {
117
+ // Only process write_file actions for file restoration
118
+ if (action.tool_name === 'write_file') {
119
+ const snapshot = parseSnapshot(action.before_snapshot);
120
+ if (snapshot) {
121
+ fileRestores.push({
122
+ action_id: action.action_id,
123
+ file_path: snapshot.file_path,
124
+ snapshot,
125
+ });
126
+ }
127
+ }
128
+ }
129
+ return {
130
+ session_id,
131
+ actions,
132
+ fileRestores,
133
+ };
134
+ }
135
+ /**
136
+ * Execute rollback transaction (atomic, all-or-nothing)
137
+ */
138
+ function executeRollbackTransaction(transaction) {
139
+ try {
140
+ // Phase 1: Validate all files can be restored before making any changes
141
+ const projectRoot = process.env.WAYMARK_PROJECT_ROOT || process.cwd();
142
+ for (const restore of transaction.fileRestores) {
143
+ const fullPath = path.resolve(projectRoot, restore.file_path);
144
+ // Check write permissions
145
+ if (fs.existsSync(fullPath)) {
146
+ try {
147
+ // Try to stat the file to ensure we can access it
148
+ fs.statSync(fullPath);
149
+ }
150
+ catch (err) {
151
+ throw new Error(`Cannot access file for rollback: ${fullPath}`);
152
+ }
153
+ }
154
+ }
155
+ // Phase 2: Restore all files (all-or-nothing)
156
+ let filesRestored = 0;
157
+ for (const restore of transaction.fileRestores) {
158
+ const fullPath = path.resolve(projectRoot, restore.file_path);
159
+ if (restore.snapshot.existed === false) {
160
+ // File didn't exist before, so delete it
161
+ if (fs.existsSync(fullPath)) {
162
+ fs.unlinkSync(fullPath);
163
+ }
164
+ }
165
+ else {
166
+ // File existed, restore its content
167
+ const dir = path.dirname(fullPath);
168
+ if (!fs.existsSync(dir)) {
169
+ fs.mkdirSync(dir, { recursive: true });
170
+ }
171
+ fs.writeFileSync(fullPath, restore.snapshot.content || '', 'utf-8');
172
+ }
173
+ filesRestored++;
174
+ }
175
+ // Phase 3: Mark actions as rolled back in database
176
+ for (const action of transaction.actions) {
177
+ (0, database_1.markRolledBack)(action.action_id);
178
+ }
179
+ // Phase 4: Mark session as rolled back
180
+ (0, database_1.markSessionRolledBack)(transaction.session_id);
181
+ return {
182
+ success: true,
183
+ filesRestored,
184
+ };
185
+ }
186
+ catch (error) {
187
+ return {
188
+ success: false,
189
+ filesRestored: 0,
190
+ error: error.message,
191
+ };
192
+ }
193
+ }
194
+ /**
195
+ * Rollback entire session (high-level API)
196
+ *
197
+ * Steps:
198
+ * 1. Get all actions in session
199
+ * 2. Validate all are reversible
200
+ * 3. Create rollback transaction
201
+ * 4. Execute rollback (atomic)
202
+ * 5. Record audit trail
203
+ */
204
+ function rollbackSession(session_id) {
205
+ try {
206
+ // Get actions
207
+ const actions = (0, database_1.getSessionActions)(session_id);
208
+ if (actions.length === 0) {
209
+ return {
210
+ success: false,
211
+ message: `Session ${session_id} has no actions to rollback`,
212
+ };
213
+ }
214
+ // Validate
215
+ const validation = validateRollbackable(actions);
216
+ if (!validation.isValid) {
217
+ const errorList = validation.errors.map((e) => `${e.action_id}: ${e.reason}`).join('; ');
218
+ return {
219
+ success: false,
220
+ message: `Cannot rollback session: ${errorList}`,
221
+ };
222
+ }
223
+ // Create transaction
224
+ const transaction = createRollbackTransaction(session_id, actions);
225
+ // Execute transaction
226
+ const result = executeRollbackTransaction(transaction);
227
+ if (!result.success) {
228
+ return {
229
+ success: false,
230
+ message: result.error || 'Unknown error during rollback',
231
+ };
232
+ }
233
+ return {
234
+ success: true,
235
+ message: `Successfully rolled back ${actions.length} action(s), restored ${result.filesRestored} file(s)`,
236
+ filesRestored: result.filesRestored,
237
+ };
238
+ }
239
+ catch (error) {
240
+ return {
241
+ success: false,
242
+ message: `Rollback failed: ${error.message}`,
243
+ };
244
+ }
245
+ }