delimit-cli 1.0.0 → 2.1.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.
- package/.github/workflows/api-governance.yml +24 -0
- package/README.md +57 -115
- package/adapters/codex-skill.js +87 -0
- package/adapters/cursor-extension.js +190 -0
- package/adapters/gemini-action.js +93 -0
- package/adapters/openai-function.js +112 -0
- package/adapters/xai-plugin.js +151 -0
- package/bin/delimit-cli.js +921 -0
- package/bin/delimit.js +237 -1
- package/delimit.yml +19 -0
- package/hooks/evidence-status.sh +12 -0
- package/hooks/git/commit-msg +4 -0
- package/hooks/git/pre-commit +4 -0
- package/hooks/git/pre-push +4 -0
- package/hooks/install-hooks.sh +583 -0
- package/hooks/message-auth-hook.js +9 -0
- package/hooks/message-governance-hook.js +9 -0
- package/hooks/models/claude-post.js +4 -0
- package/hooks/models/claude-pre.js +4 -0
- package/hooks/models/codex-post.js +4 -0
- package/hooks/models/codex-pre.js +4 -0
- package/hooks/models/cursor-post.js +4 -0
- package/hooks/models/cursor-pre.js +4 -0
- package/hooks/models/gemini-post.js +4 -0
- package/hooks/models/gemini-pre.js +4 -0
- package/hooks/models/openai-post.js +4 -0
- package/hooks/models/openai-pre.js +4 -0
- package/hooks/models/windsurf-post.js +4 -0
- package/hooks/models/windsurf-pre.js +4 -0
- package/hooks/models/xai-post.js +4 -0
- package/hooks/models/xai-pre.js +4 -0
- package/hooks/post-bash-hook.js +13 -0
- package/hooks/post-mcp-hook.js +13 -0
- package/hooks/post-response-hook.js +4 -0
- package/hooks/post-tool-hook.js +126 -0
- package/hooks/post-write-hook.js +13 -0
- package/hooks/pre-bash-hook.js +30 -0
- package/hooks/pre-mcp-hook.js +13 -0
- package/hooks/pre-read-hook.js +13 -0
- package/hooks/pre-search-hook.js +13 -0
- package/hooks/pre-submit-hook.js +4 -0
- package/hooks/pre-task-hook.js +13 -0
- package/hooks/pre-tool-hook.js +121 -0
- package/hooks/pre-web-hook.js +13 -0
- package/hooks/pre-write-hook.js +31 -0
- package/hooks/test-hooks.sh +12 -0
- package/hooks/update-delimit.sh +6 -0
- package/lib/agent.js +509 -0
- package/lib/api-engine.js +156 -0
- package/lib/auth-setup.js +891 -0
- package/lib/decision-engine.js +474 -0
- package/lib/hooks-installer.js +416 -0
- package/lib/platform-adapters.js +353 -0
- package/lib/proxy-handler.js +114 -0
- package/package.json +38 -30
- package/scripts/infect.js +128 -0
- package/test-decision-engine.js +181 -0
- package/test-hook.js +27 -0
- package/dist/commands/validate.d.ts +0 -2
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/validate.js +0 -106
- package/dist/commands/validate.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -71
- package/dist/index.js.map +0 -1
- package/dist/types/index.d.ts +0 -39
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -3
- package/dist/types/index.js.map +0 -1
- package/dist/utils/api.d.ts +0 -3
- package/dist/utils/api.d.ts.map +0 -1
- package/dist/utils/api.js +0 -64
- package/dist/utils/api.js.map +0 -1
- package/dist/utils/file.d.ts +0 -7
- package/dist/utils/file.d.ts.map +0 -1
- package/dist/utils/file.js +0 -69
- package/dist/utils/file.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -14
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -28
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/masker.d.ts +0 -14
- package/dist/utils/masker.d.ts.map +0 -1
- package/dist/utils/masker.js +0 -89
- package/dist/utils/masker.js.map +0 -1
- package/src/commands/validate.ts +0 -150
- package/src/index.ts +0 -80
- package/src/types/index.ts +0 -41
- package/src/utils/api.ts +0 -68
- package/src/utils/file.ts +0 -71
- package/src/utils/logger.ts +0 -27
- package/src/utils/masker.ts +0 -101
- package/test-sensitive.yaml +0 -109
- package/tsconfig.json +0 -23
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
class DecisionEngine {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.lastDecision = null;
|
|
4
|
+
this.decisionHistory = [];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
makeDecision(context, policies, sessionMode) {
|
|
8
|
+
const decision = {
|
|
9
|
+
timestamp: new Date().toISOString(),
|
|
10
|
+
id: this.generateId(),
|
|
11
|
+
|
|
12
|
+
// Modes
|
|
13
|
+
configuredMode: sessionMode,
|
|
14
|
+
defaultMode: policies.defaultMode || 'advisory',
|
|
15
|
+
effectiveMode: null,
|
|
16
|
+
|
|
17
|
+
// Reasoning
|
|
18
|
+
matchedRules: [],
|
|
19
|
+
escalationPath: [],
|
|
20
|
+
policySource: this.getPolicySources(policies),
|
|
21
|
+
|
|
22
|
+
// Decision
|
|
23
|
+
action: null,
|
|
24
|
+
message: null,
|
|
25
|
+
|
|
26
|
+
// Explanation
|
|
27
|
+
explanation: {
|
|
28
|
+
why: null,
|
|
29
|
+
trigger: null,
|
|
30
|
+
rule: null,
|
|
31
|
+
source: null
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Context
|
|
35
|
+
context: {
|
|
36
|
+
command: context.command,
|
|
37
|
+
pwd: context.pwd,
|
|
38
|
+
gitBranch: context.gitBranch,
|
|
39
|
+
files: context.files || [],
|
|
40
|
+
user: process.env.USER
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Start with base mode
|
|
45
|
+
let currentMode = sessionMode === 'auto' ?
|
|
46
|
+
(policies.defaultMode || 'advisory') :
|
|
47
|
+
sessionMode;
|
|
48
|
+
|
|
49
|
+
decision.escalationPath.push({
|
|
50
|
+
mode: currentMode,
|
|
51
|
+
reason: sessionMode === 'auto' ?
|
|
52
|
+
`Default mode from ${this.findPolicySource(policies, 'defaultMode')}` :
|
|
53
|
+
'User-configured session mode'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Evaluate all rules
|
|
57
|
+
if (policies.rules) {
|
|
58
|
+
for (const rule of policies.rules) {
|
|
59
|
+
const match = this.evaluateRule(rule, context);
|
|
60
|
+
if (match) {
|
|
61
|
+
decision.matchedRules.push({
|
|
62
|
+
name: rule.name,
|
|
63
|
+
mode: rule.mode,
|
|
64
|
+
trigger: match.trigger,
|
|
65
|
+
triggerType: match.type,
|
|
66
|
+
source: match.source || 'local'
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Check if this rule escalates
|
|
70
|
+
if (this.isStrongerMode(rule.mode, currentMode)) {
|
|
71
|
+
currentMode = rule.mode;
|
|
72
|
+
decision.escalationPath.push({
|
|
73
|
+
mode: rule.mode,
|
|
74
|
+
reason: `Rule "${rule.name}" matched`,
|
|
75
|
+
trigger: match.trigger,
|
|
76
|
+
source: match.source
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (rule.final) {
|
|
81
|
+
decision.explanation.why = `Final rule "${rule.name}" matched`;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
decision.effectiveMode = currentMode;
|
|
89
|
+
|
|
90
|
+
// Build explanation
|
|
91
|
+
if (decision.matchedRules.length > 0) {
|
|
92
|
+
const strongestRule = decision.matchedRules.reduce((a, b) =>
|
|
93
|
+
this.isStrongerMode(b.mode, a.mode) ? b : a
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
decision.explanation = {
|
|
97
|
+
why: `Mode escalated to ${currentMode.toUpperCase()}`,
|
|
98
|
+
trigger: strongestRule.trigger,
|
|
99
|
+
rule: strongestRule.name,
|
|
100
|
+
source: strongestRule.source || this.findPolicySource(policies, 'rules')
|
|
101
|
+
};
|
|
102
|
+
} else {
|
|
103
|
+
decision.explanation = {
|
|
104
|
+
why: `Using ${currentMode} mode`,
|
|
105
|
+
trigger: 'No matching rules',
|
|
106
|
+
rule: null,
|
|
107
|
+
source: sessionMode === 'auto' ?
|
|
108
|
+
this.findPolicySource(policies, 'defaultMode') :
|
|
109
|
+
'user configuration'
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Determine action based on effective mode
|
|
114
|
+
switch (currentMode) {
|
|
115
|
+
case 'advisory':
|
|
116
|
+
decision.action = 'allow';
|
|
117
|
+
decision.message = this.formatMessage('advisory', decision);
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case 'guarded':
|
|
121
|
+
decision.action = 'prompt';
|
|
122
|
+
decision.message = this.formatMessage('guarded', decision);
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
case 'enforce':
|
|
126
|
+
decision.action = 'block';
|
|
127
|
+
decision.message = this.formatMessage('enforce', decision);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Store for later retrieval
|
|
132
|
+
this.lastDecision = decision;
|
|
133
|
+
this.decisionHistory.push(decision);
|
|
134
|
+
if (this.decisionHistory.length > 100) {
|
|
135
|
+
this.decisionHistory.shift();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return decision;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
evaluateRule(rule, context) {
|
|
142
|
+
if (!rule.triggers) return null;
|
|
143
|
+
|
|
144
|
+
for (const trigger of rule.triggers) {
|
|
145
|
+
// Check path patterns
|
|
146
|
+
if (trigger.path && context.files) {
|
|
147
|
+
for (const file of context.files) {
|
|
148
|
+
if (this.matchesPattern(file, trigger.path)) {
|
|
149
|
+
return {
|
|
150
|
+
type: 'path',
|
|
151
|
+
trigger: `path matches "${trigger.path}"`,
|
|
152
|
+
matched: file,
|
|
153
|
+
source: rule.source
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check content patterns
|
|
160
|
+
if (trigger.content && context.diff) {
|
|
161
|
+
for (const pattern of trigger.content) {
|
|
162
|
+
if (context.diff.includes(pattern)) {
|
|
163
|
+
return {
|
|
164
|
+
type: 'content',
|
|
165
|
+
trigger: `content contains "${pattern}"`,
|
|
166
|
+
matched: pattern,
|
|
167
|
+
source: rule.source
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check git branch
|
|
174
|
+
if (trigger.gitBranch && context.gitBranch) {
|
|
175
|
+
if (trigger.gitBranch.includes(context.gitBranch)) {
|
|
176
|
+
return {
|
|
177
|
+
type: 'branch',
|
|
178
|
+
trigger: `branch is "${context.gitBranch}"`,
|
|
179
|
+
matched: context.gitBranch,
|
|
180
|
+
source: rule.source
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check command patterns
|
|
186
|
+
if (trigger.command && context.command) {
|
|
187
|
+
if (this.matchesPattern(context.command, trigger.command)) {
|
|
188
|
+
return {
|
|
189
|
+
type: 'command',
|
|
190
|
+
trigger: `command matches "${trigger.command}"`,
|
|
191
|
+
matched: context.command,
|
|
192
|
+
source: rule.source
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
formatMessage(mode, decision) {
|
|
202
|
+
const colors = {
|
|
203
|
+
advisory: '\x1b[34m', // blue
|
|
204
|
+
guarded: '\x1b[33m', // yellow
|
|
205
|
+
enforce: '\x1b[31m' // red
|
|
206
|
+
};
|
|
207
|
+
const reset = '\x1b[0m';
|
|
208
|
+
const bold = '\x1b[1m';
|
|
209
|
+
|
|
210
|
+
let msg = `${colors[mode]}${bold}[Delimit ${mode.toUpperCase()}]${reset}\n`;
|
|
211
|
+
|
|
212
|
+
if (decision.matchedRules.length > 0) {
|
|
213
|
+
const rule = decision.matchedRules[0];
|
|
214
|
+
msg += `📋 Rule: "${rule.name}"\n`;
|
|
215
|
+
msg += `🎯 Trigger: ${rule.trigger}\n`;
|
|
216
|
+
msg += `📁 Policy: ${rule.source || 'local'}\n`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (decision.configuredMode !== decision.effectiveMode) {
|
|
220
|
+
msg += `⚡ Mode escalated from ${decision.configuredMode} → ${decision.effectiveMode}\n`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Add specific reason and action guidance
|
|
224
|
+
switch (mode) {
|
|
225
|
+
case 'advisory':
|
|
226
|
+
msg += `✅ Proceeding with standard checks\n`;
|
|
227
|
+
msg += `💡 Recommendation: Review changes before committing`;
|
|
228
|
+
break;
|
|
229
|
+
case 'guarded':
|
|
230
|
+
msg += `⚠️ Confirmation required to proceed\n`;
|
|
231
|
+
msg += `💡 To proceed: Confirm when prompted, or use --force flag\n`;
|
|
232
|
+
msg += `💡 To avoid: Switch to a feature branch or request override`;
|
|
233
|
+
break;
|
|
234
|
+
case 'enforce':
|
|
235
|
+
msg += `🛑 BLOCKED: ${this.getBlockReason(decision)}\n`;
|
|
236
|
+
msg += `\n💡 HOW TO PROCEED:\n`;
|
|
237
|
+
if (decision.context.gitBranch && ['main', 'master', 'production'].includes(decision.context.gitBranch)) {
|
|
238
|
+
msg += ` 1. Switch to a feature branch: git checkout -b feature/your-change\n`;
|
|
239
|
+
msg += ` 2. Commit your changes there\n`;
|
|
240
|
+
msg += ` 3. Create a pull request for review\n`;
|
|
241
|
+
}
|
|
242
|
+
if (decision.matchedRules.some(r => r.trigger.includes('payment'))) {
|
|
243
|
+
msg += ` 1. Payment code requires security review\n`;
|
|
244
|
+
msg += ` 2. Request review from security team\n`;
|
|
245
|
+
msg += ` 3. Use approved payment SDK methods only\n`;
|
|
246
|
+
}
|
|
247
|
+
msg += `\n📖 Policy location: ${this.getPolicyLocation(decision)}`;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return msg;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
getBlockReason(decision) {
|
|
255
|
+
if (decision.matchedRules.length === 0) {
|
|
256
|
+
return 'Enforce mode is active (no specific rule matched)';
|
|
257
|
+
}
|
|
258
|
+
const rule = decision.matchedRules[0];
|
|
259
|
+
if (rule.name === 'Production Protection') {
|
|
260
|
+
return `Direct commits to ${decision.context.gitBranch} branch are prohibited`;
|
|
261
|
+
}
|
|
262
|
+
if (rule.name === 'Payment Code Security') {
|
|
263
|
+
return 'Payment/billing code changes require security review';
|
|
264
|
+
}
|
|
265
|
+
return `Rule "${rule.name}" prohibits this action`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
getPolicyLocation(decision) {
|
|
269
|
+
const sources = [];
|
|
270
|
+
if (decision.matchedRules.length > 0) {
|
|
271
|
+
const rule = decision.matchedRules[0];
|
|
272
|
+
if (rule.source === 'project policy') {
|
|
273
|
+
sources.push('./delimit.yml');
|
|
274
|
+
} else if (rule.source === 'user policy') {
|
|
275
|
+
sources.push('~/.config/delimit/delimit.yml');
|
|
276
|
+
} else if (rule.source === 'org policy') {
|
|
277
|
+
sources.push('Organization policy (contact admin)');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return sources.length > 0 ? sources.join(', ') : 'delimit.yml (default location)';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
explainDecision(decisionId) {
|
|
284
|
+
const decision = decisionId ?
|
|
285
|
+
this.decisionHistory.find(d => d.id === decisionId) :
|
|
286
|
+
this.lastDecision;
|
|
287
|
+
|
|
288
|
+
if (!decision) {
|
|
289
|
+
return 'No decision found';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let explanation = '\n📊 GOVERNANCE DECISION EXPLANATION\n';
|
|
293
|
+
explanation += '═══════════════════════════════════\n\n';
|
|
294
|
+
|
|
295
|
+
explanation += `Decision ID: ${decision.id}\n`;
|
|
296
|
+
explanation += `Timestamp: ${decision.timestamp}\n\n`;
|
|
297
|
+
|
|
298
|
+
// PRIMARY REASON - Clear and upfront
|
|
299
|
+
explanation += '❌ WHY BLOCKED\n';
|
|
300
|
+
if (decision.action === 'block') {
|
|
301
|
+
explanation += this.getDetailedBlockReason(decision) + '\n\n';
|
|
302
|
+
} else if (decision.action === 'prompt') {
|
|
303
|
+
explanation += `Confirmation required: ${decision.explanation.why}\n\n`;
|
|
304
|
+
} else {
|
|
305
|
+
explanation += `Allowed: ${decision.explanation.why}\n\n`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// HOW TO PROCEED
|
|
309
|
+
if (decision.action !== 'allow') {
|
|
310
|
+
explanation += '✅ HOW TO PROCEED\n';
|
|
311
|
+
explanation += this.getActionGuidance(decision) + '\n';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// TRIGGERING CONTEXT
|
|
315
|
+
explanation += '📍 WHAT TRIGGERED THIS\n';
|
|
316
|
+
if (decision.context.files && decision.context.files.length > 0) {
|
|
317
|
+
explanation += `├─ Files modified:\n`;
|
|
318
|
+
decision.context.files.slice(0, 5).forEach(f => {
|
|
319
|
+
explanation += `│ • ${f}\n`;
|
|
320
|
+
});
|
|
321
|
+
if (decision.context.files.length > 5) {
|
|
322
|
+
explanation += `│ • ... and ${decision.context.files.length - 5} more\n`;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
explanation += `├─ Branch: ${decision.context.gitBranch || 'unknown'}\n`;
|
|
326
|
+
explanation += `├─ Directory: ${decision.context.pwd}\n`;
|
|
327
|
+
explanation += `└─ User: ${decision.context.user}\n\n`;
|
|
328
|
+
|
|
329
|
+
// POLICY SOURCE
|
|
330
|
+
explanation += '📖 POLICY DETAILS\n';
|
|
331
|
+
if (decision.matchedRules.length > 0) {
|
|
332
|
+
const rule = decision.matchedRules[0];
|
|
333
|
+
explanation += `├─ Rule: "${rule.name}"\n`;
|
|
334
|
+
explanation += `├─ Location: ${this.getPolicyFileLocation(rule.source)}\n`;
|
|
335
|
+
explanation += `├─ Trigger type: ${rule.triggerType}\n`;
|
|
336
|
+
explanation += `└─ Exact trigger: ${rule.trigger}\n`;
|
|
337
|
+
} else {
|
|
338
|
+
explanation += `├─ No specific rule matched\n`;
|
|
339
|
+
explanation += `└─ Using default mode: ${decision.defaultMode}\n`;
|
|
340
|
+
}
|
|
341
|
+
explanation += '\n';
|
|
342
|
+
|
|
343
|
+
// MODE DETAILS (less prominent)
|
|
344
|
+
explanation += '🎚️ MODE DETAILS\n';
|
|
345
|
+
explanation += `├─ Session mode: ${decision.configuredMode}\n`;
|
|
346
|
+
explanation += `├─ Default mode: ${decision.defaultMode}\n`;
|
|
347
|
+
explanation += `└─ Effective mode: ${decision.effectiveMode}\n`;
|
|
348
|
+
|
|
349
|
+
if (decision.escalationPath.length > 1) {
|
|
350
|
+
explanation += '\n📈 ESCALATION HISTORY\n';
|
|
351
|
+
decision.escalationPath.forEach((step, i) => {
|
|
352
|
+
const prefix = i === decision.escalationPath.length - 1 ? '└─' : '├─';
|
|
353
|
+
explanation += `${prefix} ${step.mode}: ${step.reason}\n`;
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// AUDIT TRAIL
|
|
358
|
+
explanation += '\n🔍 AUDIT\n';
|
|
359
|
+
explanation += `├─ Decision ID: ${decision.id}\n`;
|
|
360
|
+
explanation += `├─ Timestamp: ${decision.timestamp}\n`;
|
|
361
|
+
explanation += `└─ Audit log: ~/.delimit/audit/${new Date(decision.timestamp).toISOString().split('T')[0]}.jsonl\n`;
|
|
362
|
+
|
|
363
|
+
return explanation;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
getDetailedBlockReason(decision) {
|
|
367
|
+
if (decision.matchedRules.length === 0) {
|
|
368
|
+
return 'Enforce mode is active but no specific rule matched.\nThis suggests a configuration issue.';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const rule = decision.matchedRules[0];
|
|
372
|
+
let reason = '';
|
|
373
|
+
|
|
374
|
+
if (rule.name === 'Production Protection') {
|
|
375
|
+
reason = `Direct commits to the ${decision.context.gitBranch} branch are prohibited.\n`;
|
|
376
|
+
reason += `This branch is protected to prevent accidental production changes.`;
|
|
377
|
+
} else if (rule.name === 'Payment Code Security') {
|
|
378
|
+
reason = `Changes to payment/billing code require security review.\n`;
|
|
379
|
+
reason += `Files matching "${rule.trigger}" triggered this protection.`;
|
|
380
|
+
} else if (rule.name === 'AI-Generated Code Review') {
|
|
381
|
+
reason = `AI-generated code requires human review before committing.\n`;
|
|
382
|
+
reason += `Detected by: ${rule.trigger}`;
|
|
383
|
+
} else {
|
|
384
|
+
reason = `Rule "${rule.name}" prohibits this action.\n`;
|
|
385
|
+
reason += `Trigger: ${rule.trigger}`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return reason;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
getActionGuidance(decision) {
|
|
392
|
+
let guidance = '';
|
|
393
|
+
|
|
394
|
+
if (decision.action === 'block') {
|
|
395
|
+
if (decision.context.gitBranch && ['main', 'master', 'production'].includes(decision.context.gitBranch)) {
|
|
396
|
+
guidance += `1. Switch to a feature branch:\n`;
|
|
397
|
+
guidance += ` git checkout -b feature/your-change\n`;
|
|
398
|
+
guidance += `2. Commit your changes there\n`;
|
|
399
|
+
guidance += `3. Create a pull request for review\n`;
|
|
400
|
+
} else if (decision.matchedRules.some(r => r.trigger && r.trigger.includes('payment'))) {
|
|
401
|
+
guidance += `1. Request security review for payment code\n`;
|
|
402
|
+
guidance += `2. Use approved payment SDK methods\n`;
|
|
403
|
+
guidance += `3. Add security tests for payment flows\n`;
|
|
404
|
+
} else {
|
|
405
|
+
guidance += `1. Review the policy rules in ${this.getPolicyFileLocation()}\n`;
|
|
406
|
+
guidance += `2. Adjust your changes to comply\n`;
|
|
407
|
+
guidance += `3. Or request an exception from the policy owner\n`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (decision.effectiveMode === 'enforce') {
|
|
411
|
+
guidance += `\nNote: Enforce mode cannot be overridden locally.\n`;
|
|
412
|
+
guidance += `Contact your administrator for exceptions.\n`;
|
|
413
|
+
}
|
|
414
|
+
} else if (decision.action === 'prompt') {
|
|
415
|
+
guidance += `• Type 'y' to proceed with the action\n`;
|
|
416
|
+
guidance += `• Type 'n' to cancel\n`;
|
|
417
|
+
guidance += `• Or use --force flag to skip confirmation\n`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return guidance;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
getPolicyFileLocation(source) {
|
|
424
|
+
if (!source) return './delimit.yml';
|
|
425
|
+
if (source === 'project policy') return './delimit.yml';
|
|
426
|
+
if (source === 'user policy') return '~/.config/delimit/delimit.yml';
|
|
427
|
+
if (source === 'system policy') return '/etc/delimit/delimit.yml';
|
|
428
|
+
if (source === 'org policy') return 'Organization policy (contact admin)';
|
|
429
|
+
if (source === 'defaults') return 'Built-in defaults';
|
|
430
|
+
// Handle rule-specific sources with paths
|
|
431
|
+
if (source && source._sourcePath) return source._sourcePath;
|
|
432
|
+
return source;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
isStrongerMode(mode1, mode2) {
|
|
436
|
+
const strength = { advisory: 1, guarded: 2, enforce: 3 };
|
|
437
|
+
return strength[mode1] > strength[mode2];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
matchesPattern(str, pattern) {
|
|
441
|
+
// Simple glob matching - in production use minimatch
|
|
442
|
+
const regex = pattern
|
|
443
|
+
.replace(/\*/g, '.*')
|
|
444
|
+
.replace(/\?/g, '.');
|
|
445
|
+
return new RegExp(`^${regex}$`).test(str);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
getPolicySources(policies) {
|
|
449
|
+
const sources = [];
|
|
450
|
+
if (policies._sources) {
|
|
451
|
+
if (policies._sources.org) sources.push('org');
|
|
452
|
+
if (policies._sources.user) sources.push('user');
|
|
453
|
+
if (policies._sources.project) sources.push('project');
|
|
454
|
+
}
|
|
455
|
+
return sources.length > 0 ? sources : ['local'];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
findPolicySource(policies, field) {
|
|
459
|
+
// Handle the new policy structure with defaultModeSource
|
|
460
|
+
if (field === 'defaultMode' && policies.defaultModeSource) {
|
|
461
|
+
return policies.defaultModeSource;
|
|
462
|
+
}
|
|
463
|
+
if (policies._sources && policies._sources[field]) {
|
|
464
|
+
return policies._sources[field];
|
|
465
|
+
}
|
|
466
|
+
return 'defaults';
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
generateId() {
|
|
470
|
+
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
module.exports = DecisionEngine;
|