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/ssrf.js CHANGED
@@ -17,12 +17,16 @@ import net from 'node:net';
17
17
  * - Unspecified: ::
18
18
  * - IPv6 unique-local: fc00::/7
19
19
  * - CGNAT: 100.64.0.0/10 (RFC 6598)
20
+ * - Broadcast: 255.255.255.255
21
+ * - Multicast: 224.0.0.0/4 (RFC 5771)
22
+ * - Documentation: 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24 (RFC 5737)
23
+ * - Benchmarking: 198.18.0.0/15 (RFC 2544)
20
24
  */
21
25
  export function isPrivateIP(ip) {
22
26
  // IPv4
23
27
  if (net.isIPv4(ip)) {
24
28
  const parts = ip.split('.').map(Number);
25
- const [a, b] = parts;
29
+ const [a, b, c] = parts;
26
30
  // 0.0.0.0/8
27
31
  if (a === 0)
28
32
  return true;
@@ -44,6 +48,24 @@ export function isPrivateIP(ip) {
44
48
  // 100.64.0.0/10 (CGNAT)
45
49
  if (a === 100 && b >= 64 && b <= 127)
46
50
  return true;
51
+ // 255.255.255.255 (broadcast)
52
+ if (a === 255 && b === 255 && c === 255 && parts[3] === 255)
53
+ return true;
54
+ // 224.0.0.0/4 (multicast) — 224.0.0.0 to 239.255.255.255
55
+ if (a >= 224 && a <= 239)
56
+ return true;
57
+ // 192.0.2.0/24 (documentation, RFC 5737)
58
+ if (a === 192 && b === 0 && c === 2)
59
+ return true;
60
+ // 198.51.100.0/24 (documentation, RFC 5737)
61
+ if (a === 198 && b === 51 && c === 100)
62
+ return true;
63
+ // 203.0.113.0/24 (documentation, RFC 5737)
64
+ if (a === 203 && b === 0 && c === 113)
65
+ return true;
66
+ // 198.18.0.0/15 (benchmarking, RFC 2544) — 198.18.0.0 to 198.19.255.255
67
+ if (a === 198 && b >= 18 && b <= 19)
68
+ return true;
47
69
  return false;
48
70
  }
49
71
  // IPv6
@@ -166,4 +188,3 @@ export function validateScreenshotUrl(rawUrl) {
166
188
  }
167
189
  return null;
168
190
  }
169
- //# sourceMappingURL=ssrf.js.map
@@ -154,11 +154,10 @@ export class SwarmMonitor {
154
154
  return this.cachedSocketNames;
155
155
  }
156
156
  const entries = await readdir(tmpdir());
157
- const pattern = this.config.socketGlobPattern.replace('tmux-', '');
158
157
  // Match "tmux-<socketName>" directories (tmux socket dirs start with "tmux-")
159
158
  const socketNames = [];
160
159
  for (const entry of entries) {
161
- if (entry.startsWith('tmux-') && entry.includes(pattern)) {
160
+ if (entry.startsWith('tmux-')) {
162
161
  // Extract socket name: tmux-<socketName> → <socketName>
163
162
  const socketName = entry.slice(5); // remove "tmux-"
164
163
  // Verify it's a claude-swarm-* socket
@@ -264,4 +263,3 @@ export class SwarmMonitor {
264
263
  return this.lastResult.swarms.filter(s => s.parentSession !== null && s.teammates.length > 0);
265
264
  }
266
265
  }
267
- //# sourceMappingURL=swarm-monitor.js.map
@@ -142,11 +142,13 @@ export function detectUIState(paneText) {
142
142
  return 'waiting_for_input';
143
143
  return 'unknown';
144
144
  }
145
+ /** Number of lines from the bottom of the pane to scan for active spinners. */
146
+ const SPINNER_SEARCH_LINES = 30;
145
147
  /** Check if any line in the pane has an active spinner character followed by working text. */
146
148
  function hasSpinnerAnywhere(lines) {
147
149
  // Only check lines in the content area (not the very bottom few which are prompt/footer)
148
150
  const searchEnd = Math.max(0, lines.length - 3);
149
- for (let i = Math.max(0, lines.length - 20); i < searchEnd; i++) {
151
+ for (let i = Math.max(0, lines.length - SPINNER_SEARCH_LINES); i < searchEnd; i++) {
150
152
  const stripped = lines[i].trim();
151
153
  if (!stripped)
152
154
  continue;
@@ -340,4 +342,3 @@ function findLastNonEmpty(lines, from = 0) {
340
342
  }
341
343
  return last;
342
344
  }
343
- //# sourceMappingURL=terminal-parser.js.map
@@ -32,4 +32,3 @@ export class TmuxCaptureCache {
32
32
  this.cache.clear();
33
33
  }
34
34
  }
35
- //# sourceMappingURL=tmux-capture-cache.js.map
package/dist/tmux.js CHANGED
@@ -39,7 +39,7 @@ export class TmuxManager {
39
39
  this.socketName = socketName ?? `aegis-${process.pid}`;
40
40
  }
41
41
  /** Promise-chain queue that serializes all tmux CLI calls to prevent race conditions. */
42
- queue = Promise.resolve(undefined);
42
+ queue = Promise.resolve();
43
43
  /** #403: Counter of in-flight createWindow calls — direct methods must queue when > 0. */
44
44
  _creatingCount = 0;
45
45
  /** #357: Short-lived cache for window existence checks to reduce CLI calls. */
@@ -748,4 +748,3 @@ export class TmuxManager {
748
748
  function sleep(ms) {
749
749
  return new Promise(resolve => setTimeout(resolve, ms));
750
750
  }
751
- //# sourceMappingURL=tmux.js.map
@@ -4,7 +4,7 @@
4
4
  * Port of CCBot's transcript_parser.py.
5
5
  * Reads CC session JSONL files and extracts structured messages.
6
6
  */
7
- import { stat, readFile, open } from 'node:fs/promises';
7
+ import { readFile, open } from 'node:fs/promises';
8
8
  import { createReadStream, existsSync } from 'node:fs';
9
9
  import { join } from 'node:path';
10
10
  import { homedir } from 'node:os';
@@ -154,62 +154,65 @@ export function parseEntries(entries) {
154
154
  }
155
155
  /** Read JSONL file from byte offset, return new entries + new offset. */
156
156
  export async function readNewEntries(filePath, fromOffset) {
157
- const fileStat = await stat(filePath);
158
- // File truncated (e.g. after /clear)
159
- if (fromOffset > fileStat.size) {
160
- return { entries: [], newOffset: 0, raw: [] };
161
- }
162
- if (fromOffset >= fileStat.size) {
163
- return { entries: [], newOffset: fromOffset, raw: [] };
164
- }
165
- // Read from byte offset to end using createReadStream to avoid loading entire file
166
- // Issue #222: Only read from offset forward, not the whole file
167
- // Issue #259: If offset lands mid-entry, scan backwards to previous newline
168
- // Issue #409: Use async I/O instead of readFileSync to avoid blocking the event loop
169
- let effectiveOffset = fromOffset;
170
- if (effectiveOffset > 0) {
171
- const scanSize = 4096;
172
- const scanStart = Math.max(0, effectiveOffset - scanSize);
173
- const scanLen = effectiveOffset - scanStart;
174
- const scanBuf = Buffer.alloc(scanLen);
175
- const fd = await open(filePath, 'r');
176
- try {
177
- await fd.read(scanBuf, 0, scanLen, scanStart);
157
+ // Issue #623: Use a single fd for stat + read to eliminate TOCTOU race.
158
+ const fd = await open(filePath, 'r');
159
+ try {
160
+ const fileStat = await fd.stat();
161
+ // File truncated (e.g. after /clear)
162
+ if (fromOffset > fileStat.size) {
163
+ return { entries: [], newOffset: 0, raw: [] };
178
164
  }
179
- finally {
180
- await fd.close();
165
+ if (fromOffset >= fileStat.size) {
166
+ return { entries: [], newOffset: fromOffset, raw: [] };
181
167
  }
182
- let foundNewline = false;
183
- for (let i = scanBuf.length - 1; i >= 0; i--) {
184
- if (scanBuf[i] === 0x0a) { // '\n'
185
- effectiveOffset = scanStart + i + 1;
186
- foundNewline = true;
187
- break;
168
+ // Read from byte offset to end using createReadStream to avoid loading entire file
169
+ // Issue #222: Only read from offset forward, not the whole file
170
+ // Issue #259: If offset lands mid-entry, scan backwards to previous newline
171
+ // Issue #409: Use async I/O instead of readFileSync to avoid blocking the event loop
172
+ let effectiveOffset = fromOffset;
173
+ if (effectiveOffset > 0) {
174
+ const scanSize = 4096;
175
+ const scanStart = Math.max(0, effectiveOffset - scanSize);
176
+ const scanLen = effectiveOffset - scanStart;
177
+ const scanBuf = Buffer.alloc(scanLen);
178
+ await fd.read(scanBuf, 0, scanLen, scanStart);
179
+ let foundNewline = false;
180
+ for (let i = scanBuf.length - 1; i >= 0; i--) {
181
+ if (scanBuf[i] === 0x0a) { // '\n'
182
+ effectiveOffset = scanStart + i + 1;
183
+ foundNewline = true;
184
+ break;
185
+ }
186
+ }
187
+ // Issue #579: If no newline found and we didn't scan from byte 0,
188
+ // fall back to offset 0 to avoid starting mid-line.
189
+ if (!foundNewline && scanStart > 0) {
190
+ effectiveOffset = 0;
188
191
  }
189
192
  }
190
- // Issue #579: If no newline found and we didn't scan from byte 0,
191
- // fall back to offset 0 to avoid starting mid-line.
192
- if (!foundNewline && scanStart > 0) {
193
- effectiveOffset = 0;
193
+ const slicedContent = await new Promise((resolve, reject) => {
194
+ const chunks = [];
195
+ // Reuse the same fd autoClose: false because we close it in the outer finally
196
+ const stream = createReadStream(filePath, { fd: fd.fd, start: effectiveOffset, autoClose: false });
197
+ stream.on('data', (chunk) => { if (typeof chunk !== 'string')
198
+ chunks.push(chunk); });
199
+ stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
200
+ stream.on('error', reject);
201
+ });
202
+ const lines = slicedContent.split('\n');
203
+ const rawEntries = [];
204
+ for (const line of lines) {
205
+ const entry = parseLine(line);
206
+ if (entry) {
207
+ rawEntries.push(entry);
208
+ }
194
209
  }
210
+ const parsed = parseEntries(rawEntries);
211
+ return { entries: parsed, newOffset: fileStat.size, raw: rawEntries };
195
212
  }
196
- const slicedContent = await new Promise((resolve, reject) => {
197
- const chunks = [];
198
- const stream = createReadStream(filePath, { start: effectiveOffset });
199
- stream.on('data', (chunk) => chunks.push(chunk));
200
- stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
201
- stream.on('error', reject);
202
- });
203
- const lines = slicedContent.split('\n');
204
- const rawEntries = [];
205
- for (const line of lines) {
206
- const entry = parseLine(line);
207
- if (entry) {
208
- rawEntries.push(entry);
209
- }
213
+ finally {
214
+ await fd.close();
210
215
  }
211
- const parsed = parseEntries(rawEntries);
212
- return { entries: parsed, newOffset: fileStat.size, raw: rawEntries };
213
216
  }
214
217
  /** Find the JSONL file for a session ID. */
215
218
  export async function findSessionFile(sessionId, claudeProjectsDir = DEFAULT_CLAUDE_PROJECTS_DIR) {
@@ -248,4 +251,3 @@ export async function findSessionFile(sessionId, claudeProjectsDir = DEFAULT_CLA
248
251
  }
249
252
  return null;
250
253
  }
251
- //# sourceMappingURL=transcript.js.map
@@ -52,4 +52,3 @@ export function redactSecretsFromText(text, headers) {
52
52
  }
53
53
  return result;
54
54
  }
55
- //# sourceMappingURL=redact-headers.js.map
@@ -40,6 +40,25 @@ export declare const webhookEndpointSchema: z.ZodObject<{
40
40
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
41
41
  timeoutMs: z.ZodOptional<z.ZodNumber>;
42
42
  }, z.core.$strict>;
43
+ /** POST /v1/hooks/:eventName — CC hook event payload (Issue #665). */
44
+ export declare const hookBodySchema: z.ZodObject<{
45
+ session_id: z.ZodOptional<z.ZodString>;
46
+ agent_name: z.ZodOptional<z.ZodString>;
47
+ agent_type: z.ZodOptional<z.ZodString>;
48
+ tool_name: z.ZodOptional<z.ZodString>;
49
+ tool_input: z.ZodOptional<z.ZodObject<{
50
+ command: z.ZodOptional<z.ZodString>;
51
+ }, z.core.$loose>>;
52
+ tool_use_id: z.ZodOptional<z.ZodString>;
53
+ permission_prompt: z.ZodOptional<z.ZodString>;
54
+ permission_mode: z.ZodOptional<z.ZodString>;
55
+ hook_event_name: z.ZodOptional<z.ZodString>;
56
+ model: z.ZodOptional<z.ZodString>;
57
+ timestamp: z.ZodOptional<z.ZodString>;
58
+ stop_reason: z.ZodOptional<z.ZodString>;
59
+ cwd: z.ZodOptional<z.ZodString>;
60
+ command: z.ZodOptional<z.ZodString>;
61
+ }, z.core.$loose>;
43
62
  /** POST /v1/sessions/:id/hooks/permission */
44
63
  export declare const permissionHookSchema: z.ZodObject<{
45
64
  session_id: z.ZodOptional<z.ZodString>;
@@ -64,6 +83,9 @@ export declare const batchSessionSchema: z.ZodObject<{
64
83
  default: "default";
65
84
  bypassPermissions: "bypassPermissions";
66
85
  plan: "plan";
86
+ acceptEdits: "acceptEdits";
87
+ dontAsk: "dontAsk";
88
+ auto: "auto";
67
89
  }>>;
68
90
  autoApprove: z.ZodOptional<z.ZodBoolean>;
69
91
  stallThresholdMs: z.ZodOptional<z.ZodNumber>;
@@ -82,6 +104,9 @@ export declare const pipelineSchema: z.ZodObject<{
82
104
  default: "default";
83
105
  bypassPermissions: "bypassPermissions";
84
106
  plan: "plan";
107
+ acceptEdits: "acceptEdits";
108
+ dontAsk: "dontAsk";
109
+ auto: "auto";
85
110
  }>>;
86
111
  autoApprove: z.ZodOptional<z.ZodBoolean>;
87
112
  }, z.core.$strip>>;
@@ -104,9 +129,9 @@ export declare const persistedStateSchema: z.ZodRecord<z.ZodString, z.ZodObject<
104
129
  monitorOffset: z.ZodNumber;
105
130
  status: z.ZodEnum<{
106
131
  unknown: "unknown";
132
+ permission_prompt: "permission_prompt";
107
133
  idle: "idle";
108
134
  working: "working";
109
- permission_prompt: "permission_prompt";
110
135
  bash_approval: "bash_approval";
111
136
  plan_mode: "plan_mode";
112
137
  ask_question: "ask_question";
@@ -116,7 +141,14 @@ export declare const persistedStateSchema: z.ZodRecord<z.ZodString, z.ZodObject<
116
141
  lastActivity: z.ZodNumber;
117
142
  stallThresholdMs: z.ZodNumber;
118
143
  permissionStallMs: z.ZodDefault<z.ZodNumber>;
119
- permissionMode: z.ZodString;
144
+ permissionMode: z.ZodEnum<{
145
+ default: "default";
146
+ bypassPermissions: "bypassPermissions";
147
+ plan: "plan";
148
+ acceptEdits: "acceptEdits";
149
+ dontAsk: "dontAsk";
150
+ auto: "auto";
151
+ }>;
120
152
  settingsPatched: z.ZodOptional<z.ZodBoolean>;
121
153
  hookSettingsFile: z.ZodOptional<z.ZodString>;
122
154
  lastHookAt: z.ZodOptional<z.ZodNumber>;
@@ -43,6 +43,23 @@ export const webhookEndpointSchema = z.object({
43
43
  headers: z.record(z.string(), z.string()).optional(),
44
44
  timeoutMs: z.number().int().positive().optional(),
45
45
  }).strict();
46
+ /** POST /v1/hooks/:eventName — CC hook event payload (Issue #665). */
47
+ export const hookBodySchema = z.object({
48
+ session_id: z.string().optional(),
49
+ agent_name: z.string().optional(),
50
+ agent_type: z.string().optional(),
51
+ tool_name: z.string().optional(),
52
+ tool_input: z.object({ command: z.string().optional() }).passthrough().optional(),
53
+ tool_use_id: z.string().optional(),
54
+ permission_prompt: z.string().optional(),
55
+ permission_mode: z.string().optional(),
56
+ hook_event_name: z.string().optional(),
57
+ model: z.string().optional(),
58
+ timestamp: z.string().optional(),
59
+ stop_reason: z.string().optional(),
60
+ cwd: z.string().optional(),
61
+ command: z.string().optional(),
62
+ }).passthrough();
46
63
  /** POST /v1/sessions/:id/hooks/permission */
47
64
  export const permissionHookSchema = z.object({
48
65
  session_id: z.string().optional(),
@@ -61,7 +78,7 @@ const batchSessionSpecSchema = z.object({
61
78
  name: z.string().max(200).optional(),
62
79
  workDir: z.string().min(1),
63
80
  prompt: z.string().max(100_000).optional(),
64
- permissionMode: z.enum(['default', 'bypassPermissions', 'plan']).optional(),
81
+ permissionMode: z.enum(['default', 'bypassPermissions', 'plan', 'acceptEdits', 'dontAsk', 'auto']).optional(),
65
82
  autoApprove: z.boolean().optional(),
66
83
  stallThresholdMs: z.number().int().positive().max(3_600_000).optional(),
67
84
  });
@@ -74,7 +91,7 @@ const pipelineStageSchema = z.object({
74
91
  workDir: z.string().min(1).optional(),
75
92
  prompt: z.string().min(1).max(MAX_INPUT_LENGTH),
76
93
  dependsOn: z.array(z.string()).optional(),
77
- permissionMode: z.enum(['default', 'bypassPermissions', 'plan']).optional(),
94
+ permissionMode: z.enum(['default', 'bypassPermissions', 'plan', 'acceptEdits', 'dontAsk', 'auto']).optional(),
78
95
  autoApprove: z.boolean().optional(),
79
96
  });
80
97
  /** POST /v1/pipelines */
@@ -122,7 +139,7 @@ export const persistedStateSchema = z.record(z.string(), z.object({
122
139
  lastActivity: z.number(),
123
140
  stallThresholdMs: z.number(),
124
141
  permissionStallMs: z.number().default(300_000),
125
- permissionMode: z.string(),
142
+ permissionMode: z.enum(['default', 'bypassPermissions', 'plan', 'acceptEdits', 'dontAsk', 'auto']),
126
143
  settingsPatched: z.boolean().optional(),
127
144
  hookSettingsFile: z.string().optional(),
128
145
  lastHookAt: z.number().optional(),
@@ -267,4 +284,3 @@ export async function validateWorkDir(workDir, allowedWorkDirs = []) {
267
284
  }
268
285
  return realPath;
269
286
  }
270
- //# sourceMappingURL=validation.js.map
@@ -31,7 +31,8 @@ const sessionPolls = new Map();
31
31
  /** Reset all internal state (for testing). */
32
32
  export function _resetForTesting() {
33
33
  for (const poll of sessionPolls.values()) {
34
- clearInterval(poll.timer);
34
+ if (poll.timer)
35
+ clearInterval(poll.timer);
35
36
  }
36
37
  sessionPolls.clear();
37
38
  }
@@ -282,7 +283,8 @@ function evictSubscriber(sessionId, socket, sub) {
282
283
  poll.subscribers.delete(socket);
283
284
  // If no more subscribers, clean up the poll timer
284
285
  if (poll.subscribers.size === 0) {
285
- clearInterval(poll.timer);
286
+ if (poll.timer)
287
+ clearInterval(poll.timer);
286
288
  sessionPolls.delete(sessionId);
287
289
  }
288
290
  }
@@ -300,4 +302,3 @@ function send(ws, msg) {
300
302
  function sendError(ws, message) {
301
303
  send(ws, { type: 'error', message });
302
304
  }
303
- //# sourceMappingURL=ws-terminal.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aegis-bridge",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "type": "module",
5
5
  "description": "Orchestrate Claude Code sessions via API. Create, brief, monitor, refine, ship.",
6
6
  "main": "dist/server.js",
@@ -26,7 +26,7 @@
26
26
  "scripts": {
27
27
  "build": "tsc && npm run build:copy-dashboard",
28
28
  "build:copy-dashboard": "node scripts/copy-dashboard.mjs",
29
- "build:dashboard": "cd dashboard && npm install && npm run build",
29
+ "build:dashboard": "cd dashboard && npm ci && npm run build",
30
30
  "start": "node dist/cli.js",
31
31
  "dev": "tsc && node dist/cli.js",
32
32
  "prepublishOnly": "npm run build:dashboard && npm run build",
@@ -63,7 +63,7 @@
63
63
  "node": ">=20.0.0"
64
64
  },
65
65
  "devDependencies": {
66
- "@types/node": "^25.5.0",
66
+ "@types/node": "^20.0.0",
67
67
  "@types/ws": "^8.18.1",
68
68
  "typescript": "^6.0.2",
69
69
  "vitest": "^4.1.2"