aegis-bridge 2.15.2 → 2.15.3

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/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  <img src="https://img.shields.io/github/actions/workflow/status/OneStepAt4time/aegis/ci.yml?branch=main" alt="CI" />
8
8
  <img src="https://img.shields.io/npm/l/aegis-bridge.svg" alt="license" />
9
9
  <img src="https://img.shields.io/badge/node-%3E%3D20.0.0-blue.svg" alt="node" />
10
+ <img src="https://img.shields.io/badge/MCP-ready-green.svg" alt="MCP ready" />
10
11
  </p>
11
12
 
12
13
  <p align="center">
@@ -96,6 +97,14 @@ Or via `.mcp.json`:
96
97
 
97
98
  **3 prompts** — `implement_issue`, `review_pr`, `debug_session`
98
99
 
100
+ ## Ecosystem Integrations
101
+
102
+ Aegis works beyond Claude Code anywhere an MCP host can launch a local stdio server.
103
+
104
+ - [Cursor integration](docs/integrations/cursor.md)
105
+ - [Windsurf integration](docs/integrations/windsurf.md)
106
+ - [MCP Registry preparation](docs/integrations/mcp-registry.md)
107
+
99
108
  ---
100
109
 
101
110
  ## REST API
@@ -0,0 +1,16 @@
1
+ export type ConsensusFocusArea = 'correctness' | 'security' | 'performance';
2
+ export interface ConsensusRequest {
3
+ id: string;
4
+ targetSessionId: string;
5
+ reviewerIds: string[];
6
+ focusAreas: ConsensusFocusArea[];
7
+ status: 'running' | 'completed' | 'failed';
8
+ createdAt: number;
9
+ }
10
+ export interface ConsensusReview {
11
+ reviewerId: string;
12
+ focusArea: ConsensusFocusArea;
13
+ findings: string[];
14
+ }
15
+ export declare function buildConsensusPrompt(targetSessionId: string, focusArea: ConsensusFocusArea): string;
16
+ export declare function mergeConsensusFindings(reviews: ConsensusReview[]): string[];
@@ -0,0 +1,19 @@
1
+ export function buildConsensusPrompt(targetSessionId, focusArea) {
2
+ return [
3
+ `Review Aegis session ${targetSessionId}.`,
4
+ `Focus area: ${focusArea}.`,
5
+ 'Return concise findings ordered by severity.',
6
+ 'Prefer concrete regressions, risks, and missing verification.',
7
+ ].join(' ');
8
+ }
9
+ export function mergeConsensusFindings(reviews) {
10
+ const merged = new Set();
11
+ for (const review of reviews) {
12
+ for (const finding of review.findings) {
13
+ const normalized = finding.trim();
14
+ if (normalized)
15
+ merged.add(normalized);
16
+ }
17
+ }
18
+ return Array.from(merged.values());
19
+ }
package/dist/hooks.js CHANGED
@@ -15,6 +15,7 @@
15
15
  * Issue #169: Phase 3 — Hook-driven status detection.
16
16
  */
17
17
  import { isValidUUID, hookBodySchema, parseIntSafe } from './validation.js';
18
+ import { evaluatePermissionProfile } from './permission-evaluator.js';
18
19
  /** CC hook events that require a decision response. */
19
20
  const DECISION_EVENTS = new Set(['PreToolUse', 'PermissionRequest']);
20
21
  /** Permission modes that should be auto-approved via hook response. */
@@ -289,6 +290,37 @@ export function registerHookRoutes(app, deps) {
289
290
  // Timeout: allow without answer (CC shows question to user in terminal)
290
291
  console.log(`Hooks: AskUserQuestion timeout for session ${sessionId} — allowing without answer`);
291
292
  }
293
+ if (session.permissionProfile) {
294
+ const evaluation = evaluatePermissionProfile(session.permissionProfile, {
295
+ toolName,
296
+ toolInput: hookBody.tool_input,
297
+ });
298
+ if (evaluation.behavior === 'deny') {
299
+ deps.eventBus.emit(sessionId, {
300
+ event: 'permission_denied',
301
+ sessionId,
302
+ timestamp: new Date().toISOString(),
303
+ data: { toolName, reason: evaluation.reason },
304
+ });
305
+ return reply.status(200).send({
306
+ hookSpecificOutput: {
307
+ hookEventName: 'PreToolUse',
308
+ permissionDecision: 'deny',
309
+ reason: evaluation.reason,
310
+ },
311
+ });
312
+ }
313
+ if (evaluation.behavior === 'ask') {
314
+ deps.eventBus.emitApproval(sessionId, `Permission profile requires approval for ${toolName}`);
315
+ const decision = await deps.sessions.waitForPermissionDecision(sessionId, PERMISSION_TIMEOUT_MS, toolName, evaluation.reason);
316
+ return reply.status(200).send({
317
+ hookSpecificOutput: {
318
+ hookEventName: 'PreToolUse',
319
+ permissionDecision: decision,
320
+ },
321
+ });
322
+ }
323
+ }
292
324
  // Default: allow without modification
293
325
  return reply.status(200).send({
294
326
  hookSpecificOutput: {
@@ -0,0 +1,10 @@
1
+ import type { PermissionProfile } from './validation.js';
2
+ export interface PermissionEvaluationInput {
3
+ toolName: string;
4
+ toolInput?: Record<string, unknown>;
5
+ }
6
+ export interface PermissionEvaluationResult {
7
+ behavior: 'allow' | 'deny' | 'ask';
8
+ reason: string;
9
+ }
10
+ export declare function evaluatePermissionProfile(profile: PermissionProfile, input: PermissionEvaluationInput): PermissionEvaluationResult;
@@ -0,0 +1,48 @@
1
+ function globToRegExp(pattern) {
2
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
3
+ return new RegExp(`^${escaped}$`, 'i');
4
+ }
5
+ function extractCandidatePaths(toolInput) {
6
+ if (!toolInput)
7
+ return [];
8
+ const values = [toolInput.path, toolInput.file_path, toolInput.target, ...(Array.isArray(toolInput.paths) ? toolInput.paths : [])];
9
+ return values.filter((v) => typeof v === 'string');
10
+ }
11
+ function extractContentSize(toolInput) {
12
+ const content = toolInput?.content;
13
+ return typeof content === 'string' ? content.length : null;
14
+ }
15
+ function isLikelyWriteTool(toolName) {
16
+ return /write|edit|delete|rename|move|create/i.test(toolName);
17
+ }
18
+ export function evaluatePermissionProfile(profile, input) {
19
+ for (const rule of profile.rules) {
20
+ if (rule.tool !== input.toolName)
21
+ continue;
22
+ if (rule.pattern) {
23
+ const candidate = typeof input.toolInput?.command === 'string'
24
+ ? input.toolInput.command
25
+ : JSON.stringify(input.toolInput ?? {});
26
+ if (!globToRegExp(rule.pattern).test(candidate))
27
+ continue;
28
+ }
29
+ if (rule.constraints?.readOnly && isLikelyWriteTool(input.toolName)) {
30
+ return { behavior: 'deny', reason: `Denied by readOnly constraint for ${input.toolName}` };
31
+ }
32
+ if (rule.constraints?.paths && rule.constraints.paths.length > 0) {
33
+ const paths = extractCandidatePaths(input.toolInput);
34
+ const allowed = paths.every((candidate) => rule.constraints.paths.some((prefix) => candidate.startsWith(prefix)));
35
+ if (!allowed) {
36
+ return { behavior: 'deny', reason: `Denied by path constraint for ${input.toolName}` };
37
+ }
38
+ }
39
+ if (rule.constraints?.maxFileSize) {
40
+ const size = extractContentSize(input.toolInput);
41
+ if (size !== null && size > rule.constraints.maxFileSize) {
42
+ return { behavior: 'deny', reason: `Denied by maxFileSize constraint for ${input.toolName}` };
43
+ }
44
+ }
45
+ return { behavior: rule.behavior, reason: `Matched rule for ${input.toolName}` };
46
+ }
47
+ return { behavior: profile.defaultBehavior, reason: 'No matching permission rule' };
48
+ }
package/dist/server.js CHANGED
@@ -37,6 +37,7 @@ import { registerHookRoutes } from './hooks.js';
37
37
  import { registerWsTerminalRoute } from './ws-terminal.js';
38
38
  import { registerMemoryRoutes } from './memory-routes.js';
39
39
  import { registerModelRouterRoutes } from './model-router.js';
40
+ import { buildConsensusPrompt } from './consensus.js';
40
41
  import * as templateStore from './template-store.js';
41
42
  import { SwarmMonitor } from './swarm-monitor.js';
42
43
  import { killAllSessions } from './signal-cleanup-helper.js';
@@ -48,9 +49,10 @@ import { MemoryBridge } from './memory-bridge.js';
48
49
  import { cleanupTerminatedSessionState } from './session-cleanup.js';
49
50
  import { normalizeApiErrorPayload } from './api-error-envelope.js';
50
51
  import { listenWithRetry, removePidFile, writePidFile } from './startup.js';
51
- import { authKeySchema, sendMessageSchema, commandSchema, bashSchema, screenshotSchema, permissionHookSchema, stopHookSchema, batchSessionSchema, pipelineSchema, handshakeRequestSchema, parseIntSafe, isValidUUID, compareSemver, extractCCVersion, MIN_CC_VERSION, } from './validation.js';
52
+ import { authKeySchema, sendMessageSchema, commandSchema, bashSchema, screenshotSchema, permissionHookSchema, stopHookSchema, batchSessionSchema, pipelineSchema, handshakeRequestSchema, parseIntSafe, isValidUUID, compareSemver, extractCCVersion, MIN_CC_VERSION, permissionProfileSchema, } from './validation.js';
52
53
  const __filename = fileURLToPath(import.meta.url);
53
54
  const __dirname = path.dirname(__filename);
55
+ const consensusRequests = new Map();
54
56
  // ── Configuration ────────────────────────────────────────────────────
55
57
  // Issue #349: CSP policy for dashboard responses (shared between static and SPA fallback)
56
58
  const DASHBOARD_CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss:";
@@ -904,6 +906,48 @@ async function forkSessionHandler(req, reply) {
904
906
  }
905
907
  app.post('/v1/sessions/:id/fork', forkSessionHandler);
906
908
  app.post('/sessions/:id/fork', forkSessionHandler);
909
+ async function createConsensusHandler(req, reply) {
910
+ const targetSessionId = req.params.id;
911
+ const target = sessions.getSession(targetSessionId);
912
+ if (!target)
913
+ return reply.status(404).send({ error: 'Target session not found' });
914
+ const focusAreas = (req.body?.focusAreas && req.body.focusAreas.length > 0)
915
+ ? req.body.focusAreas
916
+ : ['correctness', 'security', 'performance'];
917
+ const reviewerCount = Math.min(5, Math.max(1, req.body?.reviewerCount ?? focusAreas.length));
918
+ const selectedFocus = focusAreas.slice(0, reviewerCount);
919
+ const reviewerIds = [];
920
+ for (let i = 0; i < selectedFocus.length; i += 1) {
921
+ const focus = selectedFocus[i];
922
+ const child = await sessions.createSession({
923
+ workDir: target.workDir,
924
+ name: `consensus-${focus}-${targetSessionId.slice(0, 6)}`,
925
+ parentId: targetSessionId,
926
+ permissionMode: target.permissionMode,
927
+ });
928
+ reviewerIds.push(child.id);
929
+ await sessions.sendInitialPrompt(child.id, buildConsensusPrompt(targetSessionId, focus));
930
+ }
931
+ const consensusId = crypto.randomUUID();
932
+ const record = {
933
+ id: consensusId,
934
+ targetSessionId,
935
+ reviewerIds,
936
+ focusAreas: selectedFocus,
937
+ status: 'running',
938
+ createdAt: Date.now(),
939
+ };
940
+ consensusRequests.set(consensusId, record);
941
+ return reply.status(202).send(record);
942
+ }
943
+ function getConsensusHandler(req, reply) {
944
+ const item = consensusRequests.get(req.params.id);
945
+ if (!item)
946
+ return reply.status(404).send({ error: 'Consensus request not found' });
947
+ return item;
948
+ }
949
+ app.post('/v1/sessions/:id/consensus', createConsensusHandler);
950
+ app.get('/v1/consensus/:id', getConsensusHandler);
907
951
  async function getPermissionPolicyHandler(req, reply) {
908
952
  const sessionId = req.params.id;
909
953
  const session = sessions.getSession(sessionId);
@@ -928,6 +972,29 @@ app.get('/v1/sessions/:id/permissions', getPermissionPolicyHandler);
928
972
  app.put('/v1/sessions/:id/permissions', updatePermissionPolicyHandler);
929
973
  app.get('/sessions/:id/permissions', getPermissionPolicyHandler);
930
974
  app.put('/sessions/:id/permissions', updatePermissionPolicyHandler);
975
+ async function getPermissionProfileHandler(req, reply) {
976
+ const sessionId = req.params.id;
977
+ const session = sessions.getSession(sessionId);
978
+ if (!session)
979
+ return reply.status(404).send({ error: 'Session not found' });
980
+ return { permissionProfile: session.permissionProfile ?? null };
981
+ }
982
+ async function updatePermissionProfileHandler(req, reply) {
983
+ const sessionId = req.params.id;
984
+ const session = sessions.getSession(sessionId);
985
+ if (!session)
986
+ return reply.status(404).send({ error: 'Session not found' });
987
+ const parsed = permissionProfileSchema.safeParse(req.body ?? {});
988
+ if (!parsed.success)
989
+ return reply.status(400).send({ error: 'Invalid permission profile', details: parsed.error.issues });
990
+ session.permissionProfile = parsed.data;
991
+ await sessions.save();
992
+ return { permissionProfile: parsed.data };
993
+ }
994
+ app.get('/v1/sessions/:id/permission-profile', getPermissionProfileHandler);
995
+ app.put('/v1/sessions/:id/permission-profile', updatePermissionProfileHandler);
996
+ app.get('/sessions/:id/permission-profile', getPermissionProfileHandler);
997
+ app.put('/sessions/:id/permission-profile', updatePermissionProfileHandler);
931
998
  // Read messages
932
999
  async function readMessagesHandler(req, reply) {
933
1000
  try {
package/dist/session.d.ts CHANGED
@@ -8,7 +8,7 @@ import { TmuxManager } from './tmux.js';
8
8
  import { type ParsedEntry } from './transcript.js';
9
9
  import { type UIState } from './terminal-parser.js';
10
10
  import type { Config } from './config.js';
11
- import { type PermissionPolicy } from './validation.js';
11
+ import { type PermissionPolicy, type PermissionProfile } from './validation.js';
12
12
  import { type PermissionDecision } from './permission-request-manager.js';
13
13
  export interface SessionInfo {
14
14
  id: string;
@@ -40,6 +40,7 @@ export interface SessionInfo {
40
40
  parentId?: string;
41
41
  children?: string[];
42
42
  permissionPolicy?: PermissionPolicy;
43
+ permissionProfile?: PermissionProfile;
43
44
  prd?: string;
44
45
  }
45
46
  export interface SessionState {
@@ -141,6 +141,48 @@ export declare const permissionRuleSchema: z.ZodObject<{
141
141
  commandPattern: z.ZodOptional<z.ZodString>;
142
142
  }, z.core.$strip>;
143
143
  export type PermissionPolicy = z.infer<typeof permissionRuleSchema>[];
144
+ /** Issue #742: richer per-session permission profile. */
145
+ export declare const permissionConstraintSchema: z.ZodObject<{
146
+ readOnly: z.ZodOptional<z.ZodBoolean>;
147
+ paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
148
+ maxFileSize: z.ZodOptional<z.ZodNumber>;
149
+ }, z.core.$strict>;
150
+ export declare const permissionProfileRuleSchema: z.ZodObject<{
151
+ tool: z.ZodString;
152
+ behavior: z.ZodEnum<{
153
+ allow: "allow";
154
+ deny: "deny";
155
+ ask: "ask";
156
+ }>;
157
+ pattern: z.ZodOptional<z.ZodString>;
158
+ constraints: z.ZodOptional<z.ZodObject<{
159
+ readOnly: z.ZodOptional<z.ZodBoolean>;
160
+ paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
161
+ maxFileSize: z.ZodOptional<z.ZodNumber>;
162
+ }, z.core.$strict>>;
163
+ }, z.core.$strict>;
164
+ export declare const permissionProfileSchema: z.ZodObject<{
165
+ defaultBehavior: z.ZodEnum<{
166
+ allow: "allow";
167
+ deny: "deny";
168
+ ask: "ask";
169
+ }>;
170
+ rules: z.ZodArray<z.ZodObject<{
171
+ tool: z.ZodString;
172
+ behavior: z.ZodEnum<{
173
+ allow: "allow";
174
+ deny: "deny";
175
+ ask: "ask";
176
+ }>;
177
+ pattern: z.ZodOptional<z.ZodString>;
178
+ constraints: z.ZodOptional<z.ZodObject<{
179
+ readOnly: z.ZodOptional<z.ZodBoolean>;
180
+ paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
181
+ maxFileSize: z.ZodOptional<z.ZodNumber>;
182
+ }, z.core.$strict>>;
183
+ }, z.core.$strict>>;
184
+ }, z.core.$strict>;
185
+ export type PermissionProfile = z.infer<typeof permissionProfileSchema>;
144
186
  /** Schema for persisted SessionState (sessions: { [id]: SessionInfo }). */
145
187
  export declare const persistedStateSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
146
188
  id: z.ZodString;
@@ -206,6 +248,27 @@ export declare const persistedStateSchema: z.ZodRecord<z.ZodString, z.ZodObject<
206
248
  toolName: z.ZodOptional<z.ZodString>;
207
249
  commandPattern: z.ZodOptional<z.ZodString>;
208
250
  }, z.core.$strip>>>;
251
+ permissionProfile: z.ZodOptional<z.ZodObject<{
252
+ defaultBehavior: z.ZodEnum<{
253
+ allow: "allow";
254
+ deny: "deny";
255
+ ask: "ask";
256
+ }>;
257
+ rules: z.ZodArray<z.ZodObject<{
258
+ tool: z.ZodString;
259
+ behavior: z.ZodEnum<{
260
+ allow: "allow";
261
+ deny: "deny";
262
+ ask: "ask";
263
+ }>;
264
+ pattern: z.ZodOptional<z.ZodString>;
265
+ constraints: z.ZodOptional<z.ZodObject<{
266
+ readOnly: z.ZodOptional<z.ZodBoolean>;
267
+ paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
268
+ maxFileSize: z.ZodOptional<z.ZodNumber>;
269
+ }, z.core.$strict>>;
270
+ }, z.core.$strict>>;
271
+ }, z.core.$strict>>;
209
272
  }, z.core.$strip>>;
210
273
  /** Schema for a single continuation pointer entry in session_map.json (Issue #900). */
211
274
  export declare const sessionMapEntrySchema: z.ZodObject<{
@@ -138,6 +138,22 @@ export const permissionRuleSchema = z.object({
138
138
  toolName: z.string().optional(),
139
139
  commandPattern: z.string().optional(),
140
140
  });
141
+ /** Issue #742: richer per-session permission profile. */
142
+ export const permissionConstraintSchema = z.object({
143
+ readOnly: z.boolean().optional(),
144
+ paths: z.array(z.string().min(1)).max(50).optional(),
145
+ maxFileSize: z.number().int().positive().max(10_000_000).optional(),
146
+ }).strict();
147
+ export const permissionProfileRuleSchema = z.object({
148
+ tool: z.string().min(1),
149
+ behavior: z.enum(['allow', 'deny', 'ask']),
150
+ pattern: z.string().optional(),
151
+ constraints: permissionConstraintSchema.optional(),
152
+ }).strict();
153
+ export const permissionProfileSchema = z.object({
154
+ defaultBehavior: z.enum(['allow', 'deny', 'ask']),
155
+ rules: z.array(permissionProfileRuleSchema).max(100),
156
+ }).strict();
141
157
  /** Schema for persisted SessionState (sessions: { [id]: SessionInfo }). */
142
158
  export const persistedStateSchema = z.record(z.string(), z.object({
143
159
  id: z.string(),
@@ -173,6 +189,7 @@ export const persistedStateSchema = z.record(z.string(), z.object({
173
189
  toolName: z.string().optional(),
174
190
  commandPattern: z.string().optional(),
175
191
  })).optional(),
192
+ permissionProfile: permissionProfileSchema.optional(),
176
193
  }));
177
194
  /** Schema for a single continuation pointer entry in session_map.json (Issue #900). */
178
195
  export const sessionMapEntrySchema = z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aegis-bridge",
3
- "version": "2.15.2",
3
+ "version": "2.15.3",
4
4
  "type": "module",
5
5
  "description": "Orchestrate Claude Code sessions via API. Create, brief, monitor, refine, ship.",
6
6
  "main": "dist/server.js",