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.
- package/dashboard/dist/assets/{index-G8fziBeQ.js → index-SoKhTCVa.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dist/hook-settings.d.ts +12 -3
- package/dist/hook-settings.js +59 -2
- package/dist/server.js +87 -3
- package/dist/session.d.ts +6 -0
- package/dist/session.js +22 -1
- package/dist/tool-registry.d.ts +40 -0
- package/dist/tool-registry.js +83 -0
- package/dist/validation.d.ts +36 -0
- package/dist/validation.js +15 -0
- package/package.json +1 -1
- package/dist/dashboard/assets/index-9Hkkvm_I.css +0 -32
- package/dist/dashboard/assets/index-G8fziBeQ.js +0 -262
- package/dist/dashboard/index.html +0 -14
|
@@ -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-
|
|
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">
|
package/dist/hook-settings.d.ts
CHANGED
|
@@ -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
|
|
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>;
|
package/dist/hook-settings.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/dist/validation.d.ts
CHANGED
|
@@ -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<{
|
package/dist/validation.js
CHANGED
|
@@ -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({
|