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.
Files changed (46) hide show
  1. package/dashboard/dist/assets/{index-DPp-wise.css → index-B7DYf7vF.css} +1 -1
  2. package/dashboard/dist/assets/{index-I_vW1gcQ.js → index-DxAes2EQ.js} +47 -47
  3. package/dashboard/dist/index.html +2 -2
  4. package/dist/auth.js +1 -2
  5. package/dist/channels/index.js +0 -1
  6. package/dist/channels/manager.js +0 -1
  7. package/dist/channels/telegram-style.js +0 -1
  8. package/dist/channels/telegram.js +0 -1
  9. package/dist/channels/types.js +0 -1
  10. package/dist/channels/webhook.js +0 -1
  11. package/dist/cli.js +0 -1
  12. package/dist/config.js +11 -5
  13. package/dist/dashboard/assets/{index-DPp-wise.css → index-B7DYf7vF.css} +1 -1
  14. package/dist/dashboard/assets/{index-I_vW1gcQ.js → index-DxAes2EQ.js} +47 -47
  15. package/dist/dashboard/index.html +2 -2
  16. package/dist/error-categories.js +0 -1
  17. package/dist/events.d.ts +2 -0
  18. package/dist/events.js +21 -3
  19. package/dist/hook-settings.js +13 -7
  20. package/dist/hook.js +0 -1
  21. package/dist/hooks.js +21 -20
  22. package/dist/jsonl-watcher.js +0 -1
  23. package/dist/mcp-server.js +0 -1
  24. package/dist/metrics.d.ts +2 -0
  25. package/dist/metrics.js +30 -17
  26. package/dist/monitor.js +1 -2
  27. package/dist/permission-guard.js +0 -1
  28. package/dist/pipeline.js +0 -1
  29. package/dist/screenshot.js +0 -1
  30. package/dist/server.js +88 -274
  31. package/dist/session.js +14 -9
  32. package/dist/signal-cleanup-helper.js +0 -1
  33. package/dist/sse-limiter.js +0 -1
  34. package/dist/sse-writer.js +0 -1
  35. package/dist/ssrf.d.ts +4 -0
  36. package/dist/ssrf.js +23 -2
  37. package/dist/swarm-monitor.js +1 -3
  38. package/dist/terminal-parser.js +3 -2
  39. package/dist/tmux-capture-cache.js +0 -1
  40. package/dist/tmux.js +1 -2
  41. package/dist/transcript.js +53 -51
  42. package/dist/utils/redact-headers.js +0 -1
  43. package/dist/validation.d.ts +34 -2
  44. package/dist/validation.js +20 -4
  45. package/dist/ws-terminal.js +4 -3
  46. 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 timestamps = ipRateLimits.get(ip) || [];
129
- // Prune old entries
130
- while (timestamps.length > 0 && timestamps[0] < now - IP_WINDOW_MS) {
131
- timestamps.shift();
132
- }
133
- timestamps.push(now);
134
- ipRateLimits.set(ip, timestamps);
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 timestamps.length > limit;
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, timestamps] of ipRateLimits) {
146
+ for (const [ip, bucket] of ipRateLimits) {
142
147
  // All timestamps are old — remove the entry entirely
143
- if (timestamps.length === 0 || timestamps[timestamps.length - 1] < cutoff) {
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
- app.get('/v1/health', async () => {
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' && 'getDeadLetterQueue' in ch) {
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
- app.post('/v1/sessions', async (req, reply) => {
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
- // Backwards compat (Issue #607: same reuse logic as v1 route)
513
- app.post('/sessions', async (req, reply) => {
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
- app.get('/v1/sessions/:id', async (req, reply) => {
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', async (req, reply) => {
558
- const session = sessions.getSession(req.params.id);
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
- app.get('/v1/sessions/:id/health', async (req, reply) => {
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
- app.post('/v1/sessions/:id/send', async (req, reply) => {
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
- app.get('/v1/sessions/:id/read', async (req, reply) => {
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', async (req, reply) => {
648
- try {
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
- app.post('/v1/sessions/:id/approve', async (req, reply) => {
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', async (req, reply) => {
671
- try {
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
- app.post('/v1/sessions/:id/reject', async (req, reply) => {
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', async (req, reply) => {
698
- try {
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
- app.post('/v1/sessions/:id/escape', async (req, reply) => {
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
- app.post('/v1/sessions/:id/interrupt', async (req, reply) => {
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', async (req, reply) => {
755
- try {
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
- app.delete('/v1/sessions/:id', async (req, reply) => {
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
- app.get('/v1/sessions/:id/pane', async (req, reply) => {
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
- app.post('/v1/sessions/:id/command', async (req, reply) => {
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', async (req, reply) => {
827
- const parsed = commandSchema.safeParse(req.body);
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
- app.post('/v1/sessions/:id/bash', async (req, reply) => {
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
- app.get('/v1/sessions/:id/summary', async (req, reply) => {
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', async (req, reply) => {
879
- try {
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
- app.post('/v1/sessions/:id/screenshot', async (req, reply) => {
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', async (req, reply) => {
930
- const parsed = screenshotSchema.safeParse(req.body);
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
@@ -115,4 +115,3 @@ export function createSignalHandler(sessions, tmux) {
115
115
  });
116
116
  };
117
117
  }
118
- //# sourceMappingURL=signal-cleanup-helper.js.map
@@ -59,4 +59,3 @@ export class SSEConnectionLimiter {
59
59
  }
60
60
  }
61
61
  }
62
- //# sourceMappingURL=sse-limiter.js.map
@@ -92,4 +92,3 @@ export class SSEWriter {
92
92
  this.onCleanup();
93
93
  }
94
94
  }
95
- //# sourceMappingURL=sse-writer.js.map
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
  /**