aegis-bridge 2.4.0 → 2.5.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-DPp-wise.css → index-B7DYf7vF.css} +1 -1
- package/dashboard/dist/assets/{index-I_vW1gcQ.js → index-DxAes2EQ.js} +47 -47
- package/dashboard/dist/index.html +2 -2
- package/dist/auth.js +1 -2
- package/dist/channels/index.js +0 -1
- package/dist/channels/manager.js +0 -1
- package/dist/channels/telegram-style.js +0 -1
- package/dist/channels/telegram.js +0 -1
- package/dist/channels/types.js +0 -1
- package/dist/channels/webhook.js +0 -1
- package/dist/cli.js +0 -1
- package/dist/config.js +11 -5
- package/dist/dashboard/assets/{index-DPp-wise.css → index-B7DYf7vF.css} +1 -1
- package/dist/dashboard/assets/{index-I_vW1gcQ.js → index-DxAes2EQ.js} +47 -47
- package/dist/dashboard/index.html +2 -2
- package/dist/error-categories.js +0 -1
- package/dist/events.d.ts +2 -0
- package/dist/events.js +21 -3
- package/dist/hook-settings.js +13 -7
- package/dist/hook.js +0 -1
- package/dist/hooks.js +21 -20
- package/dist/jsonl-watcher.js +0 -1
- package/dist/mcp-server.js +0 -1
- package/dist/metrics.d.ts +2 -0
- package/dist/metrics.js +30 -17
- package/dist/monitor.js +1 -2
- package/dist/permission-guard.js +0 -1
- package/dist/pipeline.js +0 -1
- package/dist/screenshot.js +0 -1
- package/dist/server.js +88 -274
- package/dist/session.js +14 -9
- package/dist/signal-cleanup-helper.js +0 -1
- package/dist/sse-limiter.js +0 -1
- package/dist/sse-writer.js +0 -1
- package/dist/ssrf.d.ts +4 -0
- package/dist/ssrf.js +23 -2
- package/dist/swarm-monitor.js +1 -3
- package/dist/terminal-parser.js +3 -2
- package/dist/tmux-capture-cache.js +0 -1
- package/dist/tmux.js +1 -2
- package/dist/transcript.js +53 -51
- package/dist/utils/redact-headers.js +0 -1
- package/dist/validation.d.ts +34 -2
- package/dist/validation.js +20 -4
- package/dist/ws-terminal.js +4 -3
- package/package.json +3 -3
|
@@ -5,8 +5,8 @@
|
|
|
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-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/dashboard/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/dashboard/assets/index-DxAes2EQ.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/dashboard/assets/index-B7DYf7vF.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body class="bg-[#0a0a0f] text-gray-200 antialiased">
|
|
12
12
|
<div id="root"></div>
|
package/dist/error-categories.js
CHANGED
package/dist/events.d.ts
CHANGED
|
@@ -74,6 +74,8 @@ export declare class SessionEventBus {
|
|
|
74
74
|
subscriberCount(sessionId: string): number;
|
|
75
75
|
/** Global emitter for aggregating events across all sessions. */
|
|
76
76
|
private globalEmitter;
|
|
77
|
+
/** #689: Pending setImmediate timers for cleanup on destroy. */
|
|
78
|
+
private pendingTimers;
|
|
77
79
|
/** Subscribe to events from ALL sessions (new and existing). Returns unsubscribe function. */
|
|
78
80
|
subscribeGlobal(handler: (event: GlobalSSEEvent) => void): () => void;
|
|
79
81
|
/** Emit a session created event to global subscribers. */
|
package/dist/events.js
CHANGED
|
@@ -98,7 +98,11 @@ export class SessionEventBus {
|
|
|
98
98
|
}
|
|
99
99
|
const emitter = this.emitters.get(sessionId);
|
|
100
100
|
if (emitter) {
|
|
101
|
-
setImmediate(() =>
|
|
101
|
+
const imm = setImmediate(() => {
|
|
102
|
+
this.pendingTimers.delete(imm);
|
|
103
|
+
emitter.emit('event', event);
|
|
104
|
+
});
|
|
105
|
+
this.pendingTimers.add(imm);
|
|
102
106
|
}
|
|
103
107
|
// Forward to global subscribers
|
|
104
108
|
if (this.globalEmitter) {
|
|
@@ -108,7 +112,11 @@ export class SessionEventBus {
|
|
|
108
112
|
if (this.globalEventBuffer.length > SessionEventBus.BUFFER_SIZE) {
|
|
109
113
|
this.globalEventBuffer.splice(0, this.globalEventBuffer.length - SessionEventBus.BUFFER_SIZE);
|
|
110
114
|
}
|
|
111
|
-
setImmediate(() =>
|
|
115
|
+
const imm = setImmediate(() => {
|
|
116
|
+
this.pendingTimers.delete(imm);
|
|
117
|
+
this.globalEmitter?.emit('event', globalEvent);
|
|
118
|
+
});
|
|
119
|
+
this.pendingTimers.add(imm);
|
|
112
120
|
}
|
|
113
121
|
}
|
|
114
122
|
/** Get events emitted after the given event ID for a session. */
|
|
@@ -217,6 +225,8 @@ export class SessionEventBus {
|
|
|
217
225
|
// ── Global (all-session) SSE ──────────────────────────────────────
|
|
218
226
|
/** Global emitter for aggregating events across all sessions. */
|
|
219
227
|
globalEmitter = null;
|
|
228
|
+
/** #689: Pending setImmediate timers for cleanup on destroy. */
|
|
229
|
+
pendingTimers = new Set();
|
|
220
230
|
/** Subscribe to events from ALL sessions (new and existing). Returns unsubscribe function. */
|
|
221
231
|
subscribeGlobal(handler) {
|
|
222
232
|
if (!this.globalEmitter) {
|
|
@@ -226,6 +236,10 @@ export class SessionEventBus {
|
|
|
226
236
|
this.globalEmitter.on('event', handler);
|
|
227
237
|
return () => {
|
|
228
238
|
this.globalEmitter?.off('event', handler);
|
|
239
|
+
// #689: Nullify globalEmitter when all subscribers leave
|
|
240
|
+
if (this.globalEmitter && this.globalEmitter.listenerCount('event') === 0) {
|
|
241
|
+
this.globalEmitter = null;
|
|
242
|
+
}
|
|
229
243
|
};
|
|
230
244
|
}
|
|
231
245
|
/** Emit a session created event to global subscribers. */
|
|
@@ -262,6 +276,11 @@ export class SessionEventBus {
|
|
|
262
276
|
}
|
|
263
277
|
/** Clean up all emitters. */
|
|
264
278
|
destroy() {
|
|
279
|
+
// #689: Clear pending setImmediate timers before removing listeners
|
|
280
|
+
for (const imm of this.pendingTimers) {
|
|
281
|
+
clearImmediate(imm);
|
|
282
|
+
}
|
|
283
|
+
this.pendingTimers.clear();
|
|
265
284
|
for (const emitter of this.emitters.values()) {
|
|
266
285
|
emitter.removeAllListeners();
|
|
267
286
|
}
|
|
@@ -272,4 +291,3 @@ export class SessionEventBus {
|
|
|
272
291
|
this.globalEmitter = null;
|
|
273
292
|
}
|
|
274
293
|
}
|
|
275
|
-
//# sourceMappingURL=events.js.map
|
package/dist/hook-settings.js
CHANGED
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Issue #169: Phase 2 — Inject CC settings.json with HTTP hooks.
|
|
15
15
|
*/
|
|
16
|
-
import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
|
|
16
|
+
import { readFile, writeFile, unlink, mkdir, rmdir } from 'node:fs/promises';
|
|
17
17
|
import { existsSync } from 'node:fs';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import { tmpdir } from 'node:os';
|
|
20
|
+
import { randomBytes } from 'node:crypto';
|
|
20
21
|
import { ccSettingsSchema } from './validation.js';
|
|
21
22
|
/** CC hook events that support `type: "http"`.
|
|
22
23
|
*
|
|
@@ -112,12 +113,13 @@ export async function writeHookSettingsFile(baseUrl, sessionId, workDir) {
|
|
|
112
113
|
...hookSettings.hooks,
|
|
113
114
|
},
|
|
114
115
|
};
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
116
|
+
// Issue #648: Use unpredictable directory name and restrictive permissions
|
|
117
|
+
// to prevent symlink attacks and information disclosure in /tmp.
|
|
118
|
+
const suffix = randomBytes(4).toString('hex');
|
|
119
|
+
const settingsDir = join(tmpdir(), `aegis-hooks-${suffix}`);
|
|
120
|
+
await mkdir(settingsDir, { recursive: true, mode: 0o700 });
|
|
119
121
|
const filePath = join(settingsDir, `hooks-${sessionId}.json`);
|
|
120
|
-
await writeFile(filePath, JSON.stringify(combined, null, 2) + '\n', 'utf-8');
|
|
122
|
+
await writeFile(filePath, JSON.stringify(combined, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
121
123
|
return filePath;
|
|
122
124
|
}
|
|
123
125
|
/**
|
|
@@ -129,10 +131,14 @@ export async function cleanupHookSettingsFile(filePath) {
|
|
|
129
131
|
try {
|
|
130
132
|
if (existsSync(filePath)) {
|
|
131
133
|
await unlink(filePath);
|
|
134
|
+
// Issue #648: Also remove the randomized parent directory
|
|
135
|
+
const parentDir = join(filePath, '..');
|
|
136
|
+
await rmdir(parentDir).catch(() => {
|
|
137
|
+
// Non-fatal: directory may not be empty or already removed
|
|
138
|
+
});
|
|
132
139
|
}
|
|
133
140
|
}
|
|
134
141
|
catch {
|
|
135
142
|
// Non-fatal: temp file cleanup failed
|
|
136
143
|
}
|
|
137
144
|
}
|
|
138
|
-
//# sourceMappingURL=hook-settings.js.map
|
package/dist/hook.js
CHANGED
package/dist/hooks.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Issue #169: Phase 1 — HTTP hooks infrastructure.
|
|
15
15
|
* Issue #169: Phase 3 — Hook-driven status detection.
|
|
16
16
|
*/
|
|
17
|
-
import { isValidUUID } from './validation.js';
|
|
17
|
+
import { isValidUUID, hookBodySchema } from './validation.js';
|
|
18
18
|
/** CC hook events that require a decision response. */
|
|
19
19
|
const DECISION_EVENTS = new Set(['PreToolUse', 'PermissionRequest']);
|
|
20
20
|
/** Permission modes that should be auto-approved via hook response. */
|
|
@@ -114,10 +114,15 @@ export function registerHookRoutes(app, deps) {
|
|
|
114
114
|
if (!session) {
|
|
115
115
|
return reply.status(404).send({ error: `Session ${sessionId} not found` });
|
|
116
116
|
}
|
|
117
|
+
// Issue #665: Validate hook body with Zod instead of unsafe casts
|
|
118
|
+
const parseResult = hookBodySchema.safeParse(req.body);
|
|
119
|
+
if (!parseResult.success) {
|
|
120
|
+
return reply.status(400).send({ error: `Invalid hook body: ${parseResult.error.message}` });
|
|
121
|
+
}
|
|
122
|
+
const hookBody = parseResult.data;
|
|
117
123
|
// Issue #88: Track active subagents
|
|
118
|
-
const hookBody = req.body;
|
|
119
124
|
if (eventName === 'SubagentStart') {
|
|
120
|
-
const agentName = hookBody
|
|
125
|
+
const agentName = hookBody.agent_name || hookBody.tool_input?.command || 'unknown';
|
|
121
126
|
deps.sessions.addSubagent(sessionId, agentName);
|
|
122
127
|
deps.eventBus.emit(sessionId, {
|
|
123
128
|
event: 'subagent_start',
|
|
@@ -127,7 +132,7 @@ export function registerHookRoutes(app, deps) {
|
|
|
127
132
|
});
|
|
128
133
|
}
|
|
129
134
|
else if (eventName === 'SubagentStop') {
|
|
130
|
-
const agentName = hookBody
|
|
135
|
+
const agentName = hookBody.agent_name || 'unknown';
|
|
131
136
|
deps.sessions.removeSubagent(sessionId, agentName);
|
|
132
137
|
deps.eventBus.emit(sessionId, {
|
|
133
138
|
event: 'subagent_stop',
|
|
@@ -149,16 +154,15 @@ export function registerHookRoutes(app, deps) {
|
|
|
149
154
|
if (eventName === 'PreCompact' || eventName === 'PostCompact') {
|
|
150
155
|
session.lastActivity = Date.now();
|
|
151
156
|
}
|
|
152
|
-
// Forward the
|
|
153
|
-
deps.eventBus.emitHook(sessionId, eventName,
|
|
157
|
+
// Forward the validated hook event to SSE subscribers
|
|
158
|
+
deps.eventBus.emitHook(sessionId, eventName, hookBody);
|
|
154
159
|
// Issue #89 L25: Capture model field from hook payload for dashboard display
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
deps.sessions.updateSessionModel(sessionId, hookPayload.model);
|
|
160
|
+
if (hookBody.model) {
|
|
161
|
+
deps.sessions.updateSessionModel(sessionId, hookBody.model);
|
|
158
162
|
}
|
|
159
163
|
// Issue #89 L24: Validate permission_mode from PermissionRequest hook
|
|
160
164
|
if (eventName === 'PermissionRequest') {
|
|
161
|
-
const rawMode = hookBody
|
|
165
|
+
const rawMode = hookBody.permission_mode;
|
|
162
166
|
if (rawMode !== undefined && !VALID_PERMISSION_MODES.has(rawMode)) {
|
|
163
167
|
console.warn(`Hooks: invalid permission_mode "${rawMode}" from PermissionRequest, using "default"`);
|
|
164
168
|
hookBody.permission_mode = 'default';
|
|
@@ -167,8 +171,8 @@ export function registerHookRoutes(app, deps) {
|
|
|
167
171
|
// Issue #169 Phase 3: Update session status from hook event
|
|
168
172
|
// Issue #87: Extract timestamp from hook payload for latency calculation
|
|
169
173
|
const hookReceivedAt = Date.now();
|
|
170
|
-
const hookEventTimestamp =
|
|
171
|
-
? new Date(
|
|
174
|
+
const hookEventTimestamp = hookBody.timestamp
|
|
175
|
+
? new Date(hookBody.timestamp).getTime()
|
|
172
176
|
: undefined;
|
|
173
177
|
// Issue #87: Record hook latency if we have a timestamp from the payload
|
|
174
178
|
if (hookEventTimestamp && deps.metrics) {
|
|
@@ -202,22 +206,20 @@ export function registerHookRoutes(app, deps) {
|
|
|
202
206
|
deps.eventBus.emitStatus(sessionId, 'working', 'Elicitation result received (hook: ElicitationResult)');
|
|
203
207
|
break;
|
|
204
208
|
case 'PermissionRequest':
|
|
205
|
-
deps.eventBus.emitApproval(sessionId,
|
|
206
|
-
|| 'Permission requested (hook)');
|
|
209
|
+
deps.eventBus.emitApproval(sessionId, hookBody.permission_prompt || 'Permission requested (hook)');
|
|
207
210
|
break;
|
|
208
211
|
}
|
|
209
212
|
}
|
|
210
213
|
// Decision events need a response body that CC uses
|
|
211
214
|
// Format: { hookSpecificOutput: { hookEventName, permissionDecision, reason? } }
|
|
212
215
|
if (DECISION_EVENTS.has(eventName)) {
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
const permissionPrompt = hookBody?.permission_prompt || '';
|
|
216
|
+
const toolName = hookBody.tool_name || '';
|
|
217
|
+
const permissionPrompt = hookBody.permission_prompt || '';
|
|
216
218
|
if (eventName === 'PreToolUse') {
|
|
217
219
|
// Issue #336: Intercept AskUserQuestion for headless question answering
|
|
218
220
|
if (toolName === 'AskUserQuestion') {
|
|
219
|
-
const toolInput = hookBody
|
|
220
|
-
const toolUseId = hookBody
|
|
221
|
+
const toolInput = hookBody.tool_input;
|
|
222
|
+
const toolUseId = hookBody.tool_use_id || '';
|
|
221
223
|
const questionText = extractQuestionText(toolInput);
|
|
222
224
|
// Emit ask_question SSE event for external clients
|
|
223
225
|
deps.eventBus.emit(sessionId, {
|
|
@@ -281,4 +283,3 @@ export function registerHookRoutes(app, deps) {
|
|
|
281
283
|
return reply.status(200).send({ ok: true });
|
|
282
284
|
});
|
|
283
285
|
}
|
|
284
|
-
//# sourceMappingURL=hooks.js.map
|
package/dist/jsonl-watcher.js
CHANGED
package/dist/mcp-server.js
CHANGED
package/dist/metrics.d.ts
CHANGED
|
@@ -93,6 +93,8 @@ export declare class MetricsCollector {
|
|
|
93
93
|
recordPermissionResponse(sessionId: string, latencyMs: number): void;
|
|
94
94
|
recordChannelDelivery(sessionId: string, latencyMs: number): void;
|
|
95
95
|
private summarizeSamples;
|
|
96
|
+
/** Stream-aggregate a single latency field across all sessions without creating temp arrays. */
|
|
97
|
+
private aggregateLatencyField;
|
|
96
98
|
getSessionLatency(sessionId: string): SessionLatencySummary | null;
|
|
97
99
|
/** Clean up latency data for a session (called on session kill). */
|
|
98
100
|
clearSessionLatency(sessionId: string): void;
|
package/dist/metrics.js
CHANGED
|
@@ -150,6 +150,27 @@ export class MetricsCollector {
|
|
|
150
150
|
}
|
|
151
151
|
return { min, max, avg: Math.round(sum / samples.length), count: samples.length };
|
|
152
152
|
}
|
|
153
|
+
/** Stream-aggregate a single latency field across all sessions without creating temp arrays. */
|
|
154
|
+
aggregateLatencyField(field) {
|
|
155
|
+
let min;
|
|
156
|
+
let max;
|
|
157
|
+
let sum = 0;
|
|
158
|
+
let count = 0;
|
|
159
|
+
for (const lat of this.latency.values()) {
|
|
160
|
+
const samples = lat[field];
|
|
161
|
+
for (const s of samples) {
|
|
162
|
+
if (min === undefined || s < min)
|
|
163
|
+
min = s;
|
|
164
|
+
if (max === undefined || s > max)
|
|
165
|
+
max = s;
|
|
166
|
+
sum += s;
|
|
167
|
+
count++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (count === 0)
|
|
171
|
+
return { min: null, max: null, avg: null, count: 0 };
|
|
172
|
+
return { min: min, max: max, avg: Math.round(sum / count), count };
|
|
173
|
+
}
|
|
153
174
|
getSessionLatency(sessionId) {
|
|
154
175
|
const lat = this.latency.get(sessionId);
|
|
155
176
|
if (!lat)
|
|
@@ -173,17 +194,11 @@ export class MetricsCollector {
|
|
|
173
194
|
getGlobalMetrics(activeSessionCount) {
|
|
174
195
|
const avgMessages = this.global.sessionsCreated > 0
|
|
175
196
|
? Math.round(this.global.totalMessages / this.global.sessionsCreated) : 0;
|
|
176
|
-
// Issue #87:
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
for (const lat of this.latency.values()) {
|
|
182
|
-
allHookLatency.push(...lat.hook_latency_ms);
|
|
183
|
-
allStateChange.push(...lat.state_change_detection_ms);
|
|
184
|
-
allPermissionResponse.push(...lat.permission_response_ms);
|
|
185
|
-
allChannelDelivery.push(...lat.channel_delivery_ms);
|
|
186
|
-
}
|
|
197
|
+
// Issue #87: Stream-aggregate latency across all sessions (no temp arrays)
|
|
198
|
+
const aggHook = this.aggregateLatencyField('hook_latency_ms');
|
|
199
|
+
const aggStateChange = this.aggregateLatencyField('state_change_detection_ms');
|
|
200
|
+
const aggPermission = this.aggregateLatencyField('permission_response_ms');
|
|
201
|
+
const aggChannel = this.aggregateLatencyField('channel_delivery_ms');
|
|
187
202
|
return {
|
|
188
203
|
uptime: Math.round((Date.now() - this.startTime) / 1000),
|
|
189
204
|
sessions: {
|
|
@@ -207,12 +222,11 @@ export class MetricsCollector {
|
|
|
207
222
|
success_rate: this.global.promptsSent > 0
|
|
208
223
|
? Math.round((this.global.promptsDelivered / this.global.promptsSent) * 100) : null,
|
|
209
224
|
},
|
|
210
|
-
// Issue #87: Aggregate latency metrics
|
|
211
225
|
latency: {
|
|
212
|
-
hook_latency_ms:
|
|
213
|
-
state_change_detection_ms:
|
|
214
|
-
permission_response_ms:
|
|
215
|
-
channel_delivery_ms:
|
|
226
|
+
hook_latency_ms: aggHook,
|
|
227
|
+
state_change_detection_ms: aggStateChange,
|
|
228
|
+
permission_response_ms: aggPermission,
|
|
229
|
+
channel_delivery_ms: aggChannel,
|
|
216
230
|
},
|
|
217
231
|
};
|
|
218
232
|
}
|
|
@@ -223,4 +237,3 @@ export class MetricsCollector {
|
|
|
223
237
|
return this.global.sessionsCreated;
|
|
224
238
|
}
|
|
225
239
|
}
|
|
226
|
-
//# sourceMappingURL=metrics.js.map
|
package/dist/monitor.js
CHANGED
|
@@ -234,7 +234,7 @@ export class SessionMonitor {
|
|
|
234
234
|
await this.channels.statusChange(this.makePayload('status.permission_timeout', session, detail));
|
|
235
235
|
}
|
|
236
236
|
catch (e) {
|
|
237
|
-
console.error(`Monitor: auto-reject failed for session ${session.id}: ${e.message}`);
|
|
237
|
+
console.error(`Monitor: auto-reject failed for session ${session.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
}
|
|
@@ -619,4 +619,3 @@ export class SessionMonitor {
|
|
|
619
619
|
function sleep(ms) {
|
|
620
620
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
621
621
|
}
|
|
622
|
-
//# sourceMappingURL=monitor.js.map
|
package/dist/permission-guard.js
CHANGED
package/dist/pipeline.js
CHANGED
package/dist/screenshot.js
CHANGED