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.
@@ -0,0 +1,56 @@
1
+ /**
2
+ * PolicyLoader — Reads .agentpolicy/ files into process memory.
3
+ *
4
+ * Core of the zero-token-overhead design. All governance files are loaded
5
+ * into Node.js process memory on startup. The agent never sees these files —
6
+ * it only sees tool call results (allowed/blocked).
7
+ *
8
+ * Role resolution merges the skeleton fields (role.name, scope.primary_paths)
9
+ * with extension fields (paths.read/write, forbidden_actions) into a single
10
+ * ResolvedRole for fast enforcement lookups.
11
+ */
12
+ import type { PolicyState, ResolvedRole, AegisMcpConfig } from '../types.js';
13
+ export declare class PolicyLoader {
14
+ private config;
15
+ private state;
16
+ private watcher;
17
+ private onReload?;
18
+ constructor(config: AegisMcpConfig);
19
+ /**
20
+ * Load all policy files into memory. Call once on startup.
21
+ */
22
+ load(): Promise<PolicyState>;
23
+ /**
24
+ * Get current policy state. Throws if not loaded.
25
+ */
26
+ getState(): PolicyState;
27
+ /**
28
+ * Start watching .agentpolicy/ for changes and auto-reload.
29
+ */
30
+ startWatching(onReload?: () => void): void;
31
+ /**
32
+ * Stop watching and clean up.
33
+ */
34
+ stopWatching(): Promise<void>;
35
+ /**
36
+ * Get the resolved role for the configured agent, falling back to default.
37
+ */
38
+ getActiveRole(): ResolvedRole;
39
+ private resolvePolicyDir;
40
+ private loadJson;
41
+ private loadRoles;
42
+ /**
43
+ * Merge skeleton and extension fields into a single ResolvedRole.
44
+ *
45
+ * Skeleton: role.name, role.purpose, scope.primary_paths/secondary_paths/excluded_paths
46
+ * Extensions: paths.read/write, forbidden_actions, autonomy (flat string)
47
+ *
48
+ * For writable paths: scope.primary_paths takes precedence; paths.write used as fallback.
49
+ * For readable paths: paths.read used when present; otherwise derived from writable + secondary.
50
+ */
51
+ private resolveRole;
52
+ private handleChange;
53
+ private assertExists;
54
+ private log;
55
+ }
56
+ //# sourceMappingURL=policy-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-loader.d.ts","sourceRoot":"","sources":["../../src/services/policy-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EACV,WAAW,EAIX,YAAY,EACZ,cAAc,EACf,MAAM,aAAa,CAAC;AAErB,qBAAa,YAAY;IAKX,OAAO,CAAC,MAAM;IAJ1B,OAAO,CAAC,KAAK,CAA4B;IACzC,OAAO,CAAC,OAAO,CAAyC;IACxD,OAAO,CAAC,QAAQ,CAAC,CAAa;gBAEV,MAAM,EAAE,cAAc;IAE1C;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,WAAW,CAAC;IA4BlC;;OAEG;IACH,QAAQ,IAAI,WAAW;IAOvB;;OAEG;IACH,aAAa,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI;IAgB1C;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAOnC;;OAEG;IACH,aAAa,IAAI,YAAY;IA8B7B,OAAO,CAAC,gBAAgB;YAOV,QAAQ;YAYR,SAAS;IAyBvB;;;;;;;;OAQG;IACH,OAAO,CAAC,WAAW;YA4CL,YAAY;YAYZ,YAAY;IAQ1B,OAAO,CAAC,GAAG;CAGZ"}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * PolicyLoader — Reads .agentpolicy/ files into process memory.
3
+ *
4
+ * Core of the zero-token-overhead design. All governance files are loaded
5
+ * into Node.js process memory on startup. The agent never sees these files —
6
+ * it only sees tool call results (allowed/blocked).
7
+ *
8
+ * Role resolution merges the skeleton fields (role.name, scope.primary_paths)
9
+ * with extension fields (paths.read/write, forbidden_actions) into a single
10
+ * ResolvedRole for fast enforcement lookups.
11
+ */
12
+ import { readFile, readdir, access } from 'node:fs/promises';
13
+ import { join, basename } from 'node:path';
14
+ import { watch } from 'chokidar';
15
+ export class PolicyLoader {
16
+ config;
17
+ state = null;
18
+ watcher = null;
19
+ onReload;
20
+ constructor(config) {
21
+ this.config = config;
22
+ }
23
+ /**
24
+ * Load all policy files into memory. Call once on startup.
25
+ */
26
+ async load() {
27
+ const policyDir = this.resolvePolicyDir();
28
+ await this.assertExists(policyDir, 'Policy directory');
29
+ const constitution = await this.loadJson(join(policyDir, 'constitution.json'), 'constitution.json');
30
+ const governance = await this.loadJson(join(policyDir, 'governance.json'), 'governance.json');
31
+ const roles = await this.loadRoles(join(policyDir, 'roles'));
32
+ this.state = {
33
+ constitution,
34
+ governance,
35
+ roles,
36
+ projectRoot: this.config.projectRoot,
37
+ policyDir,
38
+ };
39
+ this.log(`Policy loaded: ${roles.size} role(s)`);
40
+ return this.state;
41
+ }
42
+ /**
43
+ * Get current policy state. Throws if not loaded.
44
+ */
45
+ getState() {
46
+ if (!this.state) {
47
+ throw new Error('Policy not loaded. Call load() first.');
48
+ }
49
+ return this.state;
50
+ }
51
+ /**
52
+ * Start watching .agentpolicy/ for changes and auto-reload.
53
+ */
54
+ startWatching(onReload) {
55
+ this.onReload = onReload;
56
+ const policyDir = this.resolvePolicyDir();
57
+ this.watcher = watch(policyDir, {
58
+ ignoreInitial: true,
59
+ awaitWriteFinish: { stabilityThreshold: 300 },
60
+ });
61
+ this.watcher.on('change', (path) => this.handleChange(path));
62
+ this.watcher.on('add', (path) => this.handleChange(path));
63
+ this.watcher.on('unlink', (path) => this.handleChange(path));
64
+ this.log('Watching policy directory for changes');
65
+ }
66
+ /**
67
+ * Stop watching and clean up.
68
+ */
69
+ async stopWatching() {
70
+ if (this.watcher) {
71
+ await this.watcher.close();
72
+ this.watcher = null;
73
+ }
74
+ }
75
+ /**
76
+ * Get the resolved role for the configured agent, falling back to default.
77
+ */
78
+ getActiveRole() {
79
+ const state = this.getState();
80
+ const roleId = this.config.role;
81
+ const role = state.roles.get(roleId);
82
+ if (role)
83
+ return role;
84
+ const defaultRole = state.roles.get('default');
85
+ if (defaultRole) {
86
+ this.log(`Role "${roleId}" not found, using default`);
87
+ return defaultRole;
88
+ }
89
+ // Synthesize a permissive default if no role files exist
90
+ this.log('No role files found, using synthesized permissive default');
91
+ return {
92
+ id: 'default',
93
+ name: 'default',
94
+ purpose: 'Synthesized default role — no role files found',
95
+ writable_paths: ['**/*'],
96
+ secondary_paths: [],
97
+ excluded_paths: [],
98
+ readable_paths: ['**/*'],
99
+ autonomy: 'advisory',
100
+ forbidden_actions: [],
101
+ };
102
+ }
103
+ // ─── Private ────────────────────────────────────────────────────────────────
104
+ resolvePolicyDir() {
105
+ return join(this.config.projectRoot, this.config.policyDir ?? '.agentpolicy');
106
+ }
107
+ async loadJson(path, label) {
108
+ await this.assertExists(path, label);
109
+ const raw = await readFile(path, 'utf-8');
110
+ try {
111
+ return JSON.parse(raw);
112
+ }
113
+ catch (err) {
114
+ throw new Error(`Failed to parse ${label}: ${err instanceof Error ? err.message : String(err)}`);
115
+ }
116
+ }
117
+ async loadRoles(rolesDir) {
118
+ const roles = new Map();
119
+ try {
120
+ await access(rolesDir);
121
+ }
122
+ catch {
123
+ return roles;
124
+ }
125
+ const entries = await readdir(rolesDir, { withFileTypes: true });
126
+ for (const entry of entries) {
127
+ if (!entry.isFile() || !entry.name.endsWith('.json'))
128
+ continue;
129
+ const roleId = basename(entry.name, '.json');
130
+ const raw = await this.loadJson(join(rolesDir, entry.name), `roles/${entry.name}`);
131
+ roles.set(roleId, this.resolveRole(roleId, raw));
132
+ }
133
+ return roles;
134
+ }
135
+ /**
136
+ * Merge skeleton and extension fields into a single ResolvedRole.
137
+ *
138
+ * Skeleton: role.name, role.purpose, scope.primary_paths/secondary_paths/excluded_paths
139
+ * Extensions: paths.read/write, forbidden_actions, autonomy (flat string)
140
+ *
141
+ * For writable paths: scope.primary_paths takes precedence; paths.write used as fallback.
142
+ * For readable paths: paths.read used when present; otherwise derived from writable + secondary.
143
+ */
144
+ resolveRole(id, raw) {
145
+ // Role identity — skeleton nested object, or flat string + description
146
+ const name = typeof raw.role === 'object' ? raw.role.name : String(raw.role);
147
+ const purpose = typeof raw.role === 'object'
148
+ ? raw.role.purpose
149
+ : (raw.description ?? '');
150
+ // Writable paths — skeleton primary_paths, or extension paths.write
151
+ const writable_paths = raw.scope?.primary_paths?.length
152
+ ? raw.scope.primary_paths
153
+ : (raw.paths?.write ?? []);
154
+ // Secondary paths
155
+ const secondary_paths = raw.scope?.secondary_paths ?? [];
156
+ // Excluded paths
157
+ const excluded_paths = raw.scope?.excluded_paths ?? [];
158
+ // Readable paths — extension paths.read, or all writable + secondary
159
+ const readable_paths = raw.paths?.read?.length
160
+ ? raw.paths.read
161
+ : [...writable_paths, ...secondary_paths];
162
+ // Autonomy — flat extension string or skeleton override
163
+ const autonomy = raw.autonomy
164
+ ? String(raw.autonomy)
165
+ : 'advisory';
166
+ // Forbidden actions
167
+ const forbidden_actions = raw.forbidden_actions ?? [];
168
+ return {
169
+ id,
170
+ name,
171
+ purpose,
172
+ writable_paths,
173
+ secondary_paths,
174
+ excluded_paths,
175
+ readable_paths,
176
+ autonomy,
177
+ forbidden_actions,
178
+ };
179
+ }
180
+ async handleChange(path) {
181
+ this.log(`Policy file changed: ${path}`);
182
+ try {
183
+ await this.load();
184
+ this.onReload?.();
185
+ }
186
+ catch (err) {
187
+ this.log(`Failed to reload policy: ${err instanceof Error ? err.message : String(err)}`);
188
+ }
189
+ }
190
+ async assertExists(path, label) {
191
+ try {
192
+ await access(path);
193
+ }
194
+ catch {
195
+ throw new Error(`${label} not found at: ${path}`);
196
+ }
197
+ }
198
+ log(message) {
199
+ process.stderr.write(`[aegis-mcp] ${message}\n`);
200
+ }
201
+ }
202
+ //# sourceMappingURL=policy-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-loader.js","sourceRoot":"","sources":["../../src/services/policy-loader.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
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
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
18
+ import type { EnforcementEngine } from '../services/enforcement-engine.js';
19
+ import type { PolicyState, ResolvedRole } from '../types.js';
20
+ export declare function registerTools(server: McpServer, getEngine: () => EnforcementEngine, getState: () => PolicyState, getRole: () => ResolvedRole): void;
21
+ //# sourceMappingURL=file-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-tools.d.ts","sourceRoot":"","sources":["../../src/tools/file-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7D,wBAAgB,aAAa,CAC3B,MAAM,EAAE,SAAS,EACjB,SAAS,EAAE,MAAM,iBAAiB,EAClC,QAAQ,EAAE,MAAM,WAAW,EAC3B,OAAO,EAAE,MAAM,YAAY,GAC1B,IAAI,CAiYN"}
@@ -0,0 +1,369 @@
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
+ import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
18
+ import { dirname, join, isAbsolute } from 'node:path';
19
+ import { execSync } from 'node:child_process';
20
+ import { z } from 'zod';
21
+ export function registerTools(server, getEngine, getState, getRole) {
22
+ // ─── aegis_check_permissions ──────────────────────────────────────────────
23
+ server.registerTool('aegis_check_permissions', {
24
+ title: 'Check Permissions',
25
+ 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.
26
+
27
+ Args:
28
+ - path (string): Target file path relative to project root
29
+ - operation ('read' | 'write' | 'delete'): The operation to check
30
+
31
+ Returns:
32
+ { "allowed": true } or { "allowed": false, "reason": "..." }`,
33
+ inputSchema: {
34
+ path: z.string().describe('Target file path relative to project root'),
35
+ operation: z.enum(['read', 'write', 'delete']).describe('Operation to check'),
36
+ },
37
+ annotations: {
38
+ readOnlyHint: true,
39
+ destructiveHint: false,
40
+ idempotentHint: true,
41
+ openWorldHint: false,
42
+ },
43
+ }, async ({ path, operation }) => {
44
+ const engine = getEngine();
45
+ const verdict = operation === 'read'
46
+ ? engine.validateRead(path)
47
+ : engine.validateWrite(path);
48
+ return {
49
+ content: [{
50
+ type: 'text',
51
+ text: JSON.stringify(verdict.allowed
52
+ ? { allowed: true }
53
+ : { allowed: false, reason: verdict.reason }),
54
+ }],
55
+ };
56
+ });
57
+ // ─── aegis_write_file ─────────────────────────────────────────────────────
58
+ server.registerTool('aegis_write_file', {
59
+ title: 'Write File (Governed)',
60
+ 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.
61
+
62
+ Args:
63
+ - path (string): File path relative to project root
64
+ - content (string): File content to write
65
+
66
+ Returns:
67
+ { "status": "success", "path": "..." } or { "status": "blocked", "reason": "..." }`,
68
+ inputSchema: {
69
+ path: z.string().describe('File path relative to project root'),
70
+ content: z.string().describe('File content to write'),
71
+ },
72
+ annotations: {
73
+ readOnlyHint: false,
74
+ destructiveHint: true,
75
+ idempotentHint: true,
76
+ openWorldHint: false,
77
+ },
78
+ }, async ({ path, content }) => {
79
+ const engine = getEngine();
80
+ const state = getState();
81
+ const role = getRole();
82
+ // Validate path permissions
83
+ const pathVerdict = engine.validateWrite(path);
84
+ if (!pathVerdict.allowed) {
85
+ await logBlocked(engine, role, path, 'write', pathVerdict.reason);
86
+ return blocked(pathVerdict.reason);
87
+ }
88
+ // Scan content for sensitive patterns
89
+ const contentVerdict = engine.scanContent(content, path);
90
+ if (!contentVerdict.allowed) {
91
+ await logBlocked(engine, role, path, 'write (sensitive content)', contentVerdict.reason);
92
+ return blocked(contentVerdict.reason);
93
+ }
94
+ // Write the file
95
+ const absPath = toAbsolute(path, state.projectRoot);
96
+ await mkdir(dirname(absPath), { recursive: true });
97
+ await writeFile(absPath, content, 'utf-8');
98
+ return {
99
+ content: [{
100
+ type: 'text',
101
+ text: JSON.stringify({ status: 'success', path }),
102
+ }],
103
+ };
104
+ });
105
+ // ─── aegis_read_file ──────────────────────────────────────────────────────
106
+ server.registerTool('aegis_read_file', {
107
+ title: 'Read File (Governed)',
108
+ 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.
109
+
110
+ Args:
111
+ - path (string): File path relative to project root
112
+
113
+ Returns:
114
+ File content as text, or { "status": "blocked", "reason": "..." }`,
115
+ inputSchema: {
116
+ path: z.string().describe('File path relative to project root'),
117
+ },
118
+ annotations: {
119
+ readOnlyHint: true,
120
+ destructiveHint: false,
121
+ idempotentHint: true,
122
+ openWorldHint: false,
123
+ },
124
+ }, async ({ path }) => {
125
+ const engine = getEngine();
126
+ const state = getState();
127
+ const verdict = engine.validateRead(path);
128
+ if (!verdict.allowed) {
129
+ return blocked(verdict.reason);
130
+ }
131
+ const absPath = toAbsolute(path, state.projectRoot);
132
+ const content = await readFile(absPath, 'utf-8');
133
+ return {
134
+ content: [{
135
+ type: 'text',
136
+ text: content,
137
+ }],
138
+ };
139
+ });
140
+ // ─── aegis_delete_file ────────────────────────────────────────────────────
141
+ server.registerTool('aegis_delete_file', {
142
+ title: 'Delete File (Governed)',
143
+ description: `Delete a file with governance enforcement. Write permissions are required. If the delete violates policy, it is blocked.
144
+
145
+ Args:
146
+ - path (string): File path relative to project root
147
+
148
+ Returns:
149
+ { "status": "success", "path": "..." } or { "status": "blocked", "reason": "..." }`,
150
+ inputSchema: {
151
+ path: z.string().describe('File path relative to project root'),
152
+ },
153
+ annotations: {
154
+ readOnlyHint: false,
155
+ destructiveHint: true,
156
+ idempotentHint: false,
157
+ openWorldHint: false,
158
+ },
159
+ }, async ({ path }) => {
160
+ const engine = getEngine();
161
+ const state = getState();
162
+ const role = getRole();
163
+ const verdict = engine.validateWrite(path);
164
+ if (!verdict.allowed) {
165
+ await logBlocked(engine, role, path, 'delete', verdict.reason);
166
+ return blocked(verdict.reason);
167
+ }
168
+ const absPath = toAbsolute(path, state.projectRoot);
169
+ await unlink(absPath);
170
+ return {
171
+ content: [{
172
+ type: 'text',
173
+ text: JSON.stringify({ status: 'success', path }),
174
+ }],
175
+ };
176
+ });
177
+ // ─── aegis_execute ────────────────────────────────────────────────────────
178
+ server.registerTool('aegis_execute', {
179
+ title: 'Execute Command (Governed)',
180
+ 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.
181
+
182
+ Args:
183
+ - command (string): Shell command to execute
184
+ - cwd (string, optional): Working directory (defaults to project root)
185
+
186
+ Returns:
187
+ { "status": "success", "stdout": "...", "stderr": "..." } or { "status": "error", ... }`,
188
+ inputSchema: {
189
+ command: z.string().describe('Shell command to execute'),
190
+ cwd: z.string().optional().describe('Working directory (defaults to project root)'),
191
+ },
192
+ annotations: {
193
+ readOnlyHint: false,
194
+ destructiveHint: true,
195
+ idempotentHint: false,
196
+ openWorldHint: true,
197
+ },
198
+ }, async ({ command, cwd }) => {
199
+ const state = getState();
200
+ try {
201
+ const result = execSync(command, {
202
+ cwd: cwd ?? state.projectRoot,
203
+ encoding: 'utf-8',
204
+ timeout: 60_000,
205
+ maxBuffer: 1024 * 1024 * 10,
206
+ });
207
+ return {
208
+ content: [{
209
+ type: 'text',
210
+ text: JSON.stringify({ status: 'success', stdout: result, stderr: '' }),
211
+ }],
212
+ };
213
+ }
214
+ catch (err) {
215
+ const execErr = err;
216
+ return {
217
+ isError: true,
218
+ content: [{
219
+ type: 'text',
220
+ text: JSON.stringify({
221
+ status: 'error',
222
+ stdout: execErr.stdout ?? '',
223
+ stderr: execErr.stderr ?? execErr.message ?? 'Unknown error',
224
+ }),
225
+ }],
226
+ };
227
+ }
228
+ });
229
+ // ─── aegis_complete_task ──────────────────────────────────────────────────
230
+ server.registerTool('aegis_complete_task', {
231
+ title: 'Complete Task',
232
+ 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.
233
+
234
+ Args:
235
+ - task_id (string): Identifier for the task being completed
236
+ - summary (string): Brief summary of what was accomplished
237
+
238
+ Returns:
239
+ { "status": "passed", "gates_run": [...] } or { "status": "failed", "failures": [...] }`,
240
+ inputSchema: {
241
+ task_id: z.string().describe('Task identifier'),
242
+ summary: z.string().describe('Summary of completed work'),
243
+ },
244
+ annotations: {
245
+ readOnlyHint: false,
246
+ destructiveHint: false,
247
+ idempotentHint: true,
248
+ openWorldHint: false,
249
+ },
250
+ }, async ({ task_id, summary }) => {
251
+ const engine = getEngine();
252
+ const state = getState();
253
+ const gates = engine.getQualityGateCommands();
254
+ if (gates.length === 0) {
255
+ return {
256
+ content: [{
257
+ type: 'text',
258
+ text: JSON.stringify({
259
+ status: 'passed',
260
+ task_id,
261
+ summary,
262
+ gates_run: [],
263
+ message: 'No quality gates configured with matching build commands.',
264
+ }),
265
+ }],
266
+ };
267
+ }
268
+ const results = [];
269
+ for (const gate of gates) {
270
+ try {
271
+ const output = execSync(gate.command, {
272
+ cwd: state.projectRoot,
273
+ encoding: 'utf-8',
274
+ timeout: 120_000,
275
+ });
276
+ results.push({ name: gate.name, passed: true, output: output.slice(0, 500) });
277
+ }
278
+ catch (err) {
279
+ const execErr = err;
280
+ results.push({
281
+ name: gate.name,
282
+ passed: false,
283
+ output: (execErr.stderr ?? execErr.message ?? 'Failed').slice(0, 500),
284
+ });
285
+ }
286
+ }
287
+ const allPassed = results.every((r) => r.passed);
288
+ return {
289
+ content: [{
290
+ type: 'text',
291
+ text: JSON.stringify({
292
+ status: allPassed ? 'passed' : 'failed',
293
+ task_id,
294
+ summary,
295
+ gates_run: results,
296
+ }),
297
+ }],
298
+ };
299
+ });
300
+ // ─── aegis_policy_summary ─────────────────────────────────────────────────
301
+ server.registerTool('aegis_policy_summary', {
302
+ title: 'Policy Summary',
303
+ 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.
304
+
305
+ Returns:
306
+ { "role": "...", "writable_paths": [...], "forbidden_actions": [...], ... }`,
307
+ inputSchema: {},
308
+ annotations: {
309
+ readOnlyHint: true,
310
+ destructiveHint: false,
311
+ idempotentHint: true,
312
+ openWorldHint: false,
313
+ },
314
+ }, async () => {
315
+ const role = getRole();
316
+ const state = getState();
317
+ const protocol = state.governance.override_protocol;
318
+ const summary = {
319
+ role: role.id,
320
+ role_name: role.name,
321
+ purpose: role.purpose,
322
+ autonomy: role.autonomy,
323
+ writable_paths: role.writable_paths,
324
+ secondary_paths: role.secondary_paths,
325
+ excluded_paths: role.excluded_paths,
326
+ readable_paths: role.readable_paths,
327
+ forbidden_actions: role.forbidden_actions,
328
+ governance_forbidden_paths: state.governance.permissions.boundaries.forbidden ?? [],
329
+ override_behavior: protocol?.behavior ?? 'warn_confirm_and_log',
330
+ immutable_policies: protocol?.immutable_policies ?? [],
331
+ quality_gates: {
332
+ must_pass_tests: state.governance.quality_gate.pre_commit.must_pass_tests ?? false,
333
+ must_pass_lint: state.governance.quality_gate.pre_commit.must_pass_lint ?? false,
334
+ must_pass_typecheck: state.governance.quality_gate.pre_commit.must_pass_typecheck ?? false,
335
+ },
336
+ };
337
+ return {
338
+ content: [{
339
+ type: 'text',
340
+ text: JSON.stringify(summary),
341
+ }],
342
+ };
343
+ });
344
+ }
345
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
346
+ function toAbsolute(path, projectRoot) {
347
+ return isAbsolute(path) ? path : join(projectRoot, path);
348
+ }
349
+ function blocked(reason) {
350
+ return {
351
+ isError: true,
352
+ content: [{
353
+ type: 'text',
354
+ text: JSON.stringify({ status: 'blocked', reason }),
355
+ }],
356
+ };
357
+ }
358
+ async function logBlocked(engine, role, path, operation, reason) {
359
+ await engine.logOverride({
360
+ timestamp: new Date().toISOString(),
361
+ policy_violated: reason,
362
+ policy_text: reason,
363
+ action_requested: `${operation}: ${path}`,
364
+ human_confirmed: false,
365
+ agent_role: role.id,
366
+ rationale: 'Blocked by enforcement layer',
367
+ });
368
+ }
369
+ //# sourceMappingURL=file-tools.js.map