aegis-mcp-server 0.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/LICENSE +21 -0
- package/README.md +95 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +144 -0
- package/dist/index.js.map +1 -0
- package/dist/services/enforcement-engine.d.ts +64 -0
- package/dist/services/enforcement-engine.d.ts.map +1 -0
- package/dist/services/enforcement-engine.js +271 -0
- package/dist/services/enforcement-engine.js.map +1 -0
- package/dist/services/policy-loader.d.ts +56 -0
- package/dist/services/policy-loader.d.ts.map +1 -0
- package/dist/services/policy-loader.js +202 -0
- package/dist/services/policy-loader.js.map +1 -0
- package/dist/tools/file-tools.d.ts +21 -0
- package/dist/tools/file-tools.d.ts.map +1 -0
- package/dist/tools/file-tools.js +369 -0
- package/dist/tools/file-tools.js.map +1 -0
- package/dist/types.d.ts +286 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/package.json +41 -0
- package/src/index.ts +171 -0
- package/src/services/enforcement-engine.ts +322 -0
- package/src/services/policy-loader.ts +255 -0
- package/src/tools/file-tools.ts +453 -0
- package/src/types.ts +305 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governed File Tools — MCP tool registrations for file operations.
|
|
3
|
+
*
|
|
4
|
+
* These are the tools agents call instead of raw file system access.
|
|
5
|
+
* Every call is validated against the loaded policy before execution.
|
|
6
|
+
* The agent never sees the policy — only the verdict.
|
|
7
|
+
*
|
|
8
|
+
* Tools:
|
|
9
|
+
* aegis_check_permissions — Pre-check before writing (saves wasted generation)
|
|
10
|
+
* aegis_write_file — Governed write with path + content validation
|
|
11
|
+
* aegis_read_file — Governed read with path validation
|
|
12
|
+
* aegis_delete_file — Governed delete (uses write permissions)
|
|
13
|
+
* aegis_execute — Governed command execution
|
|
14
|
+
* aegis_complete_task — Task completion with quality gate validation
|
|
15
|
+
* aegis_policy_summary — Minimal role/permissions summary (~200 tokens)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
|
|
19
|
+
import { dirname, join, isAbsolute } from 'node:path';
|
|
20
|
+
import { execSync } from 'node:child_process';
|
|
21
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import type { EnforcementEngine } from '../services/enforcement-engine.js';
|
|
24
|
+
import type { PolicyState, ResolvedRole } from '../types.js';
|
|
25
|
+
|
|
26
|
+
export function registerTools(
|
|
27
|
+
server: McpServer,
|
|
28
|
+
getEngine: () => EnforcementEngine,
|
|
29
|
+
getState: () => PolicyState,
|
|
30
|
+
getRole: () => ResolvedRole
|
|
31
|
+
): void {
|
|
32
|
+
|
|
33
|
+
// ─── aegis_check_permissions ──────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
server.registerTool(
|
|
36
|
+
'aegis_check_permissions',
|
|
37
|
+
{
|
|
38
|
+
title: 'Check Permissions',
|
|
39
|
+
description: `Check if an operation is allowed on a path before attempting it. Use this to pre-validate before writing or reading files — saves you from composing content that would be blocked.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
- path (string): Target file path relative to project root
|
|
43
|
+
- operation ('read' | 'write' | 'delete'): The operation to check
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
{ "allowed": true } or { "allowed": false, "reason": "..." }`,
|
|
47
|
+
inputSchema: {
|
|
48
|
+
path: z.string().describe('Target file path relative to project root'),
|
|
49
|
+
operation: z.enum(['read', 'write', 'delete']).describe('Operation to check'),
|
|
50
|
+
},
|
|
51
|
+
annotations: {
|
|
52
|
+
readOnlyHint: true,
|
|
53
|
+
destructiveHint: false,
|
|
54
|
+
idempotentHint: true,
|
|
55
|
+
openWorldHint: false,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
async ({ path, operation }) => {
|
|
59
|
+
const engine = getEngine();
|
|
60
|
+
const verdict = operation === 'read'
|
|
61
|
+
? engine.validateRead(path)
|
|
62
|
+
: engine.validateWrite(path);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
content: [{
|
|
66
|
+
type: 'text' as const,
|
|
67
|
+
text: JSON.stringify(
|
|
68
|
+
verdict.allowed
|
|
69
|
+
? { allowed: true }
|
|
70
|
+
: { allowed: false, reason: verdict.reason }
|
|
71
|
+
),
|
|
72
|
+
}],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// ─── aegis_write_file ─────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
server.registerTool(
|
|
80
|
+
'aegis_write_file',
|
|
81
|
+
{
|
|
82
|
+
title: 'Write File (Governed)',
|
|
83
|
+
description: `Write content to a file with governance enforcement. Path is validated against your role's permissions and governance boundaries. Content is scanned for sensitive patterns. If the write violates policy, it is blocked and you receive the specific reason.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
- path (string): File path relative to project root
|
|
87
|
+
- content (string): File content to write
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
{ "status": "success", "path": "..." } or { "status": "blocked", "reason": "..." }`,
|
|
91
|
+
inputSchema: {
|
|
92
|
+
path: z.string().describe('File path relative to project root'),
|
|
93
|
+
content: z.string().describe('File content to write'),
|
|
94
|
+
},
|
|
95
|
+
annotations: {
|
|
96
|
+
readOnlyHint: false,
|
|
97
|
+
destructiveHint: true,
|
|
98
|
+
idempotentHint: true,
|
|
99
|
+
openWorldHint: false,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
async ({ path, content }) => {
|
|
103
|
+
const engine = getEngine();
|
|
104
|
+
const state = getState();
|
|
105
|
+
const role = getRole();
|
|
106
|
+
|
|
107
|
+
// Validate path permissions
|
|
108
|
+
const pathVerdict = engine.validateWrite(path);
|
|
109
|
+
if (!pathVerdict.allowed) {
|
|
110
|
+
await logBlocked(engine, role, path, 'write', pathVerdict.reason);
|
|
111
|
+
return blocked(pathVerdict.reason);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Scan content for sensitive patterns
|
|
115
|
+
const contentVerdict = engine.scanContent(content, path);
|
|
116
|
+
if (!contentVerdict.allowed) {
|
|
117
|
+
await logBlocked(engine, role, path, 'write (sensitive content)', contentVerdict.reason);
|
|
118
|
+
return blocked(contentVerdict.reason);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Write the file
|
|
122
|
+
const absPath = toAbsolute(path, state.projectRoot);
|
|
123
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
124
|
+
await writeFile(absPath, content, 'utf-8');
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
content: [{
|
|
128
|
+
type: 'text' as const,
|
|
129
|
+
text: JSON.stringify({ status: 'success', path }),
|
|
130
|
+
}],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// ─── aegis_read_file ──────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
server.registerTool(
|
|
138
|
+
'aegis_read_file',
|
|
139
|
+
{
|
|
140
|
+
title: 'Read File (Governed)',
|
|
141
|
+
description: `Read the contents of a file with governance enforcement. Path is validated against your role's read permissions. If the read violates policy, it is blocked.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
- path (string): File path relative to project root
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
File content as text, or { "status": "blocked", "reason": "..." }`,
|
|
148
|
+
inputSchema: {
|
|
149
|
+
path: z.string().describe('File path relative to project root'),
|
|
150
|
+
},
|
|
151
|
+
annotations: {
|
|
152
|
+
readOnlyHint: true,
|
|
153
|
+
destructiveHint: false,
|
|
154
|
+
idempotentHint: true,
|
|
155
|
+
openWorldHint: false,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
async ({ path }) => {
|
|
159
|
+
const engine = getEngine();
|
|
160
|
+
const state = getState();
|
|
161
|
+
|
|
162
|
+
const verdict = engine.validateRead(path);
|
|
163
|
+
if (!verdict.allowed) {
|
|
164
|
+
return blocked(verdict.reason);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const absPath = toAbsolute(path, state.projectRoot);
|
|
168
|
+
const content = await readFile(absPath, 'utf-8');
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
content: [{
|
|
172
|
+
type: 'text' as const,
|
|
173
|
+
text: content,
|
|
174
|
+
}],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// ─── aegis_delete_file ────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
server.registerTool(
|
|
182
|
+
'aegis_delete_file',
|
|
183
|
+
{
|
|
184
|
+
title: 'Delete File (Governed)',
|
|
185
|
+
description: `Delete a file with governance enforcement. Write permissions are required. If the delete violates policy, it is blocked.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
- path (string): File path relative to project root
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
{ "status": "success", "path": "..." } or { "status": "blocked", "reason": "..." }`,
|
|
192
|
+
inputSchema: {
|
|
193
|
+
path: z.string().describe('File path relative to project root'),
|
|
194
|
+
},
|
|
195
|
+
annotations: {
|
|
196
|
+
readOnlyHint: false,
|
|
197
|
+
destructiveHint: true,
|
|
198
|
+
idempotentHint: false,
|
|
199
|
+
openWorldHint: false,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
async ({ path }) => {
|
|
203
|
+
const engine = getEngine();
|
|
204
|
+
const state = getState();
|
|
205
|
+
const role = getRole();
|
|
206
|
+
|
|
207
|
+
const verdict = engine.validateWrite(path);
|
|
208
|
+
if (!verdict.allowed) {
|
|
209
|
+
await logBlocked(engine, role, path, 'delete', verdict.reason);
|
|
210
|
+
return blocked(verdict.reason);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const absPath = toAbsolute(path, state.projectRoot);
|
|
214
|
+
await unlink(absPath);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
content: [{
|
|
218
|
+
type: 'text' as const,
|
|
219
|
+
text: JSON.stringify({ status: 'success', path }),
|
|
220
|
+
}],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// ─── aegis_execute ────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
server.registerTool(
|
|
228
|
+
'aegis_execute',
|
|
229
|
+
{
|
|
230
|
+
title: 'Execute Command (Governed)',
|
|
231
|
+
description: `Execute a shell command in the project directory. Currently validates that the command runs within the project root. Future versions will enforce command-level permissions.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
- command (string): Shell command to execute
|
|
235
|
+
- cwd (string, optional): Working directory (defaults to project root)
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
{ "status": "success", "stdout": "...", "stderr": "..." } or { "status": "error", ... }`,
|
|
239
|
+
inputSchema: {
|
|
240
|
+
command: z.string().describe('Shell command to execute'),
|
|
241
|
+
cwd: z.string().optional().describe('Working directory (defaults to project root)'),
|
|
242
|
+
},
|
|
243
|
+
annotations: {
|
|
244
|
+
readOnlyHint: false,
|
|
245
|
+
destructiveHint: true,
|
|
246
|
+
idempotentHint: false,
|
|
247
|
+
openWorldHint: true,
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
async ({ command, cwd }) => {
|
|
251
|
+
const state = getState();
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const result = execSync(command, {
|
|
255
|
+
cwd: cwd ?? state.projectRoot,
|
|
256
|
+
encoding: 'utf-8',
|
|
257
|
+
timeout: 60_000,
|
|
258
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
content: [{
|
|
263
|
+
type: 'text' as const,
|
|
264
|
+
text: JSON.stringify({ status: 'success', stdout: result, stderr: '' }),
|
|
265
|
+
}],
|
|
266
|
+
};
|
|
267
|
+
} catch (err: unknown) {
|
|
268
|
+
const execErr = err as { stdout?: string; stderr?: string; message?: string };
|
|
269
|
+
return {
|
|
270
|
+
isError: true,
|
|
271
|
+
content: [{
|
|
272
|
+
type: 'text' as const,
|
|
273
|
+
text: JSON.stringify({
|
|
274
|
+
status: 'error',
|
|
275
|
+
stdout: execErr.stdout ?? '',
|
|
276
|
+
stderr: execErr.stderr ?? execErr.message ?? 'Unknown error',
|
|
277
|
+
}),
|
|
278
|
+
}],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// ─── aegis_complete_task ──────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
server.registerTool(
|
|
287
|
+
'aegis_complete_task',
|
|
288
|
+
{
|
|
289
|
+
title: 'Complete Task',
|
|
290
|
+
description: `Signal task completion and run required quality gates. Maps the governance quality_gate.pre_commit flags to build_commands and runs each required check. Returns pass/fail with details.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
- task_id (string): Identifier for the task being completed
|
|
294
|
+
- summary (string): Brief summary of what was accomplished
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
{ "status": "passed", "gates_run": [...] } or { "status": "failed", "failures": [...] }`,
|
|
298
|
+
inputSchema: {
|
|
299
|
+
task_id: z.string().describe('Task identifier'),
|
|
300
|
+
summary: z.string().describe('Summary of completed work'),
|
|
301
|
+
},
|
|
302
|
+
annotations: {
|
|
303
|
+
readOnlyHint: false,
|
|
304
|
+
destructiveHint: false,
|
|
305
|
+
idempotentHint: true,
|
|
306
|
+
openWorldHint: false,
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
async ({ task_id, summary }) => {
|
|
310
|
+
const engine = getEngine();
|
|
311
|
+
const state = getState();
|
|
312
|
+
const gates = engine.getQualityGateCommands();
|
|
313
|
+
|
|
314
|
+
if (gates.length === 0) {
|
|
315
|
+
return {
|
|
316
|
+
content: [{
|
|
317
|
+
type: 'text' as const,
|
|
318
|
+
text: JSON.stringify({
|
|
319
|
+
status: 'passed',
|
|
320
|
+
task_id,
|
|
321
|
+
summary,
|
|
322
|
+
gates_run: [],
|
|
323
|
+
message: 'No quality gates configured with matching build commands.',
|
|
324
|
+
}),
|
|
325
|
+
}],
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const results: Array<{ name: string; passed: boolean; output?: string }> = [];
|
|
330
|
+
|
|
331
|
+
for (const gate of gates) {
|
|
332
|
+
try {
|
|
333
|
+
const output = execSync(gate.command, {
|
|
334
|
+
cwd: state.projectRoot,
|
|
335
|
+
encoding: 'utf-8',
|
|
336
|
+
timeout: 120_000,
|
|
337
|
+
});
|
|
338
|
+
results.push({ name: gate.name, passed: true, output: output.slice(0, 500) });
|
|
339
|
+
} catch (err: unknown) {
|
|
340
|
+
const execErr = err as { stderr?: string; message?: string };
|
|
341
|
+
results.push({
|
|
342
|
+
name: gate.name,
|
|
343
|
+
passed: false,
|
|
344
|
+
output: (execErr.stderr ?? execErr.message ?? 'Failed').slice(0, 500),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const allPassed = results.every((r) => r.passed);
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
content: [{
|
|
353
|
+
type: 'text' as const,
|
|
354
|
+
text: JSON.stringify({
|
|
355
|
+
status: allPassed ? 'passed' : 'failed',
|
|
356
|
+
task_id,
|
|
357
|
+
summary,
|
|
358
|
+
gates_run: results,
|
|
359
|
+
}),
|
|
360
|
+
}],
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// ─── aegis_policy_summary ─────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
server.registerTool(
|
|
368
|
+
'aegis_policy_summary',
|
|
369
|
+
{
|
|
370
|
+
title: 'Policy Summary',
|
|
371
|
+
description: `Get a minimal summary of your current role and permissions. Returns your role name, writable paths, excluded paths, forbidden actions, and key governance rules — just enough to understand your boundaries without loading full policy files.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
{ "role": "...", "writable_paths": [...], "forbidden_actions": [...], ... }`,
|
|
375
|
+
inputSchema: {},
|
|
376
|
+
annotations: {
|
|
377
|
+
readOnlyHint: true,
|
|
378
|
+
destructiveHint: false,
|
|
379
|
+
idempotentHint: true,
|
|
380
|
+
openWorldHint: false,
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
async () => {
|
|
384
|
+
const role = getRole();
|
|
385
|
+
const state = getState();
|
|
386
|
+
const protocol = state.governance.override_protocol;
|
|
387
|
+
|
|
388
|
+
const summary = {
|
|
389
|
+
role: role.id,
|
|
390
|
+
role_name: role.name,
|
|
391
|
+
purpose: role.purpose,
|
|
392
|
+
autonomy: role.autonomy,
|
|
393
|
+
writable_paths: role.writable_paths,
|
|
394
|
+
secondary_paths: role.secondary_paths,
|
|
395
|
+
excluded_paths: role.excluded_paths,
|
|
396
|
+
readable_paths: role.readable_paths,
|
|
397
|
+
forbidden_actions: role.forbidden_actions,
|
|
398
|
+
governance_forbidden_paths: state.governance.permissions.boundaries.forbidden ?? [],
|
|
399
|
+
override_behavior: protocol?.behavior ?? 'warn_confirm_and_log',
|
|
400
|
+
immutable_policies: protocol?.immutable_policies ?? [],
|
|
401
|
+
quality_gates: {
|
|
402
|
+
must_pass_tests: state.governance.quality_gate.pre_commit.must_pass_tests ?? false,
|
|
403
|
+
must_pass_lint: state.governance.quality_gate.pre_commit.must_pass_lint ?? false,
|
|
404
|
+
must_pass_typecheck: state.governance.quality_gate.pre_commit.must_pass_typecheck ?? false,
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
content: [{
|
|
410
|
+
type: 'text' as const,
|
|
411
|
+
text: JSON.stringify(summary),
|
|
412
|
+
}],
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
function toAbsolute(path: string, projectRoot: string): string {
|
|
421
|
+
return isAbsolute(path) ? path : join(projectRoot, path);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function blocked(reason: string): {
|
|
425
|
+
isError: boolean;
|
|
426
|
+
content: Array<{ type: 'text'; text: string }>;
|
|
427
|
+
} {
|
|
428
|
+
return {
|
|
429
|
+
isError: true,
|
|
430
|
+
content: [{
|
|
431
|
+
type: 'text' as const,
|
|
432
|
+
text: JSON.stringify({ status: 'blocked', reason }),
|
|
433
|
+
}],
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function logBlocked(
|
|
438
|
+
engine: EnforcementEngine,
|
|
439
|
+
role: ResolvedRole,
|
|
440
|
+
path: string,
|
|
441
|
+
operation: string,
|
|
442
|
+
reason: string
|
|
443
|
+
): Promise<void> {
|
|
444
|
+
await engine.logOverride({
|
|
445
|
+
timestamp: new Date().toISOString(),
|
|
446
|
+
policy_violated: reason,
|
|
447
|
+
policy_text: reason,
|
|
448
|
+
action_requested: `${operation}: ${path}`,
|
|
449
|
+
human_confirmed: false,
|
|
450
|
+
agent_role: role.id,
|
|
451
|
+
rationale: 'Blocked by enforcement layer',
|
|
452
|
+
});
|
|
453
|
+
}
|