delimit-cli 1.0.0 → 2.1.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/.github/workflows/api-governance.yml +43 -0
- package/README.md +70 -113
- 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
package/lib/agent.js
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const yaml = require('js-yaml');
|
|
7
|
+
const minimatch = require('minimatch');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
const DecisionEngine = require('./decision-engine');
|
|
10
|
+
|
|
11
|
+
class DelimitAgent {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.app = express();
|
|
14
|
+
this.app.use(express.json());
|
|
15
|
+
|
|
16
|
+
// State
|
|
17
|
+
this.sessionMode = 'auto';
|
|
18
|
+
this.globalPolicies = {}; // Cached global/user/org policies
|
|
19
|
+
this.projectPolicyCache = new Map(); // Cache for project-specific policies
|
|
20
|
+
this.auditLog = [];
|
|
21
|
+
this.port = process.env.DELIMIT_AGENT_PORT || 7823;
|
|
22
|
+
this.decisionEngine = new DecisionEngine();
|
|
23
|
+
this.startTime = Date.now();
|
|
24
|
+
this.cacheExpiry = 5 * 60 * 1000; // 5 minute cache for project policies
|
|
25
|
+
|
|
26
|
+
// Setup routes
|
|
27
|
+
this.setupRoutes();
|
|
28
|
+
|
|
29
|
+
// Load global policies only
|
|
30
|
+
this.loadGlobalPolicies();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setupRoutes() {
|
|
34
|
+
// Evaluate governance for an action
|
|
35
|
+
this.app.post('/evaluate', (req, res) => {
|
|
36
|
+
try {
|
|
37
|
+
const context = req.body;
|
|
38
|
+
const decision = this.evaluateGovernance(context);
|
|
39
|
+
this.logDecision(decision, context);
|
|
40
|
+
res.json(decision);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Error in /evaluate:', error);
|
|
43
|
+
res.status(500).json({ error: 'Internal server error', action: 'allow' });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Set session mode
|
|
48
|
+
this.app.post('/mode', (req, res) => {
|
|
49
|
+
this.sessionMode = req.body.mode;
|
|
50
|
+
res.json({ success: true, mode: this.sessionMode });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Get status
|
|
54
|
+
this.app.get('/status', (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
// Create dummy context for status check
|
|
57
|
+
const dummyContext = { pwd: process.cwd() };
|
|
58
|
+
const resolvedPolicies = this.resolvePoliciesForContext(dummyContext);
|
|
59
|
+
const mergedPolicy = this.mergePoliciesWithSources(resolvedPolicies);
|
|
60
|
+
|
|
61
|
+
const recentDecisions = this.auditLog.slice(-5).map(d => ({
|
|
62
|
+
timestamp: new Date(d.timestamp).toLocaleTimeString(),
|
|
63
|
+
mode: d.mode,
|
|
64
|
+
action: d.action,
|
|
65
|
+
rule: d.rule
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
res.json({
|
|
69
|
+
sessionMode: this.sessionMode,
|
|
70
|
+
defaultMode: mergedPolicy.defaultMode || 'advisory',
|
|
71
|
+
effectiveMode: this.decisionEngine.lastDecision?.effectiveMode,
|
|
72
|
+
policiesLoaded: Object.keys(this.globalPolicies),
|
|
73
|
+
totalRules: mergedPolicy.rules?.length || 0,
|
|
74
|
+
auditLogSize: this.auditLog.length,
|
|
75
|
+
lastDecision: this.decisionEngine.lastDecision ? {
|
|
76
|
+
timestamp: this.decisionEngine.lastDecision.timestamp,
|
|
77
|
+
action: this.decisionEngine.lastDecision.action
|
|
78
|
+
} : null,
|
|
79
|
+
recentDecisions: recentDecisions,
|
|
80
|
+
uptime: process.uptime()
|
|
81
|
+
});
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Error in /status:', error);
|
|
84
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Get recent audit log
|
|
89
|
+
this.app.get('/audit', (req, res) => {
|
|
90
|
+
res.json(this.auditLog.slice(-100));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Explain a decision
|
|
94
|
+
this.app.get('/explain/:id', (req, res) => {
|
|
95
|
+
const id = req.params.id;
|
|
96
|
+
let explanation;
|
|
97
|
+
|
|
98
|
+
if (id === 'last') {
|
|
99
|
+
explanation = this.decisionEngine.explainDecision();
|
|
100
|
+
} else {
|
|
101
|
+
explanation = this.decisionEngine.explainDecision(id);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (explanation === 'No decision found') {
|
|
105
|
+
res.status(404).json({ error: 'Decision not found' });
|
|
106
|
+
} else {
|
|
107
|
+
res.json({ explanation });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
loadGlobalPolicies() {
|
|
113
|
+
this.globalPolicies = {};
|
|
114
|
+
|
|
115
|
+
// Load organization policy from environment or config
|
|
116
|
+
const orgPolicyUrl = process.env.DELIMIT_ORG_POLICY_URL;
|
|
117
|
+
if (orgPolicyUrl) {
|
|
118
|
+
// TODO: Implement remote org policy fetching
|
|
119
|
+
console.log('[Agent] Org policy URL configured:', orgPolicyUrl);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Load user policy from standard location
|
|
123
|
+
const userPolicyPath = path.join(process.env.HOME, '.config', 'delimit', 'delimit.yml');
|
|
124
|
+
if (fs.existsSync(userPolicyPath)) {
|
|
125
|
+
try {
|
|
126
|
+
this.globalPolicies.user = {
|
|
127
|
+
policy: yaml.load(fs.readFileSync(userPolicyPath, 'utf8')),
|
|
128
|
+
path: userPolicyPath,
|
|
129
|
+
loadedAt: Date.now()
|
|
130
|
+
};
|
|
131
|
+
console.log('[Agent] Loaded user policy from', userPolicyPath);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
console.error('[Agent] Failed to load user policy:', e.message);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Load system-wide default policy if exists
|
|
138
|
+
const systemPolicyPath = '/etc/delimit/delimit.yml';
|
|
139
|
+
if (fs.existsSync(systemPolicyPath)) {
|
|
140
|
+
try {
|
|
141
|
+
this.globalPolicies.system = {
|
|
142
|
+
policy: yaml.load(fs.readFileSync(systemPolicyPath, 'utf8')),
|
|
143
|
+
path: systemPolicyPath,
|
|
144
|
+
loadedAt: Date.now()
|
|
145
|
+
};
|
|
146
|
+
console.log('[Agent] Loaded system policy from', systemPolicyPath);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
console.error('[Agent] Failed to load system policy:', e.message);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
evaluateGovernance(context) {
|
|
154
|
+
// Resolve policies for this specific context
|
|
155
|
+
const resolvedPolicies = this.resolvePoliciesForContext(context);
|
|
156
|
+
|
|
157
|
+
// Check for malformed project policy BEFORE merging
|
|
158
|
+
if (resolvedPolicies.project?.error) {
|
|
159
|
+
// Project policy is malformed - this is a critical error
|
|
160
|
+
// Fail closed: block the action and report the error
|
|
161
|
+
console.error('[Agent] CRITICAL: Project policy is malformed, failing closed');
|
|
162
|
+
return {
|
|
163
|
+
timestamp: new Date().toISOString(),
|
|
164
|
+
mode: 'enforce',
|
|
165
|
+
rule: 'MALFORMED_POLICY',
|
|
166
|
+
action: 'block',
|
|
167
|
+
message: `🛑 GOVERNANCE ERROR: Project policy at ${resolvedPolicies.project.path} is malformed\n` +
|
|
168
|
+
`Error: ${resolvedPolicies.project.error}\n` +
|
|
169
|
+
`Action blocked until policy is fixed.\n` +
|
|
170
|
+
`Fix the policy file or remove it to proceed.`,
|
|
171
|
+
requiresOverride: true,
|
|
172
|
+
error: true,
|
|
173
|
+
policyError: resolvedPolicies.project.error
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Merge policies in correct precedence order
|
|
178
|
+
const mergedPolicy = this.mergePoliciesWithSources(resolvedPolicies);
|
|
179
|
+
|
|
180
|
+
// The merged policy now contains accurate source information
|
|
181
|
+
// No need to re-add it since mergePoliciesWithSources handles it correctly
|
|
182
|
+
|
|
183
|
+
// Use the DecisionEngine for evaluation
|
|
184
|
+
const decision = this.decisionEngine.makeDecision(context, mergedPolicy, this.sessionMode);
|
|
185
|
+
|
|
186
|
+
// Convert to legacy format for backward compatibility
|
|
187
|
+
return {
|
|
188
|
+
timestamp: decision.timestamp,
|
|
189
|
+
mode: decision.effectiveMode,
|
|
190
|
+
rule: decision.matchedRules.length > 0 ? decision.matchedRules[0].name : null,
|
|
191
|
+
action: decision.action,
|
|
192
|
+
message: decision.message,
|
|
193
|
+
requiresOverride: decision.action === 'block' &&
|
|
194
|
+
!mergedPolicy.overrides?.allowEnforceOverride,
|
|
195
|
+
policySource: decision.explanation?.source || mergedPolicy.defaultModeSource,
|
|
196
|
+
precedenceOrder: mergedPolicy._precedenceOrder
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
ruleMatches(rule, context) {
|
|
201
|
+
if (!rule.triggers) return false;
|
|
202
|
+
|
|
203
|
+
for (const trigger of rule.triggers) {
|
|
204
|
+
let matches = true;
|
|
205
|
+
|
|
206
|
+
// Check path patterns
|
|
207
|
+
if (trigger.path && context.files) {
|
|
208
|
+
const pathMatches = context.files.some(file =>
|
|
209
|
+
minimatch(file, trigger.path)
|
|
210
|
+
);
|
|
211
|
+
matches = matches && pathMatches;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check content patterns
|
|
215
|
+
if (trigger.content && context.diff) {
|
|
216
|
+
const contentMatches = trigger.content.some(pattern =>
|
|
217
|
+
context.diff.includes(pattern)
|
|
218
|
+
);
|
|
219
|
+
matches = matches && contentMatches;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check command patterns
|
|
223
|
+
if (trigger.command && context.command) {
|
|
224
|
+
matches = matches && minimatch(context.command, trigger.command);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check git branch
|
|
228
|
+
if (trigger.gitBranch && context.gitBranch) {
|
|
229
|
+
matches = matches && trigger.gitBranch.includes(context.gitBranch);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check commit message
|
|
233
|
+
if (trigger.commitMessage && context.commitMessage) {
|
|
234
|
+
const regex = new RegExp(trigger.commitMessage);
|
|
235
|
+
matches = matches && regex.test(context.commitMessage);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (matches) return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
getStrongerMode(mode1, mode2) {
|
|
245
|
+
const strength = { advisory: 1, guarded: 2, enforce: 3 };
|
|
246
|
+
return strength[mode2] > strength[mode1] ? mode2 : mode1;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
makeDecision(mode, rule, context, policy) {
|
|
250
|
+
const decision = {
|
|
251
|
+
timestamp: new Date().toISOString(),
|
|
252
|
+
mode: mode,
|
|
253
|
+
rule: rule ? rule.name : null,
|
|
254
|
+
action: 'allow',
|
|
255
|
+
message: '',
|
|
256
|
+
requiresOverride: false
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
switch (mode) {
|
|
260
|
+
case 'advisory':
|
|
261
|
+
decision.action = 'allow';
|
|
262
|
+
decision.message = rule ?
|
|
263
|
+
`[Advisory] ${rule.name} policy triggered` :
|
|
264
|
+
'[Advisory] Proceeding with standard checks';
|
|
265
|
+
break;
|
|
266
|
+
|
|
267
|
+
case 'guarded':
|
|
268
|
+
decision.action = 'prompt';
|
|
269
|
+
decision.message = rule ?
|
|
270
|
+
`[Guarded] ${rule.name} policy requires confirmation` :
|
|
271
|
+
'[Guarded] Action requires confirmation';
|
|
272
|
+
decision.requiresOverride = true;
|
|
273
|
+
break;
|
|
274
|
+
|
|
275
|
+
case 'enforce':
|
|
276
|
+
decision.action = 'block';
|
|
277
|
+
decision.message = rule ?
|
|
278
|
+
`[Enforce] Blocked by ${rule.name} policy` :
|
|
279
|
+
'[Enforce] Action blocked by governance';
|
|
280
|
+
decision.requiresOverride = !policy.overrides?.allowEnforceOverride;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return decision;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
resolvePoliciesForContext(context) {
|
|
288
|
+
const policies = {
|
|
289
|
+
org: this.globalPolicies.org || null,
|
|
290
|
+
system: this.globalPolicies.system || null,
|
|
291
|
+
user: this.globalPolicies.user || null,
|
|
292
|
+
project: null
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Resolve project policy for this specific context
|
|
296
|
+
if (context.pwd) {
|
|
297
|
+
policies.project = this.getProjectPolicy(context.pwd);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return policies;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
getProjectPolicy(projectPath) {
|
|
304
|
+
// Check cache first
|
|
305
|
+
const cacheKey = projectPath;
|
|
306
|
+
const cached = this.projectPolicyCache.get(cacheKey);
|
|
307
|
+
|
|
308
|
+
if (cached && (Date.now() - cached.loadedAt) < this.cacheExpiry) {
|
|
309
|
+
return cached;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Load project policy from the actual project directory
|
|
313
|
+
const policyPath = path.join(projectPath, 'delimit.yml');
|
|
314
|
+
let projectPolicy = null;
|
|
315
|
+
|
|
316
|
+
if (fs.existsSync(policyPath)) {
|
|
317
|
+
try {
|
|
318
|
+
const stat = fs.statSync(policyPath);
|
|
319
|
+
const policyContent = fs.readFileSync(policyPath, 'utf8');
|
|
320
|
+
const parsedPolicy = yaml.load(policyContent);
|
|
321
|
+
|
|
322
|
+
// Validate the parsed policy
|
|
323
|
+
if (!parsedPolicy || typeof parsedPolicy !== 'object') {
|
|
324
|
+
// Malformed policy - DO NOT fall back silently
|
|
325
|
+
projectPolicy = {
|
|
326
|
+
policy: null,
|
|
327
|
+
path: policyPath,
|
|
328
|
+
loadedAt: Date.now(),
|
|
329
|
+
modifiedAt: stat.mtime.getTime(),
|
|
330
|
+
error: 'Invalid YAML or empty policy file',
|
|
331
|
+
source: null
|
|
332
|
+
};
|
|
333
|
+
console.error(`[Agent] Project policy at ${policyPath} is malformed or empty`);
|
|
334
|
+
} else {
|
|
335
|
+
projectPolicy = {
|
|
336
|
+
policy: parsedPolicy,
|
|
337
|
+
path: policyPath,
|
|
338
|
+
loadedAt: Date.now(),
|
|
339
|
+
modifiedAt: stat.mtime.getTime(),
|
|
340
|
+
source: 'project policy'
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Cache the result
|
|
345
|
+
this.projectPolicyCache.set(cacheKey, projectPolicy);
|
|
346
|
+
|
|
347
|
+
} catch (e) {
|
|
348
|
+
console.error(`[Agent] Failed to load project policy from ${policyPath}:`, e.message);
|
|
349
|
+
// Store the error state - DO NOT silently fall back
|
|
350
|
+
projectPolicy = {
|
|
351
|
+
policy: null,
|
|
352
|
+
path: policyPath,
|
|
353
|
+
loadedAt: Date.now(),
|
|
354
|
+
error: e.message,
|
|
355
|
+
source: null
|
|
356
|
+
};
|
|
357
|
+
// Cache the failure to avoid repeated file system hits
|
|
358
|
+
this.projectPolicyCache.set(cacheKey, projectPolicy);
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
// No project policy file - this is OK, not an error
|
|
362
|
+
projectPolicy = {
|
|
363
|
+
policy: null,
|
|
364
|
+
path: policyPath,
|
|
365
|
+
loadedAt: Date.now(),
|
|
366
|
+
source: null
|
|
367
|
+
};
|
|
368
|
+
// Cache the absence of project policy
|
|
369
|
+
this.projectPolicyCache.set(cacheKey, projectPolicy);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return projectPolicy;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
mergePoliciesWithSources(resolvedPolicies) {
|
|
376
|
+
// Build merged policy with CORRECT precedence: project > user > system > org
|
|
377
|
+
// Rules from higher-precedence policies completely override rules with the same name from lower-precedence policies
|
|
378
|
+
const sources = [];
|
|
379
|
+
const rules = [];
|
|
380
|
+
const seenRuleNames = new Set();
|
|
381
|
+
let defaultMode = 'advisory';
|
|
382
|
+
let defaultModeSource = 'defaults';
|
|
383
|
+
|
|
384
|
+
// Process policies in order of precedence (HIGHEST FIRST)
|
|
385
|
+
const policiesInOrder = [
|
|
386
|
+
{ type: 'project', data: resolvedPolicies.project },
|
|
387
|
+
{ type: 'user', data: resolvedPolicies.user },
|
|
388
|
+
{ type: 'system', data: resolvedPolicies.system },
|
|
389
|
+
{ type: 'org', data: resolvedPolicies.org }
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
// First pass: determine the effective defaultMode (highest precedence wins)
|
|
393
|
+
for (const { type, data } of policiesInOrder) {
|
|
394
|
+
if (data?.policy?.defaultMode) {
|
|
395
|
+
if (defaultModeSource === 'defaults') {
|
|
396
|
+
defaultMode = data.policy.defaultMode;
|
|
397
|
+
defaultModeSource = `${type} policy`;
|
|
398
|
+
break; // Stop at first policy that defines defaultMode
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Second pass: merge rules with proper precedence
|
|
404
|
+
for (const { type, data } of policiesInOrder) {
|
|
405
|
+
if (!data) continue;
|
|
406
|
+
|
|
407
|
+
// Handle malformed policies explicitly
|
|
408
|
+
if (data.error) {
|
|
409
|
+
console.error(`[Agent] ${type} policy is malformed: ${data.error}`);
|
|
410
|
+
// Do NOT silently fall back - log the error and continue
|
|
411
|
+
sources.push({ type, path: data.path, error: data.error });
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (!data.policy) {
|
|
416
|
+
// Policy file doesn't exist or is empty
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Validate policy structure
|
|
421
|
+
if (typeof data.policy !== 'object') {
|
|
422
|
+
console.error(`[Agent] ${type} policy is not a valid object`);
|
|
423
|
+
sources.push({ type, path: data.path, error: 'Invalid policy format' });
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
sources.push({ type, path: data.path });
|
|
428
|
+
|
|
429
|
+
// Process rules if they exist and are valid
|
|
430
|
+
if (Array.isArray(data.policy.rules)) {
|
|
431
|
+
for (const rule of data.policy.rules) {
|
|
432
|
+
// Rules must have names to be overridable
|
|
433
|
+
if (!rule.name) {
|
|
434
|
+
console.warn(`[Agent] Rule from ${type} policy missing 'name' property:`, rule);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Only add rule if we haven't seen this name from a higher-precedence source
|
|
439
|
+
if (!seenRuleNames.has(rule.name)) {
|
|
440
|
+
rules.push({
|
|
441
|
+
...rule,
|
|
442
|
+
source: `${type} policy`,
|
|
443
|
+
_sourcePath: data.path
|
|
444
|
+
});
|
|
445
|
+
seenRuleNames.add(rule.name);
|
|
446
|
+
} else {
|
|
447
|
+
// Log when a rule is overridden for debugging
|
|
448
|
+
console.debug(`[Agent] Rule '${rule.name}' from ${type} policy overridden by higher precedence`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
} else if (data.policy.rules !== undefined) {
|
|
452
|
+
console.warn(`[Agent] ${type} policy has invalid 'rules' field (not an array)`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
defaultMode,
|
|
458
|
+
defaultModeSource,
|
|
459
|
+
rules,
|
|
460
|
+
_sources: sources,
|
|
461
|
+
_resolvedAt: Date.now(),
|
|
462
|
+
_context: resolvedPolicies,
|
|
463
|
+
_precedenceOrder: 'project > user > system > org'
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
logDecision(decision, context) {
|
|
468
|
+
const logEntry = {
|
|
469
|
+
...decision,
|
|
470
|
+
context: {
|
|
471
|
+
command: context.command,
|
|
472
|
+
pwd: context.pwd,
|
|
473
|
+
user: process.env.USER,
|
|
474
|
+
gitBranch: context.gitBranch
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
this.auditLog.push(logEntry);
|
|
479
|
+
|
|
480
|
+
// Also write to file
|
|
481
|
+
const logDir = path.join(process.env.HOME, '.delimit', 'audit');
|
|
482
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
483
|
+
const logFile = path.join(logDir, `${new Date().toISOString().split('T')[0]}.jsonl`);
|
|
484
|
+
fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
start() {
|
|
488
|
+
this.app.listen(this.port, '127.0.0.1', () => {
|
|
489
|
+
console.log(`[Delimit Agent] Running on port ${this.port}`);
|
|
490
|
+
console.log(`[Delimit Agent] Session mode: ${this.sessionMode}`);
|
|
491
|
+
console.log(`[Delimit Agent] Global policies loaded: ${Object.keys(this.globalPolicies).join(', ')}`);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Reload global policies periodically and clear project cache
|
|
495
|
+
setInterval(() => {
|
|
496
|
+
this.loadGlobalPolicies();
|
|
497
|
+
// Clear project policy cache to pick up changes
|
|
498
|
+
this.projectPolicyCache.clear();
|
|
499
|
+
}, 60000);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Start the agent
|
|
504
|
+
if (require.main === module) {
|
|
505
|
+
const agent = new DelimitAgent();
|
|
506
|
+
agent.start();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
module.exports = DelimitAgent;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Engine — Bridge from npm CLI to the Python gateway core.
|
|
3
|
+
*
|
|
4
|
+
* Invokes the delimit-gateway Python engine for:
|
|
5
|
+
* - lint (diff + policy)
|
|
6
|
+
* - diff (pure diff)
|
|
7
|
+
* - explain (human-readable templates)
|
|
8
|
+
* - semver (version classification)
|
|
9
|
+
*
|
|
10
|
+
* The gateway is the single implementation authority.
|
|
11
|
+
* This module is a pure translation layer.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { execSync } = require('child_process');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
|
|
19
|
+
// Gateway root — the Python engine lives here
|
|
20
|
+
const GATEWAY_ROOT = process.env.DELIMIT_GATEWAY_ROOT || '/home/delimit/delimit-gateway';
|
|
21
|
+
|
|
22
|
+
// Python executable — prefer venv if available
|
|
23
|
+
const PYTHON = (() => {
|
|
24
|
+
const venvPy = '/home/delimit/.delimit_suite/venv/bin/python';
|
|
25
|
+
if (fs.existsSync(venvPy)) return venvPy;
|
|
26
|
+
return 'python3';
|
|
27
|
+
})();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Run a Python script against the gateway core.
|
|
31
|
+
* Writes to a temp file to avoid shell escaping issues.
|
|
32
|
+
* Returns parsed JSON or throws.
|
|
33
|
+
*/
|
|
34
|
+
function runGateway(pythonCode, timeoutMs = 30000) {
|
|
35
|
+
const tmpFile = path.join(os.tmpdir(), `delimit_${process.pid}_${Date.now()}.py`);
|
|
36
|
+
try {
|
|
37
|
+
fs.writeFileSync(tmpFile, pythonCode);
|
|
38
|
+
const result = execSync(
|
|
39
|
+
`${PYTHON} "${tmpFile}"`,
|
|
40
|
+
{
|
|
41
|
+
cwd: GATEWAY_ROOT,
|
|
42
|
+
timeout: timeoutMs,
|
|
43
|
+
encoding: 'utf-8',
|
|
44
|
+
env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' },
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
return JSON.parse(result.trim());
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err.stdout) {
|
|
50
|
+
try { return JSON.parse(err.stdout.trim()); } catch (_) {}
|
|
51
|
+
}
|
|
52
|
+
throw new Error(err.stderr || err.message || 'Gateway execution failed');
|
|
53
|
+
} finally {
|
|
54
|
+
try { fs.unlinkSync(tmpFile); } catch (_) {}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Escape a string for safe embedding in Python source.
|
|
60
|
+
*/
|
|
61
|
+
function pyStr(s) {
|
|
62
|
+
if (s == null) return 'None';
|
|
63
|
+
return JSON.stringify(s); // JSON strings are valid Python strings
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* delimit lint — diff + policy evaluation (primary command)
|
|
68
|
+
*/
|
|
69
|
+
function lint(oldSpec, newSpec, opts = {}) {
|
|
70
|
+
const lines = [
|
|
71
|
+
'import json, yaml, sys',
|
|
72
|
+
'sys.path.insert(0, ".")',
|
|
73
|
+
'from core.policy_engine import evaluate_with_policy',
|
|
74
|
+
`with open(${pyStr(oldSpec)}) as f: old = yaml.safe_load(f)`,
|
|
75
|
+
`with open(${pyStr(newSpec)}) as f: new = yaml.safe_load(f)`,
|
|
76
|
+
`r = evaluate_with_policy(old, new`,
|
|
77
|
+
];
|
|
78
|
+
const args = ['include_semver=True'];
|
|
79
|
+
if (opts.policy) args.push(`policy_file=${pyStr(opts.policy)}`);
|
|
80
|
+
if (opts.version) args.push(`current_version=${pyStr(opts.version)}`);
|
|
81
|
+
if (opts.name) args.push(`api_name=${pyStr(opts.name)}`);
|
|
82
|
+
// Close the function call
|
|
83
|
+
lines[lines.length - 1] = `r = evaluate_with_policy(old, new, ${args.join(', ')})`;
|
|
84
|
+
lines.push('print(json.dumps(r))');
|
|
85
|
+
return runGateway(lines.join('\n'));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* delimit diff — pure diff, no policy
|
|
90
|
+
*/
|
|
91
|
+
function diff(oldSpec, newSpec) {
|
|
92
|
+
return runGateway([
|
|
93
|
+
'import json, yaml, sys',
|
|
94
|
+
'sys.path.insert(0, ".")',
|
|
95
|
+
'from core.diff_engine_v2 import OpenAPIDiffEngine',
|
|
96
|
+
`with open(${pyStr(oldSpec)}) as f: old = yaml.safe_load(f)`,
|
|
97
|
+
`with open(${pyStr(newSpec)}) as f: new = yaml.safe_load(f)`,
|
|
98
|
+
'engine = OpenAPIDiffEngine()',
|
|
99
|
+
'changes = engine.compare(old, new)',
|
|
100
|
+
'breaking = [c for c in changes if c.is_breaking]',
|
|
101
|
+
'r = {"total_changes": len(changes), "breaking_changes": len(breaking), "changes": [{"type": c.type.value, "path": c.path, "message": c.message, "is_breaking": c.is_breaking} for c in changes]}',
|
|
102
|
+
'print(json.dumps(r))',
|
|
103
|
+
].join('\n'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* delimit explain — human-readable explanation
|
|
108
|
+
*/
|
|
109
|
+
function explain(oldSpec, newSpec, opts = {}) {
|
|
110
|
+
const template = opts.template || 'developer';
|
|
111
|
+
const args = [`template=${pyStr(template)}`];
|
|
112
|
+
if (opts.oldVersion) args.push(`old_version=${pyStr(opts.oldVersion)}`);
|
|
113
|
+
if (opts.newVersion) args.push(`new_version=${pyStr(opts.newVersion)}`);
|
|
114
|
+
if (opts.name) args.push(`api_name=${pyStr(opts.name)}`);
|
|
115
|
+
|
|
116
|
+
return runGateway([
|
|
117
|
+
'import json, yaml, sys',
|
|
118
|
+
'sys.path.insert(0, ".")',
|
|
119
|
+
'from core.diff_engine_v2 import OpenAPIDiffEngine',
|
|
120
|
+
'from core.explainer import explain, TEMPLATES',
|
|
121
|
+
`with open(${pyStr(oldSpec)}) as f: old = yaml.safe_load(f)`,
|
|
122
|
+
`with open(${pyStr(newSpec)}) as f: new = yaml.safe_load(f)`,
|
|
123
|
+
'engine = OpenAPIDiffEngine()',
|
|
124
|
+
'changes = engine.compare(old, new)',
|
|
125
|
+
`out = explain(changes, ${args.join(', ')})`,
|
|
126
|
+
`print(json.dumps({"template": ${pyStr(template)}, "available_templates": TEMPLATES, "output": out}))`,
|
|
127
|
+
].join('\n'));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* delimit semver — classify version bump
|
|
132
|
+
*/
|
|
133
|
+
function semver(oldSpec, newSpec, currentVersion) {
|
|
134
|
+
const extraLines = currentVersion
|
|
135
|
+
? [
|
|
136
|
+
`r["current_version"] = ${pyStr(currentVersion)}`,
|
|
137
|
+
`r["next_version"] = bump_version(${pyStr(currentVersion)}, classify(changes))`,
|
|
138
|
+
]
|
|
139
|
+
: [];
|
|
140
|
+
|
|
141
|
+
return runGateway([
|
|
142
|
+
'import json, yaml, sys',
|
|
143
|
+
'sys.path.insert(0, ".")',
|
|
144
|
+
'from core.diff_engine_v2 import OpenAPIDiffEngine',
|
|
145
|
+
'from core.semver_classifier import classify_detailed, bump_version, classify',
|
|
146
|
+
`with open(${pyStr(oldSpec)}) as f: old = yaml.safe_load(f)`,
|
|
147
|
+
`with open(${pyStr(newSpec)}) as f: new = yaml.safe_load(f)`,
|
|
148
|
+
'engine = OpenAPIDiffEngine()',
|
|
149
|
+
'changes = engine.compare(old, new)',
|
|
150
|
+
'r = classify_detailed(changes)',
|
|
151
|
+
...extraLines,
|
|
152
|
+
'print(json.dumps(r))',
|
|
153
|
+
].join('\n'));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = { lint, diff, explain, semver, GATEWAY_ROOT };
|