aegis-bridge 2.6.4 → 2.7.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.
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Aegis Dashboard</title>
7
7
  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🛡️</text></svg>" />
8
- <script type="module" crossorigin src="/dashboard/assets/index-G8fziBeQ.js"></script>
8
+ <script type="module" crossorigin src="/dashboard/assets/index-SoKhTCVa.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/dashboard/assets/index-9Hkkvm_I.css">
10
10
  </head>
11
11
  <body class="bg-[#0a0a0f] text-gray-200 antialiased">
@@ -19,12 +19,11 @@
19
19
  * for Aegis status detection and event forwarding.
20
20
  *
21
21
  * Excluded (low value for Aegis):
22
- * - InstructionsLoaded, ConfigChange, CwdChanged, FileChanged (informational)
22
+ * - InstructionsLoaded, ConfigChange (informational)
23
23
  * - WorktreeCreate, WorktreeRemove (worktree management)
24
24
  * - Elicitation, ElicitationResult (MCP-specific)
25
- * - PreCompact, PostCompact (internal optimization)
26
25
  */
27
- declare const HTTP_HOOK_EVENTS: readonly ["Stop", "StopFailure", "PreToolUse", "PostToolUse", "PostToolUseFailure", "PermissionRequest", "TaskCompleted", "SessionStart", "SessionEnd", "UserPromptSubmit", "SubagentStart", "SubagentStop", "Notification", "TeammateIdle"];
26
+ declare const HTTP_HOOK_EVENTS: readonly ["Stop", "StopFailure", "PreToolUse", "PostToolUse", "PostToolUseFailure", "PermissionRequest", "TaskCompleted", "SessionStart", "SessionEnd", "UserPromptSubmit", "SubagentStart", "SubagentStop", "PreCompact", "PostCompact", "FileChanged", "CwdChanged", "Notification", "TeammateIdle"];
28
27
  export { HTTP_HOOK_EVENTS };
29
28
  export type HttpHookEvent = typeof HTTP_HOOK_EVENTS[number];
30
29
  /** Shape of a single HTTP hook entry in CC settings.json. */
@@ -66,3 +65,13 @@ export declare function writeHookSettingsFile(baseUrl: string, sessionId: string
66
65
  * @param filePath - Path to the temporary settings file
67
66
  */
68
67
  export declare function cleanupHookSettingsFile(filePath: string): Promise<void>;
68
+ /**
69
+ * Issue #936: Clean stale session hooks from settings.local.json before writing new hooks.
70
+ *
71
+ * When sessions die, their hook URLs remain in settings.local.json.
72
+ * On restart, CC loads these dead hooks and crashes.
73
+ *
74
+ * @param workDir - Project working directory
75
+ * @param activeSessionIds - Set of currently active session IDs
76
+ */
77
+ export declare function cleanupStaleSessionHooks(workDir: string, activeSessionIds: Set<string>): Promise<void>;
@@ -43,10 +43,9 @@ function validateWorkDirPath(workDir) {
43
43
  * for Aegis status detection and event forwarding.
44
44
  *
45
45
  * Excluded (low value for Aegis):
46
- * - InstructionsLoaded, ConfigChange, CwdChanged, FileChanged (informational)
46
+ * - InstructionsLoaded, ConfigChange (informational)
47
47
  * - WorktreeCreate, WorktreeRemove (worktree management)
48
48
  * - Elicitation, ElicitationResult (MCP-specific)
49
- * - PreCompact, PostCompact (internal optimization)
50
49
  */
51
50
  const HTTP_HOOK_EVENTS = [
52
51
  // Status detection (highest value)
@@ -64,6 +63,12 @@ const HTTP_HOOK_EVENTS = [
64
63
  // Subagent tracking
65
64
  'SubagentStart',
66
65
  'SubagentStop',
66
+ // Context management
67
+ 'PreCompact',
68
+ 'PostCompact',
69
+ // File & directory changes
70
+ 'FileChanged',
71
+ 'CwdChanged',
67
72
  // Notifications
68
73
  'Notification',
69
74
  'TeammateIdle',
@@ -168,3 +173,55 @@ export async function cleanupHookSettingsFile(filePath) {
168
173
  // Non-fatal: temp file cleanup failed
169
174
  }
170
175
  }
176
+ /**
177
+ * Issue #936: Clean stale session hooks from settings.local.json before writing new hooks.
178
+ *
179
+ * When sessions die, their hook URLs remain in settings.local.json.
180
+ * On restart, CC loads these dead hooks and crashes.
181
+ *
182
+ * @param workDir - Project working directory
183
+ * @param activeSessionIds - Set of currently active session IDs
184
+ */
185
+ export async function cleanupStaleSessionHooks(workDir, activeSessionIds) {
186
+ const safeWorkDir = workDir ? validateWorkDirPath(workDir) : undefined;
187
+ if (!safeWorkDir)
188
+ return;
189
+ const projectSettingsPath = join(safeWorkDir, '.claude', 'settings.local.json');
190
+ if (!existsSync(projectSettingsPath))
191
+ return;
192
+ try {
193
+ const raw = await readFile(projectSettingsPath, 'utf-8');
194
+ const parsed = ccSettingsSchema.safeParse(JSON.parse(raw));
195
+ if (!parsed.success)
196
+ return;
197
+ const settings = parsed.data;
198
+ const hooks = settings.hooks;
199
+ if (!hooks)
200
+ return;
201
+ let changed = false;
202
+ for (const [event, eventHooks] of Object.entries(hooks)) {
203
+ const filtered = eventHooks.filter(entry => {
204
+ const httpHook = entry.hooks?.find(h => h.type === 'http');
205
+ if (!httpHook)
206
+ return true;
207
+ const url = httpHook.url;
208
+ const match = url.match(/[?&]sessionId=([^&]+)/);
209
+ if (!match)
210
+ return true;
211
+ const sessionId = match[1];
212
+ if (!activeSessionIds.has(sessionId)) {
213
+ changed = true;
214
+ return false;
215
+ }
216
+ return true;
217
+ });
218
+ hooks[event] = filtered;
219
+ }
220
+ if (changed) {
221
+ await writeFile(projectSettingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
222
+ }
223
+ }
224
+ catch {
225
+ // Non-fatal: cleanup failed
226
+ }
227
+ }
package/dist/server.js CHANGED
@@ -24,11 +24,12 @@ import { ChannelManager, TelegramChannel, WebhookChannel, } from './channels/ind
24
24
  import { loadConfig } from './config.js';
25
25
  import { captureScreenshot, isPlaywrightAvailable } from './screenshot.js';
26
26
  import { validateScreenshotUrl, resolveAndCheckIp, buildHostResolverRule } from './ssrf.js';
27
- import { validateWorkDir } from './validation.js';
27
+ import { validateWorkDir, permissionRuleSchema } from './validation.js';
28
28
  import { SessionEventBus } from './events.js';
29
29
  import { SSEWriter } from './sse-writer.js';
30
30
  import { SSEConnectionLimiter } from './sse-limiter.js';
31
31
  import { PipelineManager } from './pipeline.js';
32
+ import { ToolRegistry } from './tool-registry.js';
32
33
  import { AuthManager } from './auth.js';
33
34
  import { MetricsCollector } from './metrics.js';
34
35
  import { registerPermissionRoutes } from './permission-routes.js';
@@ -57,6 +58,7 @@ const channels = new ChannelManager();
57
58
  const eventBus = new SessionEventBus();
58
59
  let sseLimiter;
59
60
  let pipelines;
61
+ let toolRegistry;
60
62
  let auth;
61
63
  let metrics;
62
64
  let swarmMonitor;
@@ -320,6 +322,7 @@ const createSessionSchema = z.object({
320
322
  stallThresholdMs: z.number().int().positive().max(3_600_000).optional(),
321
323
  permissionMode: z.enum(['default', 'bypassPermissions', 'plan', 'acceptEdits', 'dontAsk', 'auto']).optional(),
322
324
  autoApprove: z.boolean().optional(),
325
+ parentId: z.string().uuid().optional(),
323
326
  }).strict();
324
327
  // Health — Issue #397: includes tmux server health check
325
328
  async function healthHandler() {
@@ -420,6 +423,29 @@ app.get('/v1/sessions/:id/metrics', async (req, reply) => {
420
423
  return reply.status(404).send({ error: 'No metrics for this session' });
421
424
  return m;
422
425
  });
426
+ // Issue #704: Tool usage endpoints
427
+ app.get('/v1/sessions/:id/tools', async (req, reply) => {
428
+ const session = sessions.getSession(req.params.id);
429
+ if (!session)
430
+ return reply.status(404).send({ error: 'Session not found' });
431
+ // Parse JSONL on-demand for tool usage
432
+ const { readNewEntries } = await import('./transcript.js');
433
+ if (session.jsonlPath) {
434
+ try {
435
+ const result = await readNewEntries(session.jsonlPath, 0);
436
+ const entries = result.entries;
437
+ toolRegistry.processEntries(req.params.id, entries);
438
+ }
439
+ catch { /* JSONL not available */ }
440
+ }
441
+ const tools = toolRegistry.getSessionTools(req.params.id);
442
+ return { sessionId: req.params.id, tools, totalCalls: tools.reduce((sum, t) => sum + t.count, 0) };
443
+ });
444
+ app.get('/v1/tools', async () => {
445
+ const definitions = toolRegistry.getToolDefinitions();
446
+ const categories = [...new Set(definitions.map(t => t.category))];
447
+ return { tools: definitions, categories, totalTools: definitions.length };
448
+ });
423
449
  // Issue #89 L14: Webhook dead letter queue
424
450
  app.get('/v1/webhooks/dead-letter', async () => {
425
451
  for (const ch of channels.getChannels()) {
@@ -550,7 +576,7 @@ async function createSessionHandler(req, reply) {
550
576
  if (!parsed.success) {
551
577
  return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
552
578
  }
553
- const { workDir, name, prompt, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove } = parsed.data;
579
+ const { workDir, name, prompt, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove, parentId } = parsed.data;
554
580
  if (!workDir)
555
581
  return reply.status(400).send({ error: 'workDir is required' });
556
582
  // Issue #564: Validate installed Claude Code version
@@ -588,7 +614,7 @@ async function createSessionHandler(req, reply) {
588
614
  }
589
615
  }
590
616
  console.time("POST_CREATE_SESSION");
591
- const session = await sessions.createSession({ workDir: safeWorkDir, name, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove });
617
+ const session = await sessions.createSession({ workDir: safeWorkDir, name, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove, parentId });
592
618
  console.timeEnd("POST_CREATE_SESSION");
593
619
  console.time("POST_CHANNEL_CREATED");
594
620
  // Issue #625: Track session in metrics so sessionsCreated counter is accurate
@@ -682,6 +708,61 @@ async function sendMessageHandler(req, reply) {
682
708
  }
683
709
  app.post('/v1/sessions/:id/send', sendMessageHandler);
684
710
  app.post('/sessions/:id/send', sendMessageHandler);
711
+ // Issue #702: GET children sessions
712
+ async function getChildrenHandler(req, reply) {
713
+ const session = sessions.getSession(req.params.id);
714
+ if (!session)
715
+ return reply.status(404).send({ error: 'Session not found' });
716
+ const children = (session.children ?? []).map(id => {
717
+ const child = sessions.getSession(id);
718
+ if (!child)
719
+ return null;
720
+ return { id: child.id, windowName: child.windowName, status: child.status, createdAt: child.createdAt };
721
+ }).filter(Boolean);
722
+ return { children };
723
+ }
724
+ app.get('/v1/sessions/:id/children', getChildrenHandler);
725
+ app.get('/sessions/:id/children', getChildrenHandler);
726
+ async function spawnChildHandler(req, reply) {
727
+ const parentId = req.params.id;
728
+ const parent = sessions.getSession(parentId);
729
+ if (!parent)
730
+ return reply.status(404).send({ error: 'Parent session not found' });
731
+ const { name, prompt, workDir, permissionMode } = req.body ?? {};
732
+ const childName = name ?? `${parent.windowName ?? 'session'}-child`;
733
+ const childWorkDir = workDir ?? parent.workDir;
734
+ const childPermMode = permissionMode ?? parent.permissionMode ?? 'bypassPermissions';
735
+ const childSession = await sessions.createSession({ workDir: childWorkDir, name: childName, parentId, permissionMode: childPermMode });
736
+ let promptDelivery;
737
+ if (prompt) {
738
+ promptDelivery = await sessions.sendInitialPrompt(childSession.id, prompt);
739
+ }
740
+ return reply.status(201).send({ ...childSession, promptDelivery });
741
+ }
742
+ app.post('/v1/sessions/:id/spawn', spawnChildHandler);
743
+ app.post('/sessions/:id/spawn', spawnChildHandler);
744
+ async function getPermissionPolicyHandler(req, reply) {
745
+ const session = sessions.getSession(req.params.id);
746
+ if (!session)
747
+ return reply.status(404).send({ error: 'Session not found' });
748
+ return { permissionPolicy: session.permissionPolicy ?? [] };
749
+ }
750
+ async function updatePermissionPolicyHandler(req, reply) {
751
+ const session = sessions.getSession(req.params.id);
752
+ if (!session)
753
+ return reply.status(404).send({ error: 'Session not found' });
754
+ const policy = req.body ?? [];
755
+ const result = permissionRuleSchema.array().safeParse(policy);
756
+ if (!result.success)
757
+ return reply.status(400).send({ error: 'Invalid permission policy', details: result.error.issues });
758
+ session.permissionPolicy = policy;
759
+ await sessions.save();
760
+ return { permissionPolicy: policy };
761
+ }
762
+ app.get('/v1/sessions/:id/permissions', getPermissionPolicyHandler);
763
+ app.put('/v1/sessions/:id/permissions', updatePermissionPolicyHandler);
764
+ app.get('/sessions/:id/permissions', getPermissionPolicyHandler);
765
+ app.put('/sessions/:id/permissions', updatePermissionPolicyHandler);
685
766
  // Read messages
686
767
  async function readMessagesHandler(req, reply) {
687
768
  try {
@@ -1109,6 +1190,7 @@ async function reapStaleSessions(maxAgeMs) {
1109
1190
  });
1110
1191
  monitor.removeSession(session.id);
1111
1192
  metrics.cleanupSession(session.id);
1193
+ toolRegistry.cleanupSession(session.id);
1112
1194
  }
1113
1195
  catch (e) {
1114
1196
  console.error(`Reaper: failed to kill session ${session.id}:`, e);
@@ -1138,6 +1220,7 @@ async function reapZombieSessions() {
1138
1220
  eventBus.cleanupSession(session.id);
1139
1221
  await sessions.killSession(session.id);
1140
1222
  metrics.cleanupSession(session.id);
1223
+ toolRegistry.cleanupSession(session.id);
1141
1224
  await channels.sessionEnded({
1142
1225
  event: 'session.ended',
1143
1226
  timestamp: new Date().toISOString(),
@@ -1537,6 +1620,7 @@ async function main() {
1537
1620
  monitor.start();
1538
1621
  // Issue #81: Start swarm monitor for agent swarm awareness
1539
1622
  swarmMonitor = new SwarmMonitor(sessions);
1623
+ toolRegistry = new ToolRegistry();
1540
1624
  swarmMonitor.onEvent((event) => {
1541
1625
  if (!event.swarm.parentSession)
1542
1626
  return;
package/dist/session.d.ts CHANGED
@@ -8,6 +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
12
  export interface SessionInfo {
12
13
  id: string;
13
14
  windowId: string;
@@ -35,6 +36,9 @@ export interface SessionInfo {
35
36
  model?: string;
36
37
  lastDeadAt?: number;
37
38
  ccPid?: number;
39
+ parentId?: string;
40
+ children?: string[];
41
+ permissionPolicy?: PermissionPolicy;
38
42
  }
39
43
  export interface SessionState {
40
44
  sessions: Record<string, SessionInfo>;
@@ -136,6 +140,8 @@ export declare class SessionManager {
136
140
  permissionMode?: string;
137
141
  /** @deprecated Use permissionMode instead. Maps true→bypassPermissions, false→default. */
138
142
  autoApprove?: boolean;
143
+ /** Issue #702: Parent session ID for sub-agent hierarchy */
144
+ parentId?: string;
139
145
  }): Promise<SessionInfo>;
140
146
  /** Get a session by ID. */
141
147
  getSession(id: string): SessionInfo | null;
package/dist/session.js CHANGED
@@ -16,7 +16,7 @@ import { computeStallThreshold } from './config.js';
16
16
  import { neutralizeBypassPermissions, restoreSettings, cleanOrphanedBackup } from './permission-guard.js';
17
17
  import { persistedStateSchema } from './validation.js';
18
18
  import { loadContinuationPointers } from './continuation-pointer.js';
19
- import { writeHookSettingsFile, cleanupHookSettingsFile } from './hook-settings.js';
19
+ import { writeHookSettingsFile, cleanupHookSettingsFile, cleanupStaleSessionHooks } from './hook-settings.js';
20
20
  import { Mutex } from 'async-mutex';
21
21
  import { maybeInjectFault } from './fault-injection.js';
22
22
  /** Convert parsed JSON arrays to Sets for activeSubagents (#668). */
@@ -512,6 +512,17 @@ export class SessionManager {
512
512
  const hookSecret = randomBytes(32).toString('hex');
513
513
  // Issue #169 Phase 2: Generate HTTP hook settings for this session.
514
514
  // Writes a temp file with hooks pointing to Aegis's hook receiver.
515
+ // Issue #936: Clean stale session hooks from settings.local.json before writing new hooks.
516
+ // This prevents CC from loading dead hook URLs on restart.
517
+ try {
518
+ const activeIds = new Set(this.listSessions().map(s => s.id));
519
+ if (activeIds.size > 0) {
520
+ await cleanupStaleSessionHooks(opts.workDir, activeIds);
521
+ }
522
+ }
523
+ catch (e) {
524
+ console.warn(`Hook cleanup: failed to clean stale hooks: ${e.message}`);
525
+ }
515
526
  let hookSettingsFile;
516
527
  try {
517
528
  const baseUrl = `http://${this.config.host}:${this.config.port}`;
@@ -553,6 +564,16 @@ export class SessionManager {
553
564
  this.state.sessions[id] = session;
554
565
  this.invalidateSessionsListCache();
555
566
  await this.save();
567
+ // Issue #702: Register child with parent
568
+ if (opts.parentId) {
569
+ const parent = this.state.sessions[opts.parentId];
570
+ if (parent) {
571
+ if (!parent.children)
572
+ parent.children = [];
573
+ parent.children.push(id);
574
+ await this.save();
575
+ }
576
+ }
556
577
  // Issue #353: Fetch CC process PID for swarm parent matching.
557
578
  // Fire-and-forget — PID is not needed synchronously.
558
579
  // Issue #574: Add .catch() to prevent unhandled rejection if tmux fails mid-lookup.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * tool-registry.ts — Tool usage tracking and registry for CC tool introspection.
3
+ *
4
+ * Parses tool_use messages from JSONL transcripts to build per-session
5
+ * and global tool usage metrics. Exposes API endpoints for observability.
6
+ *
7
+ * Issue #704: Tool registry and schema validation for CC tool introspection.
8
+ */
9
+ import type { ParsedEntry } from './transcript.js';
10
+ /** Known CC tool definitions with metadata. */
11
+ export interface ToolDefinition {
12
+ name: string;
13
+ category: string;
14
+ description: string;
15
+ permissionLevel: string;
16
+ }
17
+ /** Per-tool usage stats within a session. */
18
+ export interface ToolUsageRecord {
19
+ name: string;
20
+ count: number;
21
+ lastUsedAt: number;
22
+ firstUsedAt: number;
23
+ errors: number;
24
+ }
25
+ /** Tool registry: known tools + per-session usage tracking. */
26
+ export declare class ToolRegistry {
27
+ private sessionUsage;
28
+ /** Built-in CC tool definitions (from CC src/tools/). */
29
+ private readonly tools;
30
+ /** Process parsed entries and extract tool usage. */
31
+ processEntries(sessionId: string, entries: ParsedEntry[]): void;
32
+ /** Get tool usage for a session, sorted by count descending. */
33
+ getSessionTools(sessionId: string): ToolUsageRecord[];
34
+ /** Get all known CC tool definitions. */
35
+ getToolDefinitions(): ToolDefinition[];
36
+ /** Get a tool definition by name. */
37
+ getToolDefinition(name: string): ToolDefinition | undefined;
38
+ /** Clean up session data. */
39
+ cleanupSession(sessionId: string): void;
40
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * tool-registry.ts — Tool usage tracking and registry for CC tool introspection.
3
+ *
4
+ * Parses tool_use messages from JSONL transcripts to build per-session
5
+ * and global tool usage metrics. Exposes API endpoints for observability.
6
+ *
7
+ * Issue #704: Tool registry and schema validation for CC tool introspection.
8
+ */
9
+ /** Tool registry: known tools + per-session usage tracking. */
10
+ export class ToolRegistry {
11
+ sessionUsage = new Map();
12
+ /** Built-in CC tool definitions (from CC src/tools/). */
13
+ tools = [
14
+ { name: 'Read', category: 'read', description: 'Read file contents', permissionLevel: 'read' },
15
+ { name: 'Write', category: 'write', description: 'Write file contents', permissionLevel: 'write' },
16
+ { name: 'Edit', category: 'edit', description: 'Edit file with search/replace', permissionLevel: 'edit' },
17
+ { name: 'MultiEdit', category: 'edit', description: 'Multiple edits in one operation', permissionLevel: 'edit' },
18
+ { name: 'Bash', category: 'bash', description: 'Execute shell commands', permissionLevel: 'bash' },
19
+ { name: 'Glob', category: 'search', description: 'Find files matching pattern', permissionLevel: 'read' },
20
+ { name: 'Grep', category: 'search', description: 'Search file contents', permissionLevel: 'read' },
21
+ { name: 'ListFiles', category: 'search', description: 'List directory contents', permissionLevel: 'read' },
22
+ { name: 'TodoWrite', category: 'edit', description: 'Update todo list', permissionLevel: 'edit' },
23
+ { name: 'TodoRead', category: 'read', description: 'Read todo list', permissionLevel: 'read' },
24
+ { name: 'WebFetch', category: 'read', description: 'Fetch web page content', permissionLevel: 'read' },
25
+ { name: 'NotebookRead', category: 'read', description: 'Read notebook cells', permissionLevel: 'read' },
26
+ { name: 'NotebookEdit', category: 'edit', description: 'Edit notebook cells', permissionLevel: 'edit' },
27
+ { name: 'AskUserQuestion', category: 'agent', description: 'Ask user for clarification', permissionLevel: 'read' },
28
+ { name: 'AgentTool', category: 'agent', description: 'Spawn sub-agent for parallel execution', permissionLevel: 'agent' },
29
+ { name: 'MCPTool', category: 'mcp', description: 'MCP server tool invocation', permissionLevel: 'mcp' },
30
+ ];
31
+ /** Process parsed entries and extract tool usage. */
32
+ processEntries(sessionId, entries) {
33
+ let usage = this.sessionUsage.get(sessionId);
34
+ if (!usage) {
35
+ usage = new Map();
36
+ this.sessionUsage.set(sessionId, usage);
37
+ }
38
+ const now = Date.now();
39
+ for (const entry of entries) {
40
+ if (entry.contentType === 'tool_use' && entry.toolName) {
41
+ const existing = usage.get(entry.toolName);
42
+ if (existing) {
43
+ existing.count++;
44
+ existing.lastUsedAt = now;
45
+ }
46
+ else {
47
+ usage.set(entry.toolName, {
48
+ name: entry.toolName,
49
+ count: 1,
50
+ lastUsedAt: now,
51
+ firstUsedAt: now,
52
+ errors: 0,
53
+ });
54
+ }
55
+ }
56
+ if (entry.contentType === 'tool_error' && entry.toolName) {
57
+ const existing = usage.get(entry.toolName);
58
+ if (existing) {
59
+ existing.errors++;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ /** Get tool usage for a session, sorted by count descending. */
65
+ getSessionTools(sessionId) {
66
+ const usage = this.sessionUsage.get(sessionId);
67
+ if (!usage)
68
+ return [];
69
+ return [...usage.values()].sort((a, b) => b.count - a.count);
70
+ }
71
+ /** Get all known CC tool definitions. */
72
+ getToolDefinitions() {
73
+ return [...this.tools];
74
+ }
75
+ /** Get a tool definition by name. */
76
+ getToolDefinition(name) {
77
+ return this.tools.find(t => t.name === name);
78
+ }
79
+ /** Clean up session data. */
80
+ cleanupSession(sessionId) {
81
+ this.sessionUsage.delete(sessionId);
82
+ }
83
+ }
@@ -123,6 +123,24 @@ export declare function clamp(value: number, min: number, max: number, fallback:
123
123
  export declare function parseIntSafe(value: string | undefined, fallback: number): number;
124
124
  /** Validate that a string looks like a UUID. */
125
125
  export declare function isValidUUID(id: string): boolean;
126
+ /** Issue #700: Permission Policy Schema */
127
+ export declare const permissionRuleSchema: z.ZodObject<{
128
+ source: z.ZodEnum<{
129
+ userSettings: "userSettings";
130
+ projectSettings: "projectSettings";
131
+ localSettings: "localSettings";
132
+ flagSettings: "flagSettings";
133
+ aegisApi: "aegisApi";
134
+ }>;
135
+ ruleBehavior: z.ZodEnum<{
136
+ allow: "allow";
137
+ deny: "deny";
138
+ ask: "ask";
139
+ }>;
140
+ toolName: z.ZodOptional<z.ZodString>;
141
+ commandPattern: z.ZodOptional<z.ZodString>;
142
+ }, z.core.$strip>;
143
+ export type PermissionPolicy = z.infer<typeof permissionRuleSchema>[];
126
144
  /** Schema for persisted SessionState (sessions: { [id]: SessionInfo }). */
127
145
  export declare const persistedStateSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
128
146
  id: z.ZodString;
@@ -170,6 +188,24 @@ export declare const persistedStateSchema: z.ZodRecord<z.ZodString, z.ZodObject<
170
188
  model: z.ZodOptional<z.ZodString>;
171
189
  lastDeadAt: z.ZodOptional<z.ZodNumber>;
172
190
  ccPid: z.ZodOptional<z.ZodNumber>;
191
+ parentId: z.ZodOptional<z.ZodString>;
192
+ children: z.ZodOptional<z.ZodArray<z.ZodString>>;
193
+ permissionPolicy: z.ZodOptional<z.ZodArray<z.ZodObject<{
194
+ source: z.ZodEnum<{
195
+ userSettings: "userSettings";
196
+ projectSettings: "projectSettings";
197
+ localSettings: "localSettings";
198
+ flagSettings: "flagSettings";
199
+ aegisApi: "aegisApi";
200
+ }>;
201
+ ruleBehavior: z.ZodEnum<{
202
+ allow: "allow";
203
+ deny: "deny";
204
+ ask: "ask";
205
+ }>;
206
+ toolName: z.ZodOptional<z.ZodString>;
207
+ commandPattern: z.ZodOptional<z.ZodString>;
208
+ }, z.core.$strip>>>;
173
209
  }, z.core.$strip>>;
174
210
  /** Schema for a single continuation pointer entry in session_map.json (Issue #900). */
175
211
  export declare const sessionMapEntrySchema: z.ZodObject<{
@@ -131,6 +131,13 @@ const UIStateEnum = z.enum([
131
131
  'permission_prompt', 'bash_approval', 'plan_mode', 'ask_question',
132
132
  'settings', 'error', 'unknown',
133
133
  ]);
134
+ /** Issue #700: Permission Policy Schema */
135
+ export const permissionRuleSchema = z.object({
136
+ source: z.enum(['userSettings', 'projectSettings', 'localSettings', 'flagSettings', 'aegisApi']),
137
+ ruleBehavior: z.enum(['allow', 'deny', 'ask']),
138
+ toolName: z.string().optional(),
139
+ commandPattern: z.string().optional(),
140
+ });
134
141
  /** Schema for persisted SessionState (sessions: { [id]: SessionInfo }). */
135
142
  export const persistedStateSchema = z.record(z.string(), z.object({
136
143
  id: z.string(),
@@ -158,6 +165,14 @@ export const persistedStateSchema = z.record(z.string(), z.object({
158
165
  model: z.string().optional(),
159
166
  lastDeadAt: z.number().optional(),
160
167
  ccPid: z.number().optional(),
168
+ parentId: z.string().uuid().optional(),
169
+ children: z.array(z.string().uuid()).optional(),
170
+ permissionPolicy: z.array(z.object({
171
+ source: z.enum(['userSettings', 'projectSettings', 'localSettings', 'flagSettings', 'aegisApi']),
172
+ ruleBehavior: z.enum(['allow', 'deny', 'ask']),
173
+ toolName: z.string().optional(),
174
+ commandPattern: z.string().optional(),
175
+ })).optional(),
161
176
  }));
162
177
  /** Schema for a single continuation pointer entry in session_map.json (Issue #900). */
163
178
  export const sessionMapEntrySchema = z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aegis-bridge",
3
- "version": "2.6.4",
3
+ "version": "2.7.0",
4
4
  "type": "module",
5
5
  "description": "Orchestrate Claude Code sessions via API. Create, brief, monitor, refine, ship.",
6
6
  "main": "dist/server.js",