aegis-bridge 2.5.2 → 2.5.3
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/dist/channels/manager.d.ts +8 -0
- package/dist/channels/manager.js +15 -0
- package/dist/channels/webhook.js +8 -2
- package/dist/events.d.ts +2 -0
- package/dist/events.js +16 -1
- package/dist/hook-settings.js +8 -8
- package/dist/hooks.js +2 -2
- package/dist/session.d.ts +2 -0
- package/dist/session.js +18 -2
- package/package.json +1 -1
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
* one broken channel never kills the bridge.
|
|
7
7
|
*/
|
|
8
8
|
import type { Channel, SessionEventPayload, InboundHandler } from './types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Thrown for retriable failures (5xx server errors, network timeouts).
|
|
11
|
+
* Only these increment the circuit breaker failure count.
|
|
12
|
+
* 4xx client errors are thrown as plain Error and do NOT trip the breaker.
|
|
13
|
+
*/
|
|
14
|
+
export declare class RetriableError extends Error {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
9
17
|
export declare class ChannelManager {
|
|
10
18
|
private channels;
|
|
11
19
|
private inboundHandler;
|
package/dist/channels/manager.js
CHANGED
|
@@ -5,6 +5,17 @@
|
|
|
5
5
|
* to every registered channel, swallowing per-channel errors so
|
|
6
6
|
* one broken channel never kills the bridge.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Thrown for retriable failures (5xx server errors, network timeouts).
|
|
10
|
+
* Only these increment the circuit breaker failure count.
|
|
11
|
+
* 4xx client errors are thrown as plain Error and do NOT trip the breaker.
|
|
12
|
+
*/
|
|
13
|
+
export class RetriableError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'RetriableError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
8
19
|
export class ChannelManager {
|
|
9
20
|
channels = [];
|
|
10
21
|
inboundHandler = null;
|
|
@@ -86,6 +97,10 @@ export class ChannelManager {
|
|
|
86
97
|
}
|
|
87
98
|
catch (e) {
|
|
88
99
|
console.error(`Channel ${ch.name} error on ${payload.event}:`, e);
|
|
100
|
+
// Only count retriable errors (5xx, network) toward circuit breaker.
|
|
101
|
+
// 4xx client errors are non-retriable — the server is healthy.
|
|
102
|
+
if (!(e instanceof RetriableError))
|
|
103
|
+
return;
|
|
89
104
|
const h = this.health.get(ch.name) ?? { failCount: 0, disabledUntil: 0 };
|
|
90
105
|
h.failCount++;
|
|
91
106
|
if (h.failCount >= ChannelManager.FAILURE_THRESHOLD) {
|
package/dist/channels/webhook.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { webhookEndpointSchema, getErrorMessage } from '../validation.js';
|
|
8
8
|
import { validateWebhookUrl } from '../ssrf.js';
|
|
9
9
|
import { redactSecretsFromText } from '../utils/redact-headers.js';
|
|
10
|
+
import { RetriableError } from './manager.js';
|
|
10
11
|
export class WebhookChannel {
|
|
11
12
|
name = 'webhook';
|
|
12
13
|
endpoints;
|
|
@@ -141,8 +142,13 @@ export class WebhookChannel {
|
|
|
141
142
|
console.error(`Webhook ${ep.url} failed after ${maxRetries} attempts for ${event}: ${lastError}`);
|
|
142
143
|
this.addToDeadLetterQueue(ep.url, event, lastError, maxRetries);
|
|
143
144
|
}
|
|
144
|
-
// Final failure
|
|
145
|
-
|
|
145
|
+
// Final failure — throw so fire() can aggregate.
|
|
146
|
+
// Use RetriableError for 5xx/network (circuit breaker counts these),
|
|
147
|
+
// plain Error for 4xx (circuit breaker ignores these).
|
|
148
|
+
if (lastError.startsWith('HTTP ') && parseInt(lastError.slice(5)) < 500) {
|
|
149
|
+
throw new Error(lastError);
|
|
150
|
+
}
|
|
151
|
+
throw new RetriableError(lastError);
|
|
146
152
|
}
|
|
147
153
|
}
|
|
148
154
|
/** Issue #89 L14: Add a failed delivery to the dead letter queue. */
|
package/dist/events.d.ts
CHANGED
|
@@ -76,6 +76,8 @@ export declare class SessionEventBus {
|
|
|
76
76
|
private globalEmitter;
|
|
77
77
|
/** #689: Pending setImmediate timers for cleanup on destroy. */
|
|
78
78
|
private pendingTimers;
|
|
79
|
+
/** #834: Pending setTimeout timers for cleanup on destroy/cleanupSession. */
|
|
80
|
+
private pendingTimeouts;
|
|
79
81
|
/** Subscribe to events from ALL sessions (new and existing). Returns unsubscribe function. */
|
|
80
82
|
subscribeGlobal(handler: (event: GlobalSSEEvent) => void): () => void;
|
|
81
83
|
/** Emit a session created event to global subscribers. */
|
package/dist/events.js
CHANGED
|
@@ -178,12 +178,15 @@ export class SessionEventBus {
|
|
|
178
178
|
// Clean up after a short delay (let clients receive the event)
|
|
179
179
|
// Capture reference — only delete if it's still the same emitter
|
|
180
180
|
// #357: Also delete the per-session event buffer to prevent unbounded map growth
|
|
181
|
-
|
|
181
|
+
// #834: Track the timer so cleanupSession/destroy can cancel it
|
|
182
|
+
const timeout = setTimeout(() => {
|
|
183
|
+
this.pendingTimeouts.delete(timeout);
|
|
182
184
|
if (this.emitters.get(sessionId) === emitter) {
|
|
183
185
|
this.emitters.delete(sessionId);
|
|
184
186
|
}
|
|
185
187
|
this.eventBuffers.delete(sessionId);
|
|
186
188
|
}, 1000);
|
|
189
|
+
this.pendingTimeouts.add(timeout);
|
|
187
190
|
}
|
|
188
191
|
/** Emit a stall event. */
|
|
189
192
|
emitStall(sessionId, stallType, detail) {
|
|
@@ -227,6 +230,8 @@ export class SessionEventBus {
|
|
|
227
230
|
globalEmitter = null;
|
|
228
231
|
/** #689: Pending setImmediate timers for cleanup on destroy. */
|
|
229
232
|
pendingTimers = new Set();
|
|
233
|
+
/** #834: Pending setTimeout timers for cleanup on destroy/cleanupSession. */
|
|
234
|
+
pendingTimeouts = new Set();
|
|
230
235
|
/** Subscribe to events from ALL sessions (new and existing). Returns unsubscribe function. */
|
|
231
236
|
subscribeGlobal(handler) {
|
|
232
237
|
if (!this.globalEmitter) {
|
|
@@ -267,6 +272,11 @@ export class SessionEventBus {
|
|
|
267
272
|
}
|
|
268
273
|
/** #398: Clean up per-session state (call when session is killed). */
|
|
269
274
|
cleanupSession(sessionId) {
|
|
275
|
+
// #834: Clear pending setTimeout for this session's emitEnded cleanup
|
|
276
|
+
for (const timeout of this.pendingTimeouts) {
|
|
277
|
+
clearTimeout(timeout);
|
|
278
|
+
this.pendingTimeouts.delete(timeout);
|
|
279
|
+
}
|
|
270
280
|
this.eventBuffers.delete(sessionId);
|
|
271
281
|
const emitter = this.emitters.get(sessionId);
|
|
272
282
|
if (emitter) {
|
|
@@ -281,6 +291,11 @@ export class SessionEventBus {
|
|
|
281
291
|
clearImmediate(imm);
|
|
282
292
|
}
|
|
283
293
|
this.pendingTimers.clear();
|
|
294
|
+
// #834: Clear pending setTimeout timers
|
|
295
|
+
for (const timeout of this.pendingTimeouts) {
|
|
296
|
+
clearTimeout(timeout);
|
|
297
|
+
}
|
|
298
|
+
this.pendingTimeouts.clear();
|
|
284
299
|
for (const emitter of this.emitters.values()) {
|
|
285
300
|
emitter.removeAllListeners();
|
|
286
301
|
}
|
package/dist/hook-settings.js
CHANGED
|
@@ -105,14 +105,14 @@ export async function writeHookSettingsFile(baseUrl, sessionId, workDir) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
-
// Deep-merge: project settings as base,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
};
|
|
108
|
+
// Deep-merge: project settings as base, hooks merged by event key so both
|
|
109
|
+
// project-level and Aegis hooks coexist (Issue #635).
|
|
110
|
+
const existingHooks = merged.hooks ?? {};
|
|
111
|
+
const mergedHooks = { ...existingHooks };
|
|
112
|
+
for (const [event, entries] of Object.entries(hookSettings.hooks)) {
|
|
113
|
+
mergedHooks[event] = [...(existingHooks[event] ?? []), ...entries];
|
|
114
|
+
}
|
|
115
|
+
const combined = { ...merged, hooks: mergedHooks };
|
|
116
116
|
// Issue #648: Use unpredictable directory name and restrictive permissions
|
|
117
117
|
// to prevent symlink attacks and information disclosure in /tmp.
|
|
118
118
|
const suffix = randomBytes(4).toString('hex');
|
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, hookBodySchema } from './validation.js';
|
|
17
|
+
import { isValidUUID, hookBodySchema, parseIntSafe } 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. */
|
|
@@ -22,7 +22,7 @@ const AUTO_APPROVE_MODES = new Set(['bypassPermissions', 'dontAsk', 'acceptEdits
|
|
|
22
22
|
/** Default timeout for waiting on client permission decision (ms). */
|
|
23
23
|
const PERMISSION_TIMEOUT_MS = 10_000;
|
|
24
24
|
/** Default timeout for waiting on external answer to AskUserQuestion (ms). */
|
|
25
|
-
const ANSWER_TIMEOUT_MS =
|
|
25
|
+
const ANSWER_TIMEOUT_MS = parseIntSafe(process.env.ANSWER_TIMEOUT_MS, 30_000);
|
|
26
26
|
/** Valid permission_mode values accepted by Claude Code. */
|
|
27
27
|
const VALID_PERMISSION_MODES = new Set(['default', 'plan', 'bypassPermissions']);
|
|
28
28
|
/** Valid CC hook event names (allow any for extensibility, but these are known). */
|
package/dist/session.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export declare class SessionManager {
|
|
|
56
56
|
private stateFile;
|
|
57
57
|
private sessionMapFile;
|
|
58
58
|
private pollTimers;
|
|
59
|
+
/** #835: Discovery timeout timers — cleared in cleanupSession to prevent orphan callbacks. */
|
|
60
|
+
private discoveryTimeouts;
|
|
59
61
|
private saveQueue;
|
|
60
62
|
private saveDebounceTimer;
|
|
61
63
|
private static readonly SAVE_DEBOUNCE_MS;
|
package/dist/session.js
CHANGED
|
@@ -50,6 +50,8 @@ export class SessionManager {
|
|
|
50
50
|
stateFile;
|
|
51
51
|
sessionMapFile;
|
|
52
52
|
pollTimers = new Map();
|
|
53
|
+
/** #835: Discovery timeout timers — cleared in cleanupSession to prevent orphan callbacks. */
|
|
54
|
+
discoveryTimeouts = new Map();
|
|
53
55
|
saveQueue = Promise.resolve(); // #218: serialize concurrent saves
|
|
54
56
|
saveDebounceTimer = null;
|
|
55
57
|
static SAVE_DEBOUNCE_MS = 5_000; // #357: debounce offset-only saves
|
|
@@ -1150,6 +1152,14 @@ export class SessionManager {
|
|
|
1150
1152
|
this.pollTimers.delete(key);
|
|
1151
1153
|
}
|
|
1152
1154
|
}
|
|
1155
|
+
// #835: Clear discovery timeout timers to prevent orphan callbacks
|
|
1156
|
+
for (const key of [id, `fs-${id}`]) {
|
|
1157
|
+
const timeout = this.discoveryTimeouts.get(key);
|
|
1158
|
+
if (timeout) {
|
|
1159
|
+
clearTimeout(timeout);
|
|
1160
|
+
this.discoveryTimeouts.delete(key);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1153
1163
|
this.cleanupPendingPermission(id);
|
|
1154
1164
|
this.cleanupPendingQuestion(id);
|
|
1155
1165
|
this.parsedEntriesCache.delete(id);
|
|
@@ -1289,7 +1299,9 @@ export class SessionManager {
|
|
|
1289
1299
|
}, 2000);
|
|
1290
1300
|
this.pollTimers.set(id, interval);
|
|
1291
1301
|
// P3 fix: Stop after 5 minutes if not found, log timeout
|
|
1292
|
-
|
|
1302
|
+
// #835: Track the timeout so cleanupSession can cancel it
|
|
1303
|
+
const discoveryTimeout = setTimeout(() => {
|
|
1304
|
+
this.discoveryTimeouts.delete(id);
|
|
1293
1305
|
const timer = this.pollTimers.get(id);
|
|
1294
1306
|
const session = this.state.sessions[id];
|
|
1295
1307
|
if (timer) {
|
|
@@ -1301,6 +1313,7 @@ export class SessionManager {
|
|
|
1301
1313
|
}
|
|
1302
1314
|
}
|
|
1303
1315
|
}, 5 * 60 * 1000);
|
|
1316
|
+
this.discoveryTimeouts.set(id, discoveryTimeout);
|
|
1304
1317
|
}
|
|
1305
1318
|
/** Issue #16: Filesystem-based discovery for --bare mode (no hooks).
|
|
1306
1319
|
* Scans the Claude projects directory for new .jsonl files created after the session.
|
|
@@ -1348,13 +1361,16 @@ export class SessionManager {
|
|
|
1348
1361
|
}, 3000);
|
|
1349
1362
|
this.pollTimers.set(`fs-${id}`, interval);
|
|
1350
1363
|
// Timeout after 5 minutes
|
|
1351
|
-
|
|
1364
|
+
// #835: Track the timeout so cleanupSession can cancel it
|
|
1365
|
+
const fsDiscoveryTimeout = setTimeout(() => {
|
|
1366
|
+
this.discoveryTimeouts.delete(`fs-${id}`);
|
|
1352
1367
|
const timer = this.pollTimers.get(`fs-${id}`);
|
|
1353
1368
|
if (timer) {
|
|
1354
1369
|
clearInterval(timer);
|
|
1355
1370
|
this.pollTimers.delete(`fs-${id}`);
|
|
1356
1371
|
}
|
|
1357
1372
|
}, 5 * 60 * 1000);
|
|
1373
|
+
this.discoveryTimeouts.set(`fs-${id}`, fsDiscoveryTimeout);
|
|
1358
1374
|
}
|
|
1359
1375
|
/** Sync CC session IDs from the hook-written session_map.json. */
|
|
1360
1376
|
async syncSessionMap() {
|