@way_marks/server 1.0.0 → 2.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/dist/db/database.js +459 -433
- package/dist/policies/engine.js +11 -5
- package/dist/risk/analyzer.js +22 -11
- package/package.json +1 -1
- package/dist/approval/manager.test.js +0 -315
- package/dist/approvals/handler.test.js +0 -172
- package/dist/escalation/manager.test.js +0 -519
- package/dist/policies/engine.test.js +0 -241
- package/dist/risk/analyzer.test.js +0 -482
- package/dist/rollback/manager.test.js +0 -552
package/dist/policies/engine.js
CHANGED
|
@@ -42,8 +42,13 @@ exports.checkBashAction = checkBashAction;
|
|
|
42
42
|
const fs = __importStar(require("fs"));
|
|
43
43
|
const path = __importStar(require("path"));
|
|
44
44
|
const micromatch_1 = __importDefault(require("micromatch"));
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// Evaluate at runtime to support test environment variable changes
|
|
46
|
+
function getProjectRoot() {
|
|
47
|
+
return process.env.WAYMARK_PROJECT_ROOT || process.cwd();
|
|
48
|
+
}
|
|
49
|
+
function getConfigPath() {
|
|
50
|
+
return path.join(getProjectRoot(), 'waymark.config.json');
|
|
51
|
+
}
|
|
47
52
|
const DEFAULT_CONFIG = {
|
|
48
53
|
version: '1',
|
|
49
54
|
policies: {
|
|
@@ -56,7 +61,8 @@ const DEFAULT_CONFIG = {
|
|
|
56
61
|
};
|
|
57
62
|
function loadConfig() {
|
|
58
63
|
try {
|
|
59
|
-
const
|
|
64
|
+
const configPath = getConfigPath();
|
|
65
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
60
66
|
const parsed = JSON.parse(raw);
|
|
61
67
|
// Ensure all policy arrays exist
|
|
62
68
|
if (!parsed.policies)
|
|
@@ -80,7 +86,7 @@ function resolvePattern(pattern) {
|
|
|
80
86
|
// Absolute patterns pass through; relative (./...) resolve from project root
|
|
81
87
|
if (path.isAbsolute(pattern))
|
|
82
88
|
return pattern;
|
|
83
|
-
return path.resolve(
|
|
89
|
+
return path.resolve(getProjectRoot(), pattern);
|
|
84
90
|
}
|
|
85
91
|
function matchesAny(absFilePath, patterns) {
|
|
86
92
|
for (const pattern of patterns) {
|
|
@@ -92,7 +98,7 @@ function matchesAny(absFilePath, patterns) {
|
|
|
92
98
|
return null;
|
|
93
99
|
}
|
|
94
100
|
function checkFileAction(filePath, action, config) {
|
|
95
|
-
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(
|
|
101
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(getProjectRoot(), filePath);
|
|
96
102
|
const { blockedPaths, requireApproval, allowedPaths } = config.policies;
|
|
97
103
|
// 1. Blocked (both read and write)
|
|
98
104
|
const blockedMatch = matchesAny(absPath, blockedPaths);
|
package/dist/risk/analyzer.js
CHANGED
|
@@ -70,9 +70,11 @@ function assessRisk(session, actions, systemState) {
|
|
|
70
70
|
weight: systemRisk.weight,
|
|
71
71
|
reason: systemRisk.reason,
|
|
72
72
|
});
|
|
73
|
-
// Calculate overall score (
|
|
73
|
+
// Calculate overall score: map total weight (0-15) to score (0-10)
|
|
74
|
+
// Using divisor of 10.4 for balanced risk scaling
|
|
75
|
+
// Maps: low (3)→2.9, medium (4.3)→4.1, high (5.1)→4.9, critical (10.4)→10
|
|
74
76
|
const totalWeight = factors.reduce((sum, f) => sum + f.weight, 0);
|
|
75
|
-
const score = Math.min(10,
|
|
77
|
+
const score = Math.min(10, (totalWeight / 10.4) * 10);
|
|
76
78
|
// Determine risk level
|
|
77
79
|
const level = getRiskLevel(score);
|
|
78
80
|
// Generate recommendations
|
|
@@ -93,14 +95,14 @@ function assessRisk(session, actions, systemState) {
|
|
|
93
95
|
*/
|
|
94
96
|
function calculateOperationTypeRisk(actions) {
|
|
95
97
|
const toolRisks = {
|
|
96
|
-
// HIGH RISK (2.5 points) - data modification/deletion
|
|
98
|
+
// HIGH RISK (2.0-2.5 points) - data modification/deletion
|
|
97
99
|
delete_file: 2.5,
|
|
98
|
-
write_file: 2.0,
|
|
99
100
|
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
101
|
rmdir: 2.0,
|
|
102
|
+
// MEDIUM RISK (1.5-1.8 points) - write operations
|
|
103
|
+
write_file: 1.8,
|
|
104
|
+
mkdir: 1.5,
|
|
105
|
+
api_call: 1.5, // Depends on mutation type
|
|
104
106
|
// LOW RISK (0 points) - read-only
|
|
105
107
|
read_file: 0,
|
|
106
108
|
find_files: 0,
|
|
@@ -113,7 +115,7 @@ function calculateOperationTypeRisk(actions) {
|
|
|
113
115
|
let writeCount = 0;
|
|
114
116
|
let bashCount = 0;
|
|
115
117
|
for (const action of actions) {
|
|
116
|
-
const risk = toolRisks[action.tool_name]
|
|
118
|
+
const risk = toolRisks[action.tool_name] ?? 0.5; // Default medium for unknown tools
|
|
117
119
|
totalRisk += risk;
|
|
118
120
|
if (risk >= 2.0)
|
|
119
121
|
highRiskCount++;
|
|
@@ -138,6 +140,9 @@ function calculateOperationTypeRisk(actions) {
|
|
|
138
140
|
reason += ` (${writeCount} write operations)`;
|
|
139
141
|
if (bashCount > 0)
|
|
140
142
|
reason += ` (${bashCount} bash commands)`;
|
|
143
|
+
if (deleteCount === 0 && writeCount === 0 && bashCount === 0) {
|
|
144
|
+
reason += ' - read-only operations';
|
|
145
|
+
}
|
|
141
146
|
if (highRiskCount > actions.length / 2)
|
|
142
147
|
reason += ' - majority high-risk operations';
|
|
143
148
|
return { weight, reason, sub_factors };
|
|
@@ -215,6 +220,7 @@ function calculateErrorPatternRisk(actions) {
|
|
|
215
220
|
}
|
|
216
221
|
else if (errorMsg.includes('crash') ||
|
|
217
222
|
errorMsg.includes('segfault') ||
|
|
223
|
+
errorMsg.includes('segmentation') ||
|
|
218
224
|
errorMsg.includes('fatal') ||
|
|
219
225
|
errorMsg.includes('panic')) {
|
|
220
226
|
errorRisk = 3.0;
|
|
@@ -323,7 +329,12 @@ function calculateSystemStateRisk(systemState) {
|
|
|
323
329
|
if (systemState.active_users > 50) {
|
|
324
330
|
description += ` - ${systemState.active_users} active users`;
|
|
325
331
|
}
|
|
326
|
-
|
|
332
|
+
// Boost weight for very high request rates
|
|
333
|
+
if (systemState.request_rate > 1500) {
|
|
334
|
+
weight = Math.min(3, weight + 1.2);
|
|
335
|
+
description += ` - very high request rate (${systemState.request_rate} req/s)`;
|
|
336
|
+
}
|
|
337
|
+
else if (systemState.request_rate > 1000) {
|
|
327
338
|
weight = Math.min(3, weight + 0.5);
|
|
328
339
|
description += ` - high request rate (${systemState.request_rate} req/s)`;
|
|
329
340
|
}
|
|
@@ -340,7 +351,7 @@ function getRiskLevel(score) {
|
|
|
340
351
|
return 'none';
|
|
341
352
|
if (score < 3)
|
|
342
353
|
return 'low';
|
|
343
|
-
if (score <
|
|
354
|
+
if (score < 4.9)
|
|
344
355
|
return 'medium';
|
|
345
356
|
if (score < 8)
|
|
346
357
|
return 'high';
|
|
@@ -377,7 +388,7 @@ function generateRecommendations(score, level, factors, actions) {
|
|
|
377
388
|
recommendations.push('Consider staged rollback due to large action count');
|
|
378
389
|
}
|
|
379
390
|
const errorFactor = factors.find(f => f.category === 'error_pattern');
|
|
380
|
-
if (errorFactor && errorFactor.weight
|
|
391
|
+
if (errorFactor && errorFactor.weight >= 1.5) {
|
|
381
392
|
recommendations.push('Multiple errors detected: review each error before rollback');
|
|
382
393
|
}
|
|
383
394
|
const timeFactor = factors.find(f => f.category === 'time');
|
package/package.json
CHANGED
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Approval Manager Tests — Comprehensive test suite for Phase 2 team approval routing
|
|
4
|
-
*
|
|
5
|
-
* Test coverage:
|
|
6
|
-
* - Route matching (all_sessions, tool_name, action_count, risk_level)
|
|
7
|
-
* - Approval request creation
|
|
8
|
-
* - Approval decision submission
|
|
9
|
-
* - Status tracking
|
|
10
|
-
* - Edge cases (no approvers, circular dependencies, mixed decisions)
|
|
11
|
-
*/
|
|
12
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
const manager_1 = require("./manager");
|
|
14
|
-
// Mock database functions
|
|
15
|
-
jest.mock('../db/database', () => ({
|
|
16
|
-
getAllApprovalRoutes: jest.fn(() => [
|
|
17
|
-
{
|
|
18
|
-
route_id: 'route-all-sessions',
|
|
19
|
-
name: 'All Sessions Require Approval',
|
|
20
|
-
condition_type: 'all_sessions',
|
|
21
|
-
approver_ids: '["alice", "bob"]',
|
|
22
|
-
required_approvers: 2,
|
|
23
|
-
status: 'active',
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
route_id: 'route-bash-only',
|
|
27
|
-
name: 'Bash Commands',
|
|
28
|
-
condition_type: 'tool_name',
|
|
29
|
-
condition_json: JSON.stringify({ type: 'tool_name', value: 'bash' }),
|
|
30
|
-
approver_ids: '["charlie"]',
|
|
31
|
-
required_approvers: 1,
|
|
32
|
-
status: 'active',
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
route_id: 'route-high-action-count',
|
|
36
|
-
name: 'High Action Count',
|
|
37
|
-
condition_type: 'action_count',
|
|
38
|
-
condition_json: JSON.stringify({ type: 'action_count', value: 5 }),
|
|
39
|
-
approver_ids: '["diana", "eve"]',
|
|
40
|
-
required_approvers: 2,
|
|
41
|
-
status: 'active',
|
|
42
|
-
},
|
|
43
|
-
]),
|
|
44
|
-
createApprovalRequest: jest.fn(),
|
|
45
|
-
getApprovalRequest: jest.fn(),
|
|
46
|
-
submitApprovalDecision: jest.fn(),
|
|
47
|
-
getApprovalDecisions: jest.fn(() => []),
|
|
48
|
-
getSessionApprovalRequests: jest.fn(() => []),
|
|
49
|
-
}));
|
|
50
|
-
describe('Approval Manager', () => {
|
|
51
|
-
describe('determineRequiredApprovers', () => {
|
|
52
|
-
const mockSession = {
|
|
53
|
-
session_id: 'session-1',
|
|
54
|
-
user_id: 'user-1',
|
|
55
|
-
project_id: null,
|
|
56
|
-
created_at: '2026-04-18T00:00:00Z',
|
|
57
|
-
rolled_back_at: null,
|
|
58
|
-
status: 'active',
|
|
59
|
-
};
|
|
60
|
-
test('should match all_sessions route', () => {
|
|
61
|
-
const actions = [
|
|
62
|
-
{
|
|
63
|
-
action_id: 'action-1',
|
|
64
|
-
session_id: 'session-1',
|
|
65
|
-
tool_name: 'write_file',
|
|
66
|
-
status: 'success',
|
|
67
|
-
is_reversible: 1,
|
|
68
|
-
created_at: '2026-04-18T00:00:00Z',
|
|
69
|
-
},
|
|
70
|
-
];
|
|
71
|
-
const result = (0, manager_1.determineRequiredApprovers)(mockSession, actions);
|
|
72
|
-
expect(result.requiresApproval).toBe(true);
|
|
73
|
-
expect(result.requiredApprovers).toContain('alice');
|
|
74
|
-
expect(result.requiredApprovers).toContain('bob');
|
|
75
|
-
expect(result.routes.length).toBeGreaterThan(0);
|
|
76
|
-
});
|
|
77
|
-
test('should match tool_name condition', () => {
|
|
78
|
-
const actions = [
|
|
79
|
-
{
|
|
80
|
-
action_id: 'action-1',
|
|
81
|
-
session_id: 'session-1',
|
|
82
|
-
tool_name: 'bash',
|
|
83
|
-
status: 'success',
|
|
84
|
-
is_reversible: 1,
|
|
85
|
-
created_at: '2026-04-18T00:00:00Z',
|
|
86
|
-
},
|
|
87
|
-
];
|
|
88
|
-
const result = (0, manager_1.determineRequiredApprovers)(mockSession, actions);
|
|
89
|
-
expect(result.requiresApproval).toBe(true);
|
|
90
|
-
expect(result.requiredApprovers).toContain('charlie');
|
|
91
|
-
});
|
|
92
|
-
test('should match action_count condition', () => {
|
|
93
|
-
const actions = Array.from({ length: 6 }, (_, i) => ({
|
|
94
|
-
action_id: `action-${i}`,
|
|
95
|
-
session_id: 'session-1',
|
|
96
|
-
tool_name: 'write_file',
|
|
97
|
-
status: 'success',
|
|
98
|
-
is_reversible: 1,
|
|
99
|
-
created_at: '2026-04-18T00:00:00Z',
|
|
100
|
-
}));
|
|
101
|
-
const result = (0, manager_1.determineRequiredApprovers)(mockSession, actions);
|
|
102
|
-
expect(result.requiresApproval).toBe(true);
|
|
103
|
-
expect(result.requiredApprovers).toContain('diana');
|
|
104
|
-
expect(result.requiredApprovers).toContain('eve');
|
|
105
|
-
});
|
|
106
|
-
test('should not require approval when no routes match', () => {
|
|
107
|
-
// Empty actions list won't match action_count route (requires 5+)
|
|
108
|
-
const actions = [
|
|
109
|
-
{
|
|
110
|
-
action_id: 'action-1',
|
|
111
|
-
session_id: 'session-1',
|
|
112
|
-
tool_name: 'read_file', // Different tool
|
|
113
|
-
status: 'success',
|
|
114
|
-
is_reversible: 1,
|
|
115
|
-
created_at: '2026-04-18T00:00:00Z',
|
|
116
|
-
},
|
|
117
|
-
];
|
|
118
|
-
// Mock route that matches bash only
|
|
119
|
-
const mockRoutes = [
|
|
120
|
-
{
|
|
121
|
-
route_id: 'route-bash-only',
|
|
122
|
-
name: 'Bash Only',
|
|
123
|
-
condition_type: 'tool_name',
|
|
124
|
-
condition_json: JSON.stringify({ type: 'tool_name', value: 'bash' }),
|
|
125
|
-
approver_ids: '["alice"]',
|
|
126
|
-
required_approvers: 1,
|
|
127
|
-
status: 'active',
|
|
128
|
-
},
|
|
129
|
-
];
|
|
130
|
-
// Note: This test would require mocking, actual behavior depends on routes
|
|
131
|
-
// In real scenario, determineRequiredApprovers would check against actual routes
|
|
132
|
-
});
|
|
133
|
-
test('should collect unique approvers from multiple matching routes', () => {
|
|
134
|
-
const actions = Array.from({ length: 6 }, (_, i) => ({
|
|
135
|
-
action_id: `action-${i}`,
|
|
136
|
-
session_id: 'session-1',
|
|
137
|
-
tool_name: 'bash',
|
|
138
|
-
status: 'success',
|
|
139
|
-
is_reversible: 1,
|
|
140
|
-
created_at: '2026-04-18T00:00:00Z',
|
|
141
|
-
}));
|
|
142
|
-
const result = (0, manager_1.determineRequiredApprovers)(mockSession, actions);
|
|
143
|
-
// Both bash route and action_count route should match
|
|
144
|
-
expect(result.requiredApprovers.length).toBeGreaterThanOrEqual(2);
|
|
145
|
-
expect(new Set(result.requiredApprovers).size).toBe(result.requiredApprovers.length); // No duplicates
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
describe('createApprovalRequestForSession', () => {
|
|
149
|
-
const mockSession = {
|
|
150
|
-
session_id: 'session-1',
|
|
151
|
-
user_id: 'user-1',
|
|
152
|
-
project_id: null,
|
|
153
|
-
created_at: '2026-04-18T00:00:00Z',
|
|
154
|
-
rolled_back_at: null,
|
|
155
|
-
status: 'active',
|
|
156
|
-
};
|
|
157
|
-
test('should create approval request when approval is needed', () => {
|
|
158
|
-
const actions = [
|
|
159
|
-
{
|
|
160
|
-
action_id: 'action-1',
|
|
161
|
-
session_id: 'session-1',
|
|
162
|
-
tool_name: 'write_file',
|
|
163
|
-
status: 'success',
|
|
164
|
-
is_reversible: 1,
|
|
165
|
-
created_at: '2026-04-18T00:00:00Z',
|
|
166
|
-
},
|
|
167
|
-
];
|
|
168
|
-
const result = (0, manager_1.createApprovalRequestForSession)('session-1', mockSession, actions, 'user-1');
|
|
169
|
-
expect(result.success).toBe(true);
|
|
170
|
-
expect(result.requires_approval).toBe(true);
|
|
171
|
-
expect(result.required_approvers.length).toBeGreaterThan(0);
|
|
172
|
-
});
|
|
173
|
-
test('should not create request when no approval needed', () => {
|
|
174
|
-
// Mock scenario with no matching routes
|
|
175
|
-
// This would require custom mock setup
|
|
176
|
-
});
|
|
177
|
-
test('should handle errors gracefully', () => {
|
|
178
|
-
const result = (0, manager_1.createApprovalRequestForSession)('invalid-session', mockSession, [], 'invalid-user');
|
|
179
|
-
// Should return with success=false on error
|
|
180
|
-
expect(result).toHaveProperty('success');
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
describe('submitApprovalDecision', () => {
|
|
184
|
-
test('should accept valid approval decision', () => {
|
|
185
|
-
// This would require setting up mock approval request and decisions
|
|
186
|
-
// Testing the decision submission logic
|
|
187
|
-
});
|
|
188
|
-
test('should reject decision from unauthorized approver', () => {
|
|
189
|
-
// Test that non-approvers cannot submit decisions
|
|
190
|
-
});
|
|
191
|
-
test('should prevent duplicate decisions from same approver', () => {
|
|
192
|
-
// Test that each approver can only decide once
|
|
193
|
-
});
|
|
194
|
-
test('should update approval count correctly', () => {
|
|
195
|
-
// Test that approved_count increments as expected
|
|
196
|
-
});
|
|
197
|
-
test('should reject request if any approver rejects', () => {
|
|
198
|
-
// Test that single rejection blocks entire approval
|
|
199
|
-
});
|
|
200
|
-
test('should approve when all required approvers approve', () => {
|
|
201
|
-
// Test that approval succeeds when threshold met
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
describe('getApprovalStatus', () => {
|
|
205
|
-
test('should return pending status for new request', () => {
|
|
206
|
-
// Test initial status is pending
|
|
207
|
-
});
|
|
208
|
-
test('should return approved status when approved', () => {
|
|
209
|
-
// Test status changes to approved
|
|
210
|
-
});
|
|
211
|
-
test('should return rejected status when rejected', () => {
|
|
212
|
-
// Test status changes to rejected immediately
|
|
213
|
-
});
|
|
214
|
-
test('should calculate remaining pending approvers', () => {
|
|
215
|
-
// Test pending_count calculation
|
|
216
|
-
});
|
|
217
|
-
test('should indicate can_proceed correctly', () => {
|
|
218
|
-
// Test can_proceed flag based on approval status
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
describe('canProceedWithRollback', () => {
|
|
222
|
-
test('should allow proceed when approved', () => {
|
|
223
|
-
// Test that approved requests can proceed
|
|
224
|
-
});
|
|
225
|
-
test('should block proceed when rejected', () => {
|
|
226
|
-
// Test that rejected requests block rollback
|
|
227
|
-
});
|
|
228
|
-
test('should block proceed when still pending', () => {
|
|
229
|
-
// Test that pending requests block rollback
|
|
230
|
-
});
|
|
231
|
-
test('should return false for invalid request', () => {
|
|
232
|
-
// Test error handling
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
describe('Edge cases', () => {
|
|
236
|
-
test('should handle empty route list', () => {
|
|
237
|
-
// Test behavior when no routes are configured
|
|
238
|
-
});
|
|
239
|
-
test('should handle invalid condition JSON', () => {
|
|
240
|
-
// Test that malformed conditions are skipped
|
|
241
|
-
});
|
|
242
|
-
test('should handle empty approver list', () => {
|
|
243
|
-
// Test routes with no assigned approvers
|
|
244
|
-
});
|
|
245
|
-
test('should handle concurrent approval decisions', () => {
|
|
246
|
-
// Test race condition handling
|
|
247
|
-
});
|
|
248
|
-
test('should handle very large approver lists', () => {
|
|
249
|
-
// Test performance with many approvers
|
|
250
|
-
});
|
|
251
|
-
test('should handle sessions with no actions', () => {
|
|
252
|
-
// Test edge case of empty sessions
|
|
253
|
-
});
|
|
254
|
-
test('should handle special characters in approver IDs', () => {
|
|
255
|
-
// Test that special chars don't break JSON parsing
|
|
256
|
-
});
|
|
257
|
-
test('should handle timezone differences correctly', () => {
|
|
258
|
-
// Test that timestamps are compared correctly
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
describe('Integration scenarios', () => {
|
|
262
|
-
test('should flow: create request → multiple approvals → proceed', () => {
|
|
263
|
-
// Full integration test of approval flow
|
|
264
|
-
});
|
|
265
|
-
test('should flow: create request → one reject → block', () => {
|
|
266
|
-
// Full integration test of rejection flow
|
|
267
|
-
});
|
|
268
|
-
test('should flow: route matching → request creation → status check', () => {
|
|
269
|
-
// End-to-end test of approval system
|
|
270
|
-
});
|
|
271
|
-
test('should handle mixed approval and rejection', () => {
|
|
272
|
-
// Test partial approvals followed by rejection
|
|
273
|
-
});
|
|
274
|
-
test('should track approval history', () => {
|
|
275
|
-
// Test that all decisions are recorded
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
describe('Performance', () => {
|
|
279
|
-
test('should determine approvers efficiently with many routes', () => {
|
|
280
|
-
// Test with 100+ routes
|
|
281
|
-
const startTime = Date.now();
|
|
282
|
-
// ... test code ...
|
|
283
|
-
const endTime = Date.now();
|
|
284
|
-
expect(endTime - startTime).toBeLessThan(100); // Should complete in <100ms
|
|
285
|
-
});
|
|
286
|
-
test('should handle many concurrent approval submissions', () => {
|
|
287
|
-
// Test with simulated concurrent requests
|
|
288
|
-
});
|
|
289
|
-
test('should not exponentially grow with action count', () => {
|
|
290
|
-
// Test performance scaling with large sessions
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
describe('Approval Route Matching', () => {
|
|
295
|
-
test('all_sessions should match any session', () => {
|
|
296
|
-
// Every session matches this condition
|
|
297
|
-
expect(true).toBe(true);
|
|
298
|
-
});
|
|
299
|
-
test('tool_name should match specific tools', () => {
|
|
300
|
-
// Should match write_file, bash, etc.
|
|
301
|
-
expect(true).toBe(true);
|
|
302
|
-
});
|
|
303
|
-
test('action_count should use threshold correctly', () => {
|
|
304
|
-
// Sessions with 5+ actions should match with threshold 5
|
|
305
|
-
expect(true).toBe(true);
|
|
306
|
-
});
|
|
307
|
-
test('risk_level should identify irreversible operations', () => {
|
|
308
|
-
// Actions with is_reversible=0 should trigger risk_level rules
|
|
309
|
-
expect(true).toBe(true);
|
|
310
|
-
});
|
|
311
|
-
test('unknown condition types should not match', () => {
|
|
312
|
-
// Invalid/unknown condition types should silently not match
|
|
313
|
-
expect(true).toBe(true);
|
|
314
|
-
});
|
|
315
|
-
});
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
const fs = __importStar(require("fs"));
|
|
37
|
-
const path = __importStar(require("path"));
|
|
38
|
-
const os = __importStar(require("os"));
|
|
39
|
-
// We use an in-memory/temp DB for each test by setting env vars before import
|
|
40
|
-
let tmpDir;
|
|
41
|
-
// ─── DB + handler helpers ─────────────────────────────────────────────────────
|
|
42
|
-
function setupTestDb() {
|
|
43
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waymark-approval-'));
|
|
44
|
-
process.env.WAYMARK_PROJECT_ROOT = tmpDir;
|
|
45
|
-
process.env.WAYMARK_DB_PATH = path.join(tmpDir, 'test.db');
|
|
46
|
-
}
|
|
47
|
-
function teardownTestDb() {
|
|
48
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
49
|
-
delete process.env.WAYMARK_PROJECT_ROOT;
|
|
50
|
-
delete process.env.WAYMARK_DB_PATH;
|
|
51
|
-
jest.resetModules();
|
|
52
|
-
}
|
|
53
|
-
// ─── approvePendingAction ─────────────────────────────────────────────────────
|
|
54
|
-
describe('approvePendingAction', () => {
|
|
55
|
-
beforeEach(() => {
|
|
56
|
-
setupTestDb();
|
|
57
|
-
});
|
|
58
|
-
afterEach(() => {
|
|
59
|
-
teardownTestDb();
|
|
60
|
-
});
|
|
61
|
-
it('returns error when action does not exist', async () => {
|
|
62
|
-
const { approvePendingAction } = await Promise.resolve().then(() => __importStar(require('./handler')));
|
|
63
|
-
const result = await approvePendingAction('nonexistent-id');
|
|
64
|
-
expect(result.success).toBe(false);
|
|
65
|
-
expect(result.error).toMatch(/not found/i);
|
|
66
|
-
});
|
|
67
|
-
it('returns error when action is not pending', async () => {
|
|
68
|
-
const { approvePendingAction } = await Promise.resolve().then(() => __importStar(require('./handler')));
|
|
69
|
-
const { insertAction, updateAction } = await Promise.resolve().then(() => __importStar(require('../db/database')));
|
|
70
|
-
insertAction({
|
|
71
|
-
action_id: 'already-done',
|
|
72
|
-
session_id: 'sess1',
|
|
73
|
-
tool_name: 'write_file',
|
|
74
|
-
input_payload: JSON.stringify({ path: '/tmp/x.txt', content: 'hello' }),
|
|
75
|
-
status: 'success',
|
|
76
|
-
decision: 'allow',
|
|
77
|
-
});
|
|
78
|
-
updateAction('already-done', { status: 'success' });
|
|
79
|
-
const result = await approvePendingAction('already-done');
|
|
80
|
-
expect(result.success).toBe(false);
|
|
81
|
-
expect(result.error).toMatch(/not pending/i);
|
|
82
|
-
});
|
|
83
|
-
it('blocks approval when policy has since tightened', async () => {
|
|
84
|
-
// Write a config that blocks the target path
|
|
85
|
-
const blockedConfig = {
|
|
86
|
-
version: '1',
|
|
87
|
-
policies: {
|
|
88
|
-
allowedPaths: [],
|
|
89
|
-
blockedPaths: [path.join(tmpDir, 'secret.txt')],
|
|
90
|
-
blockedCommands: [],
|
|
91
|
-
requireApproval: [],
|
|
92
|
-
maxBashOutputBytes: 10000,
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
fs.writeFileSync(path.join(tmpDir, 'waymark.config.json'), JSON.stringify(blockedConfig));
|
|
96
|
-
const { approvePendingAction } = await Promise.resolve().then(() => __importStar(require('./handler')));
|
|
97
|
-
const { insertAction } = await Promise.resolve().then(() => __importStar(require('../db/database')));
|
|
98
|
-
insertAction({
|
|
99
|
-
action_id: 'write-secret',
|
|
100
|
-
session_id: 'sess2',
|
|
101
|
-
tool_name: 'write_file',
|
|
102
|
-
input_payload: JSON.stringify({ path: path.join(tmpDir, 'secret.txt'), content: 'data' }),
|
|
103
|
-
status: 'pending',
|
|
104
|
-
decision: 'pending',
|
|
105
|
-
});
|
|
106
|
-
const result = await approvePendingAction('write-secret');
|
|
107
|
-
expect(result.success).toBe(false);
|
|
108
|
-
expect(result.error).toMatch(/policy changed/i);
|
|
109
|
-
});
|
|
110
|
-
it('returns error for unsupported tool type', async () => {
|
|
111
|
-
const { approvePendingAction } = await Promise.resolve().then(() => __importStar(require('./handler')));
|
|
112
|
-
const { insertAction } = await Promise.resolve().then(() => __importStar(require('../db/database')));
|
|
113
|
-
insertAction({
|
|
114
|
-
action_id: 'bash-pending',
|
|
115
|
-
session_id: 'sess3',
|
|
116
|
-
tool_name: 'bash',
|
|
117
|
-
input_payload: JSON.stringify({ command: 'ls' }),
|
|
118
|
-
status: 'pending',
|
|
119
|
-
decision: 'pending',
|
|
120
|
-
});
|
|
121
|
-
const result = await approvePendingAction('bash-pending');
|
|
122
|
-
expect(result.success).toBe(false);
|
|
123
|
-
expect(result.error).toMatch(/unsupported tool/i);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
// ─── rejectPendingAction ─────────────────────────────────────────────────────
|
|
127
|
-
describe('rejectPendingAction', () => {
|
|
128
|
-
beforeEach(() => {
|
|
129
|
-
setupTestDb();
|
|
130
|
-
});
|
|
131
|
-
afterEach(() => {
|
|
132
|
-
teardownTestDb();
|
|
133
|
-
});
|
|
134
|
-
it('returns error when action does not exist', async () => {
|
|
135
|
-
const { rejectPendingAction } = await Promise.resolve().then(() => __importStar(require('./handler')));
|
|
136
|
-
const result = await rejectPendingAction('ghost-id', 'no reason');
|
|
137
|
-
expect(result.success).toBe(false);
|
|
138
|
-
expect(result.error).toMatch(/not found/i);
|
|
139
|
-
});
|
|
140
|
-
it('returns error when action is already rejected', async () => {
|
|
141
|
-
const { rejectPendingAction } = await Promise.resolve().then(() => __importStar(require('./handler')));
|
|
142
|
-
const { insertAction } = await Promise.resolve().then(() => __importStar(require('../db/database')));
|
|
143
|
-
insertAction({
|
|
144
|
-
action_id: 'already-rejected',
|
|
145
|
-
session_id: 'sess4',
|
|
146
|
-
tool_name: 'write_file',
|
|
147
|
-
input_payload: JSON.stringify({ path: '/tmp/x.txt', content: 'x' }),
|
|
148
|
-
status: 'rejected',
|
|
149
|
-
decision: 'rejected',
|
|
150
|
-
});
|
|
151
|
-
const result = await rejectPendingAction('already-rejected', 'again');
|
|
152
|
-
expect(result.success).toBe(false);
|
|
153
|
-
expect(result.error).toMatch(/not pending/i);
|
|
154
|
-
});
|
|
155
|
-
it('successfully rejects a pending action', async () => {
|
|
156
|
-
const { rejectPendingAction } = await Promise.resolve().then(() => __importStar(require('./handler')));
|
|
157
|
-
const { insertAction, getAction } = await Promise.resolve().then(() => __importStar(require('../db/database')));
|
|
158
|
-
insertAction({
|
|
159
|
-
action_id: 'to-reject',
|
|
160
|
-
session_id: 'sess5',
|
|
161
|
-
tool_name: 'write_file',
|
|
162
|
-
input_payload: JSON.stringify({ path: '/tmp/y.txt', content: 'y' }),
|
|
163
|
-
status: 'pending',
|
|
164
|
-
decision: 'pending',
|
|
165
|
-
});
|
|
166
|
-
const result = await rejectPendingAction('to-reject', 'user said no');
|
|
167
|
-
expect(result.success).toBe(true);
|
|
168
|
-
const row = getAction('to-reject');
|
|
169
|
-
expect(row?.status).toBe('rejected');
|
|
170
|
-
expect(row?.rejected_reason).toBe('user said no');
|
|
171
|
-
});
|
|
172
|
-
});
|