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
package/dist/server.js
CHANGED
|
@@ -117,30 +117,36 @@ app.addHook('onSend', (req, reply, payload, done) => {
|
|
|
117
117
|
reply.header('Permissions-Policy', 'camera=(), microphone=()');
|
|
118
118
|
done();
|
|
119
119
|
});
|
|
120
|
-
// Auth middleware setup (Issue #39: multi-key auth with rate limiting)
|
|
121
|
-
// #228: Per-IP rate limiting (applies even with master token, with higher limits)
|
|
122
120
|
const ipRateLimits = new Map();
|
|
123
121
|
const IP_WINDOW_MS = 60_000;
|
|
124
122
|
const IP_LIMIT_NORMAL = 120; // per minute for regular keys
|
|
125
123
|
const IP_LIMIT_MASTER = 300; // per minute for master token
|
|
126
124
|
function checkIpRateLimit(ip, isMaster) {
|
|
127
125
|
const now = Date.now();
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
const cutoff = now - IP_WINDOW_MS;
|
|
127
|
+
const bucket = ipRateLimits.get(ip) || { entries: [], start: 0 };
|
|
128
|
+
// O(1) prune: advance start index past expired entries
|
|
129
|
+
while (bucket.start < bucket.entries.length && bucket.entries[bucket.start] < cutoff) {
|
|
130
|
+
bucket.start++;
|
|
131
|
+
}
|
|
132
|
+
// Compact when the leading garbage exceeds 50% of the allocated array
|
|
133
|
+
if (bucket.start > bucket.entries.length >>> 1) {
|
|
134
|
+
bucket.entries = bucket.entries.slice(bucket.start);
|
|
135
|
+
bucket.start = 0;
|
|
136
|
+
}
|
|
137
|
+
bucket.entries.push(now);
|
|
138
|
+
ipRateLimits.set(ip, bucket);
|
|
139
|
+
const activeCount = bucket.entries.length - bucket.start;
|
|
135
140
|
const limit = isMaster ? IP_LIMIT_MASTER : IP_LIMIT_NORMAL;
|
|
136
|
-
return
|
|
141
|
+
return activeCount > limit;
|
|
137
142
|
}
|
|
138
143
|
/** #357: Prune IPs whose timestamp arrays are entirely outside the rate-limit window. */
|
|
139
144
|
function pruneIpRateLimits() {
|
|
140
145
|
const cutoff = Date.now() - IP_WINDOW_MS;
|
|
141
|
-
for (const [ip,
|
|
146
|
+
for (const [ip, bucket] of ipRateLimits) {
|
|
142
147
|
// All timestamps are old — remove the entry entirely
|
|
143
|
-
|
|
148
|
+
const last = bucket.entries[bucket.entries.length - 1];
|
|
149
|
+
if (bucket.entries.length - bucket.start === 0 || (last !== undefined && last < cutoff)) {
|
|
144
150
|
ipRateLimits.delete(ip);
|
|
145
151
|
}
|
|
146
152
|
}
|
|
@@ -236,27 +242,11 @@ const createSessionSchema = z.object({
|
|
|
236
242
|
claudeCommand: z.string().max(10_000).optional(),
|
|
237
243
|
env: z.record(z.string(), z.string()).optional(),
|
|
238
244
|
stallThresholdMs: z.number().int().positive().max(3_600_000).optional(),
|
|
239
|
-
permissionMode: z.enum(['default', 'bypassPermissions', 'plan']).optional(),
|
|
245
|
+
permissionMode: z.enum(['default', 'bypassPermissions', 'plan', 'acceptEdits', 'dontAsk', 'auto']).optional(),
|
|
240
246
|
autoApprove: z.boolean().optional(),
|
|
241
247
|
}).strict();
|
|
242
248
|
// Health — Issue #397: includes tmux server health check
|
|
243
|
-
|
|
244
|
-
const pkg = await import('../package.json', { with: { type: 'json' } });
|
|
245
|
-
const activeCount = sessions.listSessions().length;
|
|
246
|
-
const totalCount = metrics.getTotalSessionsCreated();
|
|
247
|
-
const tmuxHealth = await tmux.isServerHealthy();
|
|
248
|
-
const status = tmuxHealth.healthy ? 'ok' : 'degraded';
|
|
249
|
-
return {
|
|
250
|
-
status,
|
|
251
|
-
version: pkg.default.version,
|
|
252
|
-
uptime: process.uptime(),
|
|
253
|
-
sessions: { active: activeCount, total: totalCount },
|
|
254
|
-
tmux: tmuxHealth,
|
|
255
|
-
timestamp: new Date().toISOString(),
|
|
256
|
-
};
|
|
257
|
-
});
|
|
258
|
-
// Backwards compat: unversioned health
|
|
259
|
-
app.get('/health', async () => {
|
|
249
|
+
async function healthHandler() {
|
|
260
250
|
const pkg = await import('../package.json', { with: { type: 'json' } });
|
|
261
251
|
const activeCount = sessions.listSessions().length;
|
|
262
252
|
const totalCount = metrics.getTotalSessionsCreated();
|
|
@@ -270,7 +260,9 @@ app.get('/health', async () => {
|
|
|
270
260
|
tmux: tmuxHealth,
|
|
271
261
|
timestamp: new Date().toISOString(),
|
|
272
262
|
};
|
|
273
|
-
}
|
|
263
|
+
}
|
|
264
|
+
app.get('/v1/health', healthHandler);
|
|
265
|
+
app.get('/health', healthHandler);
|
|
274
266
|
// Issue #81: Swarm awareness — list all detected CC swarms and their teammates
|
|
275
267
|
app.get('/v1/swarm', async () => {
|
|
276
268
|
const result = await swarmMonitor.scan();
|
|
@@ -335,7 +327,7 @@ app.get('/v1/sessions/:id/metrics', async (req, reply) => {
|
|
|
335
327
|
// Issue #89 L14: Webhook dead letter queue
|
|
336
328
|
app.get('/v1/webhooks/dead-letter', async () => {
|
|
337
329
|
for (const ch of channels.getChannels()) {
|
|
338
|
-
if (ch.name === 'webhook' &&
|
|
330
|
+
if (ch.name === 'webhook' && typeof ch.getDeadLetterQueue === 'function') {
|
|
339
331
|
return ch.getDeadLetterQueue();
|
|
340
332
|
}
|
|
341
333
|
}
|
|
@@ -457,7 +449,7 @@ app.get('/sessions', async () => sessions.listSessions());
|
|
|
457
449
|
/** Validate workDir — delegates to validation.ts (Issue #435). */
|
|
458
450
|
const validateWorkDirWithConfig = (workDir) => validateWorkDir(workDir, config.allowedWorkDirs);
|
|
459
451
|
// Create session (Issue #607: reuse idle session for same workDir)
|
|
460
|
-
|
|
452
|
+
async function createSessionHandler(req, reply) {
|
|
461
453
|
const parsed = createSessionSchema.safeParse(req.body);
|
|
462
454
|
if (!parsed.success) {
|
|
463
455
|
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
@@ -508,58 +500,18 @@ app.post('/v1/sessions', async (req, reply) => {
|
|
|
508
500
|
console.timeEnd("POST_SEND_INITIAL_PROMPT");
|
|
509
501
|
}
|
|
510
502
|
return reply.status(201).send({ ...session, promptDelivery });
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
app.post('/sessions',
|
|
514
|
-
const parsed = createSessionSchema.safeParse(req.body);
|
|
515
|
-
if (!parsed.success) {
|
|
516
|
-
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
517
|
-
}
|
|
518
|
-
const { workDir, name, prompt, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove } = parsed.data;
|
|
519
|
-
if (!workDir)
|
|
520
|
-
return reply.status(400).send({ error: 'workDir is required' });
|
|
521
|
-
const safeWorkDir = await validateWorkDirWithConfig(workDir);
|
|
522
|
-
if (typeof safeWorkDir === 'object')
|
|
523
|
-
return reply.status(400).send({ error: safeWorkDir.error, code: safeWorkDir.code });
|
|
524
|
-
// Issue #607: Check for an existing idle session with the same workDir
|
|
525
|
-
const existing = sessions.findIdleSessionByWorkDir(safeWorkDir);
|
|
526
|
-
if (existing) {
|
|
527
|
-
let promptDelivery;
|
|
528
|
-
if (prompt) {
|
|
529
|
-
promptDelivery = await sessions.sendInitialPrompt(existing.id, prompt);
|
|
530
|
-
metrics.promptSent(promptDelivery.delivered);
|
|
531
|
-
}
|
|
532
|
-
return reply.status(200).send({ ...existing, reused: true, promptDelivery });
|
|
533
|
-
}
|
|
534
|
-
const session = await sessions.createSession({ workDir: safeWorkDir, name, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove });
|
|
535
|
-
// Issue #46: Topic first, then prompt (same fix as v1 route)
|
|
536
|
-
await channels.sessionCreated({
|
|
537
|
-
event: 'session.created',
|
|
538
|
-
timestamp: new Date().toISOString(),
|
|
539
|
-
session: { id: session.id, name: session.windowName, workDir },
|
|
540
|
-
detail: `Session created: ${session.windowName}`,
|
|
541
|
-
meta: prompt ? { prompt: prompt.slice(0, 200), permissionMode: permissionMode ?? (autoApprove ? 'bypassPermissions' : undefined) } : undefined,
|
|
542
|
-
});
|
|
543
|
-
let promptDelivery;
|
|
544
|
-
if (prompt) {
|
|
545
|
-
promptDelivery = await sessions.sendInitialPrompt(session.id, prompt);
|
|
546
|
-
metrics.promptSent(promptDelivery.delivered);
|
|
547
|
-
}
|
|
548
|
-
return reply.status(201).send({ ...session, promptDelivery });
|
|
549
|
-
});
|
|
503
|
+
}
|
|
504
|
+
app.post('/v1/sessions', createSessionHandler);
|
|
505
|
+
app.post('/sessions', createSessionHandler);
|
|
550
506
|
// Get session (Issue #20: includes actionHints for interactive states)
|
|
551
|
-
|
|
507
|
+
async function getSessionHandler(req, reply) {
|
|
552
508
|
const session = sessions.getSession(req.params.id);
|
|
553
509
|
if (!session)
|
|
554
510
|
return reply.status(404).send({ error: 'Session not found' });
|
|
555
511
|
return addActionHints(session, sessions);
|
|
556
|
-
}
|
|
557
|
-
app.get('/sessions/:id',
|
|
558
|
-
|
|
559
|
-
if (!session)
|
|
560
|
-
return reply.status(404).send({ error: 'Session not found' });
|
|
561
|
-
return addActionHints(session, sessions);
|
|
562
|
-
});
|
|
512
|
+
}
|
|
513
|
+
app.get('/v1/sessions/:id', getSessionHandler);
|
|
514
|
+
app.get('/sessions/:id', getSessionHandler);
|
|
563
515
|
// #128: Bulk health check — returns health for all sessions in one request
|
|
564
516
|
app.get('/v1/sessions/health', async () => {
|
|
565
517
|
const allSessions = sessions.listSessions();
|
|
@@ -580,43 +532,18 @@ app.get('/v1/sessions/health', async () => {
|
|
|
580
532
|
return results;
|
|
581
533
|
});
|
|
582
534
|
// Session health check (Issue #2)
|
|
583
|
-
|
|
584
|
-
try {
|
|
585
|
-
return await sessions.getHealth(req.params.id);
|
|
586
|
-
}
|
|
587
|
-
catch (e) {
|
|
588
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
589
|
-
}
|
|
590
|
-
});
|
|
591
|
-
app.get('/sessions/:id/health', async (req, reply) => {
|
|
535
|
+
async function sessionHealthHandler(req, reply) {
|
|
592
536
|
try {
|
|
593
537
|
return await sessions.getHealth(req.params.id);
|
|
594
538
|
}
|
|
595
539
|
catch (e) {
|
|
596
540
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
597
541
|
}
|
|
598
|
-
}
|
|
542
|
+
}
|
|
543
|
+
app.get('/v1/sessions/:id/health', sessionHealthHandler);
|
|
544
|
+
app.get('/sessions/:id/health', sessionHealthHandler);
|
|
599
545
|
// Send message (with delivery verification — Issue #1)
|
|
600
|
-
|
|
601
|
-
const parsed = sendMessageSchema.safeParse(req.body);
|
|
602
|
-
if (!parsed.success)
|
|
603
|
-
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
604
|
-
const { text } = parsed.data;
|
|
605
|
-
try {
|
|
606
|
-
const result = await sessions.sendMessage(req.params.id, text);
|
|
607
|
-
await channels.message({
|
|
608
|
-
event: 'message.user',
|
|
609
|
-
timestamp: new Date().toISOString(),
|
|
610
|
-
session: { id: req.params.id, name: '', workDir: '' },
|
|
611
|
-
detail: text,
|
|
612
|
-
});
|
|
613
|
-
return { ok: true, delivered: result.delivered, attempts: result.attempts };
|
|
614
|
-
}
|
|
615
|
-
catch (e) {
|
|
616
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
617
|
-
}
|
|
618
|
-
});
|
|
619
|
-
app.post('/sessions/:id/send', async (req, reply) => {
|
|
546
|
+
async function sendMessageHandler(req, reply) {
|
|
620
547
|
const parsed = sendMessageSchema.safeParse(req.body);
|
|
621
548
|
if (!parsed.success)
|
|
622
549
|
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
@@ -634,26 +561,22 @@ app.post('/sessions/:id/send', async (req, reply) => {
|
|
|
634
561
|
catch (e) {
|
|
635
562
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
636
563
|
}
|
|
637
|
-
}
|
|
564
|
+
}
|
|
565
|
+
app.post('/v1/sessions/:id/send', sendMessageHandler);
|
|
566
|
+
app.post('/sessions/:id/send', sendMessageHandler);
|
|
638
567
|
// Read messages
|
|
639
|
-
|
|
568
|
+
async function readMessagesHandler(req, reply) {
|
|
640
569
|
try {
|
|
641
570
|
return await sessions.readMessages(req.params.id);
|
|
642
571
|
}
|
|
643
572
|
catch (e) {
|
|
644
573
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
645
574
|
}
|
|
646
|
-
}
|
|
647
|
-
app.get('/sessions/:id/read',
|
|
648
|
-
|
|
649
|
-
return await sessions.readMessages(req.params.id);
|
|
650
|
-
}
|
|
651
|
-
catch (e) {
|
|
652
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
653
|
-
}
|
|
654
|
-
});
|
|
575
|
+
}
|
|
576
|
+
app.get('/v1/sessions/:id/read', readMessagesHandler);
|
|
577
|
+
app.get('/sessions/:id/read', readMessagesHandler);
|
|
655
578
|
// Approve
|
|
656
|
-
|
|
579
|
+
async function approveHandler(req, reply) {
|
|
657
580
|
try {
|
|
658
581
|
await sessions.approve(req.params.id);
|
|
659
582
|
// Issue #87: Record permission response latency
|
|
@@ -666,22 +589,11 @@ app.post('/v1/sessions/:id/approve', async (req, reply) => {
|
|
|
666
589
|
catch (e) {
|
|
667
590
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
668
591
|
}
|
|
669
|
-
}
|
|
670
|
-
app.post('/sessions/:id/approve',
|
|
671
|
-
|
|
672
|
-
await sessions.approve(req.params.id);
|
|
673
|
-
const lat = sessions.getLatencyMetrics(req.params.id);
|
|
674
|
-
if (lat !== null && lat.permission_response_ms !== null) {
|
|
675
|
-
metrics.recordPermissionResponse(req.params.id, lat.permission_response_ms);
|
|
676
|
-
}
|
|
677
|
-
return { ok: true };
|
|
678
|
-
}
|
|
679
|
-
catch (e) {
|
|
680
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
681
|
-
}
|
|
682
|
-
});
|
|
592
|
+
}
|
|
593
|
+
app.post('/v1/sessions/:id/approve', approveHandler);
|
|
594
|
+
app.post('/sessions/:id/approve', approveHandler);
|
|
683
595
|
// Reject
|
|
684
|
-
|
|
596
|
+
async function rejectHandler(req, reply) {
|
|
685
597
|
try {
|
|
686
598
|
await sessions.reject(req.params.id);
|
|
687
599
|
const lat = sessions.getLatencyMetrics(req.params.id);
|
|
@@ -693,20 +605,9 @@ app.post('/v1/sessions/:id/reject', async (req, reply) => {
|
|
|
693
605
|
catch (e) {
|
|
694
606
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
695
607
|
}
|
|
696
|
-
}
|
|
697
|
-
app.post('/sessions/:id/reject',
|
|
698
|
-
|
|
699
|
-
await sessions.reject(req.params.id);
|
|
700
|
-
const lat = sessions.getLatencyMetrics(req.params.id);
|
|
701
|
-
if (lat !== null && lat.permission_response_ms !== null) {
|
|
702
|
-
metrics.recordPermissionResponse(req.params.id, lat.permission_response_ms);
|
|
703
|
-
}
|
|
704
|
-
return { ok: true };
|
|
705
|
-
}
|
|
706
|
-
catch (e) {
|
|
707
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
708
|
-
}
|
|
709
|
-
});
|
|
608
|
+
}
|
|
609
|
+
app.post('/v1/sessions/:id/reject', rejectHandler);
|
|
610
|
+
app.post('/sessions/:id/reject', rejectHandler);
|
|
710
611
|
// Issue #336: Answer pending AskUserQuestion
|
|
711
612
|
app.post('/v1/sessions/:id/answer', async (req, reply) => {
|
|
712
613
|
const { questionId, answer } = req.body || {};
|
|
@@ -723,16 +624,7 @@ app.post('/v1/sessions/:id/answer', async (req, reply) => {
|
|
|
723
624
|
return { ok: true };
|
|
724
625
|
});
|
|
725
626
|
// Escape
|
|
726
|
-
|
|
727
|
-
try {
|
|
728
|
-
await sessions.escape(req.params.id);
|
|
729
|
-
return { ok: true };
|
|
730
|
-
}
|
|
731
|
-
catch (e) {
|
|
732
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
733
|
-
}
|
|
734
|
-
});
|
|
735
|
-
app.post('/sessions/:id/escape', async (req, reply) => {
|
|
627
|
+
async function escapeHandler(req, reply) {
|
|
736
628
|
try {
|
|
737
629
|
await sessions.escape(req.params.id);
|
|
738
630
|
return { ok: true };
|
|
@@ -740,9 +632,11 @@ app.post('/sessions/:id/escape', async (req, reply) => {
|
|
|
740
632
|
catch (e) {
|
|
741
633
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
742
634
|
}
|
|
743
|
-
}
|
|
635
|
+
}
|
|
636
|
+
app.post('/v1/sessions/:id/escape', escapeHandler);
|
|
637
|
+
app.post('/sessions/:id/escape', escapeHandler);
|
|
744
638
|
// Interrupt (Ctrl+C)
|
|
745
|
-
|
|
639
|
+
async function interruptHandler(req, reply) {
|
|
746
640
|
try {
|
|
747
641
|
await sessions.interrupt(req.params.id);
|
|
748
642
|
return { ok: true };
|
|
@@ -750,34 +644,11 @@ app.post('/v1/sessions/:id/interrupt', async (req, reply) => {
|
|
|
750
644
|
catch (e) {
|
|
751
645
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
752
646
|
}
|
|
753
|
-
}
|
|
754
|
-
app.post('/sessions/:id/interrupt',
|
|
755
|
-
|
|
756
|
-
await sessions.interrupt(req.params.id);
|
|
757
|
-
return { ok: true };
|
|
758
|
-
}
|
|
759
|
-
catch (e) {
|
|
760
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
761
|
-
}
|
|
762
|
-
});
|
|
647
|
+
}
|
|
648
|
+
app.post('/v1/sessions/:id/interrupt', interruptHandler);
|
|
649
|
+
app.post('/sessions/:id/interrupt', interruptHandler);
|
|
763
650
|
// Kill session
|
|
764
|
-
|
|
765
|
-
if (!sessions.getSession(req.params.id)) {
|
|
766
|
-
return reply.status(404).send({ error: 'Session not found' });
|
|
767
|
-
}
|
|
768
|
-
try {
|
|
769
|
-
eventBus.emitEnded(req.params.id, 'killed');
|
|
770
|
-
await channels.sessionEnded(makePayload('session.ended', req.params.id, 'killed'));
|
|
771
|
-
await sessions.killSession(req.params.id);
|
|
772
|
-
monitor.removeSession(req.params.id);
|
|
773
|
-
metrics.cleanupSession(req.params.id);
|
|
774
|
-
return { ok: true };
|
|
775
|
-
}
|
|
776
|
-
catch (e) {
|
|
777
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
778
|
-
}
|
|
779
|
-
});
|
|
780
|
-
app.delete('/sessions/:id', async (req, reply) => {
|
|
651
|
+
async function killSessionHandler(req, reply) {
|
|
781
652
|
if (!sessions.getSession(req.params.id)) {
|
|
782
653
|
return reply.status(404).send({ error: 'Session not found' });
|
|
783
654
|
}
|
|
@@ -792,24 +663,21 @@ app.delete('/sessions/:id', async (req, reply) => {
|
|
|
792
663
|
catch (e) {
|
|
793
664
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
794
665
|
}
|
|
795
|
-
}
|
|
666
|
+
}
|
|
667
|
+
app.delete('/v1/sessions/:id', killSessionHandler);
|
|
668
|
+
app.delete('/sessions/:id', killSessionHandler);
|
|
796
669
|
// Capture raw pane
|
|
797
|
-
|
|
798
|
-
const session = sessions.getSession(req.params.id);
|
|
799
|
-
if (!session)
|
|
800
|
-
return reply.status(404).send({ error: 'Session not found' });
|
|
801
|
-
const pane = await tmux.capturePane(session.windowId);
|
|
802
|
-
return { pane };
|
|
803
|
-
});
|
|
804
|
-
app.get('/sessions/:id/pane', async (req, reply) => {
|
|
670
|
+
async function capturePaneHandler(req, reply) {
|
|
805
671
|
const session = sessions.getSession(req.params.id);
|
|
806
672
|
if (!session)
|
|
807
673
|
return reply.status(404).send({ error: 'Session not found' });
|
|
808
674
|
const pane = await tmux.capturePane(session.windowId);
|
|
809
675
|
return { pane };
|
|
810
|
-
}
|
|
676
|
+
}
|
|
677
|
+
app.get('/v1/sessions/:id/pane', capturePaneHandler);
|
|
678
|
+
app.get('/sessions/:id/pane', capturePaneHandler);
|
|
811
679
|
// Slash command
|
|
812
|
-
|
|
680
|
+
async function commandHandler(req, reply) {
|
|
813
681
|
const parsed = commandSchema.safeParse(req.body);
|
|
814
682
|
if (!parsed.success)
|
|
815
683
|
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
@@ -822,37 +690,11 @@ app.post('/v1/sessions/:id/command', async (req, reply) => {
|
|
|
822
690
|
catch (e) {
|
|
823
691
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
824
692
|
}
|
|
825
|
-
}
|
|
826
|
-
app.post('/sessions/:id/command',
|
|
827
|
-
|
|
828
|
-
if (!parsed.success)
|
|
829
|
-
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
830
|
-
const { command } = parsed.data;
|
|
831
|
-
try {
|
|
832
|
-
const cmd = command.startsWith('/') ? command : `/${command}`;
|
|
833
|
-
await sessions.sendMessage(req.params.id, cmd);
|
|
834
|
-
return { ok: true };
|
|
835
|
-
}
|
|
836
|
-
catch (e) {
|
|
837
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
838
|
-
}
|
|
839
|
-
});
|
|
693
|
+
}
|
|
694
|
+
app.post('/v1/sessions/:id/command', commandHandler);
|
|
695
|
+
app.post('/sessions/:id/command', commandHandler);
|
|
840
696
|
// Bash mode
|
|
841
|
-
|
|
842
|
-
const parsed = bashSchema.safeParse(req.body);
|
|
843
|
-
if (!parsed.success)
|
|
844
|
-
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
845
|
-
const { command } = parsed.data;
|
|
846
|
-
try {
|
|
847
|
-
const cmd = command.startsWith('!') ? command : `!${command}`;
|
|
848
|
-
await sessions.sendMessage(req.params.id, cmd);
|
|
849
|
-
return { ok: true };
|
|
850
|
-
}
|
|
851
|
-
catch (e) {
|
|
852
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
853
|
-
}
|
|
854
|
-
});
|
|
855
|
-
app.post('/sessions/:id/bash', async (req, reply) => {
|
|
697
|
+
async function bashHandler(req, reply) {
|
|
856
698
|
const parsed = bashSchema.safeParse(req.body);
|
|
857
699
|
if (!parsed.success)
|
|
858
700
|
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
@@ -865,30 +707,30 @@ app.post('/sessions/:id/bash', async (req, reply) => {
|
|
|
865
707
|
catch (e) {
|
|
866
708
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
867
709
|
}
|
|
868
|
-
}
|
|
710
|
+
}
|
|
711
|
+
app.post('/v1/sessions/:id/bash', bashHandler);
|
|
712
|
+
app.post('/sessions/:id/bash', bashHandler);
|
|
869
713
|
// Session summary (Issue #35)
|
|
870
|
-
|
|
714
|
+
async function summaryHandler(req, reply) {
|
|
871
715
|
try {
|
|
872
716
|
return await sessions.getSummary(req.params.id);
|
|
873
717
|
}
|
|
874
718
|
catch (e) {
|
|
875
719
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
876
720
|
}
|
|
877
|
-
}
|
|
878
|
-
app.get('/sessions/:id/summary',
|
|
879
|
-
|
|
880
|
-
return await sessions.getSummary(req.params.id);
|
|
881
|
-
}
|
|
882
|
-
catch (e) {
|
|
883
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
884
|
-
}
|
|
885
|
-
});
|
|
721
|
+
}
|
|
722
|
+
app.get('/v1/sessions/:id/summary', summaryHandler);
|
|
723
|
+
app.get('/sessions/:id/summary', summaryHandler);
|
|
886
724
|
// Paginated transcript read
|
|
887
725
|
app.get('/v1/sessions/:id/transcript', async (req, reply) => {
|
|
888
726
|
try {
|
|
889
727
|
const page = Math.max(1, parseInt(req.query.page || '1', 10) || 1);
|
|
890
728
|
const limit = Math.min(200, Math.max(1, parseInt(req.query.limit || '50', 10) || 50));
|
|
729
|
+
const allowedRoles = new Set(['user', 'assistant', 'system']);
|
|
891
730
|
const roleFilter = req.query.role;
|
|
731
|
+
if (roleFilter && !allowedRoles.has(roleFilter)) {
|
|
732
|
+
return reply.status(400).send({ error: `Invalid role filter: ${roleFilter}. Allowed values: user, assistant, system` });
|
|
733
|
+
}
|
|
892
734
|
return await sessions.readTranscript(req.params.id, page, limit, roleFilter);
|
|
893
735
|
}
|
|
894
736
|
catch (e) {
|
|
@@ -896,7 +738,7 @@ app.get('/v1/sessions/:id/transcript', async (req, reply) => {
|
|
|
896
738
|
}
|
|
897
739
|
});
|
|
898
740
|
// Screenshot capture (Issue #22)
|
|
899
|
-
|
|
741
|
+
async function screenshotHandler(req, reply) {
|
|
900
742
|
const parsed = screenshotSchema.safeParse(req.body);
|
|
901
743
|
if (!parsed.success)
|
|
902
744
|
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
@@ -925,36 +767,9 @@ app.post('/v1/sessions/:id/screenshot', async (req, reply) => {
|
|
|
925
767
|
catch (e) {
|
|
926
768
|
return reply.status(500).send({ error: `Screenshot failed: ${e instanceof Error ? e.message : String(e)}` });
|
|
927
769
|
}
|
|
928
|
-
}
|
|
929
|
-
app.post('/sessions/:id/screenshot',
|
|
930
|
-
|
|
931
|
-
if (!parsed.success)
|
|
932
|
-
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
933
|
-
const { url, fullPage, width, height } = parsed.data;
|
|
934
|
-
const urlError = validateScreenshotUrl(url);
|
|
935
|
-
if (urlError)
|
|
936
|
-
return reply.status(400).send({ error: urlError });
|
|
937
|
-
// Post-DNS-resolution check: resolve hostname and reject private IPs
|
|
938
|
-
const dnsError = await resolveAndCheckIp(new URL(url).hostname);
|
|
939
|
-
if (dnsError)
|
|
940
|
-
return reply.status(400).send({ error: dnsError });
|
|
941
|
-
const session = sessions.getSession(req.params.id);
|
|
942
|
-
if (!session)
|
|
943
|
-
return reply.status(404).send({ error: 'Session not found' });
|
|
944
|
-
if (!isPlaywrightAvailable()) {
|
|
945
|
-
return reply.status(501).send({
|
|
946
|
-
error: 'Playwright is not installed',
|
|
947
|
-
message: 'Install Playwright to enable screenshots: npx playwright install chromium && npm install -D playwright',
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
try {
|
|
951
|
-
const result = await captureScreenshot({ url, fullPage, width, height });
|
|
952
|
-
return reply.status(200).send(result);
|
|
953
|
-
}
|
|
954
|
-
catch (e) {
|
|
955
|
-
return reply.status(500).send({ error: `Screenshot failed: ${e instanceof Error ? e.message : String(e)}` });
|
|
956
|
-
}
|
|
957
|
-
});
|
|
770
|
+
}
|
|
771
|
+
app.post('/v1/sessions/:id/screenshot', screenshotHandler);
|
|
772
|
+
app.post('/sessions/:id/screenshot', screenshotHandler);
|
|
958
773
|
// SSE event stream (Issue #32)
|
|
959
774
|
app.get('/v1/sessions/:id/events', async (req, reply) => {
|
|
960
775
|
const session = sessions.getSession(req.params.id);
|
|
@@ -1671,4 +1486,3 @@ main().catch(err => {
|
|
|
1671
1486
|
console.error('Failed to start Aegis:', err);
|
|
1672
1487
|
process.exit(1);
|
|
1673
1488
|
});
|
|
1674
|
-
//# sourceMappingURL=server.js.map
|
package/dist/session.js
CHANGED
|
@@ -14,6 +14,18 @@ import { computeStallThreshold } from './config.js';
|
|
|
14
14
|
import { neutralizeBypassPermissions, restoreSettings, cleanOrphanedBackup } from './permission-guard.js';
|
|
15
15
|
import { persistedStateSchema, sessionMapSchema } from './validation.js';
|
|
16
16
|
import { writeHookSettingsFile, cleanupHookSettingsFile } from './hook-settings.js';
|
|
17
|
+
/** Convert parsed JSON arrays to Sets for activeSubagents (#668). */
|
|
18
|
+
function hydrateSessions(raw) {
|
|
19
|
+
const sessions = {};
|
|
20
|
+
for (const [id, s] of Object.entries(raw)) {
|
|
21
|
+
const { activeSubagents, ...rest } = s;
|
|
22
|
+
sessions[id] = {
|
|
23
|
+
...rest,
|
|
24
|
+
activeSubagents: activeSubagents ? new Set(activeSubagents) : undefined,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return sessions;
|
|
28
|
+
}
|
|
17
29
|
/**
|
|
18
30
|
* Detect whether CC is showing numbered permission options (e.g. "1. Yes, 2. No")
|
|
19
31
|
* vs a simple y/N prompt. Returns the approval method to use.
|
|
@@ -99,7 +111,7 @@ export class SessionManager {
|
|
|
99
111
|
const raw = await readFile(this.stateFile, 'utf-8');
|
|
100
112
|
const parsed = persistedStateSchema.safeParse(JSON.parse(raw));
|
|
101
113
|
if (parsed.success && this.isValidState({ sessions: parsed.data })) {
|
|
102
|
-
this.state = { sessions: parsed.data };
|
|
114
|
+
this.state = { sessions: hydrateSessions(parsed.data) };
|
|
103
115
|
}
|
|
104
116
|
else {
|
|
105
117
|
console.warn('State file failed validation, attempting backup restore');
|
|
@@ -110,7 +122,7 @@ export class SessionManager {
|
|
|
110
122
|
const backupRaw = await readFile(backupFile, 'utf-8');
|
|
111
123
|
const backupParsed = persistedStateSchema.safeParse(JSON.parse(backupRaw));
|
|
112
124
|
if (backupParsed.success && this.isValidState({ sessions: backupParsed.data })) {
|
|
113
|
-
this.state = { sessions: backupParsed.data };
|
|
125
|
+
this.state = { sessions: hydrateSessions(backupParsed.data) };
|
|
114
126
|
console.log('Restored state from backup');
|
|
115
127
|
}
|
|
116
128
|
else {
|
|
@@ -130,12 +142,6 @@ export class SessionManager {
|
|
|
130
142
|
this.state = { sessions: {} };
|
|
131
143
|
}
|
|
132
144
|
}
|
|
133
|
-
// #357: Convert deserialized activeSubagents arrays to Sets
|
|
134
|
-
for (const session of Object.values(this.state.sessions)) {
|
|
135
|
-
if (Array.isArray(session.activeSubagents)) {
|
|
136
|
-
session.activeSubagents = new Set(session.activeSubagents);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
145
|
// Create backup of successfully loaded state
|
|
140
146
|
try {
|
|
141
147
|
await writeFile(`${this.stateFile}.bak`, JSON.stringify(this.state, null, 2));
|
|
@@ -1396,4 +1402,3 @@ export class SessionManager {
|
|
|
1396
1402
|
catch { /* ignore parse errors */ }
|
|
1397
1403
|
}
|
|
1398
1404
|
}
|
|
1399
|
-
//# sourceMappingURL=session.js.map
|
package/dist/sse-limiter.js
CHANGED
package/dist/sse-writer.js
CHANGED
package/dist/ssrf.d.ts
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* - Unspecified: ::
|
|
10
10
|
* - IPv6 unique-local: fc00::/7
|
|
11
11
|
* - CGNAT: 100.64.0.0/10 (RFC 6598)
|
|
12
|
+
* - Broadcast: 255.255.255.255
|
|
13
|
+
* - Multicast: 224.0.0.0/4 (RFC 5771)
|
|
14
|
+
* - Documentation: 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24 (RFC 5737)
|
|
15
|
+
* - Benchmarking: 198.18.0.0/15 (RFC 2544)
|
|
12
16
|
*/
|
|
13
17
|
export declare function isPrivateIP(ip: string): boolean;
|
|
14
18
|
/**
|