mstro-app 0.3.0 → 0.3.4

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 (121) hide show
  1. package/README.md +3 -19
  2. package/bin/mstro.js +62 -174
  3. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  4. package/dist/server/cli/headless/claude-invoker.js +4 -3
  5. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  6. package/dist/server/cli/headless/mcp-config.js +2 -2
  7. package/dist/server/cli/headless/mcp-config.js.map +1 -1
  8. package/dist/server/cli/headless/runner.d.ts +6 -1
  9. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  10. package/dist/server/cli/headless/runner.js +36 -4
  11. package/dist/server/cli/headless/runner.js.map +1 -1
  12. package/dist/server/cli/headless/types.d.ts +1 -1
  13. package/dist/server/cli/headless/types.d.ts.map +1 -1
  14. package/dist/server/cli/improvisation-session-manager.d.ts +2 -2
  15. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  16. package/dist/server/cli/improvisation-session-manager.js +3 -2
  17. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  18. package/dist/server/index.js +6 -1
  19. package/dist/server/index.js.map +1 -1
  20. package/dist/server/mcp/bouncer-integration.d.ts +1 -1
  21. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  22. package/dist/server/mcp/bouncer-integration.js +85 -114
  23. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  24. package/dist/server/mcp/security-audit.d.ts +3 -3
  25. package/dist/server/mcp/security-audit.d.ts.map +1 -1
  26. package/dist/server/mcp/security-audit.js.map +1 -1
  27. package/dist/server/mcp/server.js +3 -2
  28. package/dist/server/mcp/server.js.map +1 -1
  29. package/dist/server/services/analytics.d.ts +2 -2
  30. package/dist/server/services/analytics.d.ts.map +1 -1
  31. package/dist/server/services/analytics.js.map +1 -1
  32. package/dist/server/services/files.js +7 -7
  33. package/dist/server/services/files.js.map +1 -1
  34. package/dist/server/services/pathUtils.js +1 -1
  35. package/dist/server/services/pathUtils.js.map +1 -1
  36. package/dist/server/services/platform.d.ts +2 -2
  37. package/dist/server/services/platform.d.ts.map +1 -1
  38. package/dist/server/services/platform.js.map +1 -1
  39. package/dist/server/services/sentry.d.ts +1 -1
  40. package/dist/server/services/sentry.d.ts.map +1 -1
  41. package/dist/server/services/sentry.js.map +1 -1
  42. package/dist/server/services/terminal/pty-manager.d.ts +10 -0
  43. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  44. package/dist/server/services/terminal/pty-manager.js +32 -4
  45. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  46. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  47. package/dist/server/services/websocket/file-utils.d.ts +4 -0
  48. package/dist/server/services/websocket/file-utils.d.ts.map +1 -1
  49. package/dist/server/services/websocket/file-utils.js +48 -23
  50. package/dist/server/services/websocket/file-utils.js.map +1 -1
  51. package/dist/server/services/websocket/git-handlers.js +17 -17
  52. package/dist/server/services/websocket/git-handlers.js.map +1 -1
  53. package/dist/server/services/websocket/git-pr-handlers.js +3 -3
  54. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
  55. package/dist/server/services/websocket/git-worktree-handlers.js +10 -10
  56. package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
  57. package/dist/server/services/websocket/handler.js +1 -1
  58. package/dist/server/services/websocket/handler.js.map +1 -1
  59. package/dist/server/services/websocket/session-handlers.d.ts +1 -1
  60. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  61. package/dist/server/services/websocket/session-handlers.js +12 -11
  62. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  63. package/dist/server/services/websocket/tab-handlers.js.map +1 -1
  64. package/dist/server/services/websocket/terminal-handlers.js +1 -1
  65. package/dist/server/services/websocket/terminal-handlers.js.map +1 -1
  66. package/dist/server/services/websocket/types.d.ts.map +1 -1
  67. package/dist/server/utils/agent-manager.d.ts +22 -2
  68. package/dist/server/utils/agent-manager.d.ts.map +1 -1
  69. package/dist/server/utils/agent-manager.js +2 -2
  70. package/dist/server/utils/agent-manager.js.map +1 -1
  71. package/dist/server/utils/paths.d.ts +0 -12
  72. package/dist/server/utils/paths.d.ts.map +1 -1
  73. package/dist/server/utils/paths.js +0 -12
  74. package/dist/server/utils/paths.js.map +1 -1
  75. package/dist/server/utils/port-manager.js.map +1 -1
  76. package/package.json +4 -3
  77. package/server/README.md +0 -1
  78. package/server/cli/headless/claude-invoker.ts +21 -16
  79. package/server/cli/headless/mcp-config.ts +8 -8
  80. package/server/cli/headless/runner.ts +32 -4
  81. package/server/cli/headless/types.ts +1 -1
  82. package/server/cli/improvisation-session-manager.ts +8 -7
  83. package/server/index.ts +15 -9
  84. package/server/mcp/README.md +0 -5
  85. package/server/mcp/bouncer-integration.ts +116 -188
  86. package/server/mcp/security-audit.ts +4 -4
  87. package/server/mcp/server.ts +6 -5
  88. package/server/services/analytics.ts +3 -3
  89. package/server/services/files.ts +13 -13
  90. package/server/services/pathUtils.ts +2 -2
  91. package/server/services/platform.ts +5 -5
  92. package/server/services/sentry.ts +1 -1
  93. package/server/services/terminal/pty-manager.ts +36 -9
  94. package/server/services/websocket/file-explorer-handlers.ts +1 -1
  95. package/server/services/websocket/file-utils.ts +52 -28
  96. package/server/services/websocket/git-handlers.ts +34 -34
  97. package/server/services/websocket/git-pr-handlers.ts +6 -6
  98. package/server/services/websocket/git-worktree-handlers.ts +20 -20
  99. package/server/services/websocket/handler.ts +2 -2
  100. package/server/services/websocket/session-handlers.ts +31 -30
  101. package/server/services/websocket/tab-handlers.ts +1 -1
  102. package/server/services/websocket/terminal-handlers.ts +2 -2
  103. package/server/services/websocket/types.ts +2 -0
  104. package/server/utils/agent-manager.ts +6 -6
  105. package/server/utils/paths.ts +0 -14
  106. package/server/utils/port-manager.ts +1 -1
  107. package/bin/configure-claude.js +0 -298
  108. package/dist/server/mcp/bouncer-cli.d.ts +0 -3
  109. package/dist/server/mcp/bouncer-cli.d.ts.map +0 -1
  110. package/dist/server/mcp/bouncer-cli.js +0 -99
  111. package/dist/server/mcp/bouncer-cli.js.map +0 -1
  112. package/hooks/bouncer.sh +0 -145
  113. package/server/cli/headless/output-utils.test.ts +0 -225
  114. package/server/cli/headless/stall-assessor.test.ts +0 -165
  115. package/server/cli/headless/tool-watchdog.test.ts +0 -429
  116. package/server/mcp/bouncer-cli.ts +0 -127
  117. package/server/mcp/bouncer-integration.test.ts +0 -161
  118. package/server/mcp/security-patterns.test.ts +0 -258
  119. package/server/services/platform.test.ts +0 -1304
  120. package/server/services/websocket/autocomplete.test.ts +0 -194
  121. package/server/services/websocket/handler.test.ts +0 -20
@@ -42,6 +42,43 @@ import {
42
42
  SAFE_OPERATIONS
43
43
  } from './security-patterns.js';
44
44
 
45
+ /** Timeout for Haiku bouncer subprocess calls (ms). Configurable via env var. */
46
+ const HAIKU_TIMEOUT_MS = parseInt(process.env.BOUNCER_HAIKU_TIMEOUT_MS || '10000', 10);
47
+
48
+ // ========== Decision Cache ==========
49
+
50
+ /** Cache TTL in ms (default 5 minutes) */
51
+ const CACHE_TTL_MS = parseInt(process.env.BOUNCER_CACHE_TTL_MS || '300000', 10);
52
+ const CACHE_MAX_SIZE = 200;
53
+
54
+ interface CachedDecision {
55
+ decision: BouncerDecision;
56
+ expiresAt: number;
57
+ }
58
+
59
+ const decisionCache = new Map<string, CachedDecision>();
60
+
61
+ function getCachedDecision(operation: string): BouncerDecision | null {
62
+ const entry = decisionCache.get(operation);
63
+ if (!entry) return null;
64
+ if (Date.now() > entry.expiresAt) {
65
+ decisionCache.delete(operation);
66
+ return null;
67
+ }
68
+ return entry.decision;
69
+ }
70
+
71
+ function cacheDecision(operation: string, decision: BouncerDecision): void {
72
+ // Don't cache low-confidence or error-fallback decisions
73
+ if (decision.confidence < 50) return;
74
+ // Evict oldest entries if cache is full
75
+ if (decisionCache.size >= CACHE_MAX_SIZE) {
76
+ const firstKey = decisionCache.keys().next().value;
77
+ if (firstKey !== undefined) decisionCache.delete(firstKey);
78
+ }
79
+ decisionCache.set(operation, { decision, expiresAt: Date.now() + CACHE_TTL_MS });
80
+ }
81
+
45
82
  export interface BouncerReviewRequest {
46
83
  operation: string;
47
84
  context?: {
@@ -53,7 +90,7 @@ export interface BouncerReviewRequest {
53
90
  userRequest?: string;
54
91
  conversationHistory?: string[];
55
92
  sessionId?: string;
56
- [key: string]: any;
93
+ [key: string]: unknown;
57
94
  };
58
95
  }
59
96
 
@@ -98,7 +135,7 @@ function tryExtractJsonBlock(text: string): string {
98
135
  return text;
99
136
  }
100
137
 
101
- function validateDecision(parsed: any): BouncerDecision {
138
+ function validateDecision(parsed: Record<string, unknown>): BouncerDecision {
102
139
  if (!parsed || typeof parsed.decision !== 'string') {
103
140
  console.error('[Bouncer] Invalid parsed response:', parsed);
104
141
  throw new Error('Haiku returned invalid response: missing or invalid decision field');
@@ -111,11 +148,11 @@ function validateDecision(parsed: any): BouncerDecision {
111
148
  }
112
149
 
113
150
  return {
114
- decision: parsed.decision,
115
- confidence: parsed.confidence || 0,
116
- reasoning: parsed.reasoning || 'No reasoning provided',
117
- threatLevel: parsed.threat_level || 'medium',
118
- alternative: parsed.alternative
151
+ decision: parsed.decision as BouncerDecision['decision'],
152
+ confidence: (parsed.confidence as number) || 0,
153
+ reasoning: (parsed.reasoning as string) || 'No reasoning provided',
154
+ threatLevel: (parsed.threat_level as BouncerDecision['threatLevel']) || 'medium',
155
+ alternative: parsed.alternative as string | undefined
119
156
  };
120
157
  }
121
158
 
@@ -195,7 +232,7 @@ or
195
232
  const timer = setTimeout(() => {
196
233
  timedOut = true;
197
234
  child.kill('SIGTERM');
198
- }, 10000);
235
+ }, HAIKU_TIMEOUT_MS);
199
236
 
200
237
  child.stdout.on('data', (data) => {
201
238
  output += data.toString();
@@ -209,7 +246,7 @@ or
209
246
  clearTimeout(timer);
210
247
 
211
248
  if (timedOut) {
212
- reject(new Error('Haiku analysis timeout after 10s'));
249
+ reject(new Error(`Haiku analysis timed out after ${HAIKU_TIMEOUT_MS}ms`));
213
250
  return;
214
251
  }
215
252
 
@@ -221,9 +258,9 @@ or
221
258
  try {
222
259
  const decision = parseHaikuResponse(output.trim());
223
260
  resolve(decision);
224
- } catch (error: any) {
261
+ } catch (error: unknown) {
225
262
  console.error('[Bouncer] Parse error details:', error);
226
- reject(new Error(`Failed to parse Haiku response: ${error.message}`));
263
+ reject(new Error(`Failed to parse Haiku response: ${error instanceof Error ? error.message : String(error)}`));
227
264
  }
228
265
  });
229
266
 
@@ -234,16 +271,55 @@ or
234
271
  });
235
272
  }
236
273
 
274
+ /**
275
+ * Finalize a bouncer decision: log, track analytics, cache, and return.
276
+ */
277
+ function finalizeDecision(
278
+ operation: string,
279
+ decision: BouncerDecision,
280
+ layer: string,
281
+ startTime: number,
282
+ context: BouncerReviewRequest['context'],
283
+ logFn: typeof import('./security-audit.js')['logBouncerDecision'],
284
+ opts?: { error?: string; skipCache?: boolean; skipAnalytics?: boolean },
285
+ ): BouncerDecision {
286
+ const latencyMs = Math.round(performance.now() - startTime);
287
+
288
+ logFn(operation, decision.decision, decision.confidence, decision.reasoning, {
289
+ context, threatLevel: decision.threatLevel, layer, latencyMs, ...(opts?.error && { error: opts.error }),
290
+ });
291
+
292
+ if (!opts?.skipAnalytics) {
293
+ const event = decision.decision === 'deny' ? AnalyticsEvents.BOUNCER_TOOL_DENIED : AnalyticsEvents.BOUNCER_TOOL_ALLOWED;
294
+ trackEvent(event, {
295
+ layer,
296
+ operation_length: operation.length,
297
+ threat_level: decision.threatLevel,
298
+ confidence: decision.confidence,
299
+ latency_ms: latencyMs,
300
+ });
301
+ }
302
+
303
+ if (!opts?.skipCache) cacheDecision(operation, decision);
304
+ return decision;
305
+ }
306
+
237
307
  /**
238
308
  * Main bouncer review function - 2-layer hybrid system
239
309
  */
240
310
  export async function reviewOperation(request: BouncerReviewRequest): Promise<BouncerDecision> {
241
- // Import audit logger
242
311
  const { logBouncerDecision } = await import('./security-audit.js');
243
-
244
312
  const startTime = performance.now();
245
-
246
313
  const { operation } = request;
314
+ const fin = (d: BouncerDecision, layer: string, opts?: Parameters<typeof finalizeDecision>[6]) =>
315
+ finalizeDecision(operation, d, layer, startTime, request.context, logBouncerDecision, opts);
316
+
317
+ // Check cache first
318
+ const cached = getCachedDecision(operation);
319
+ if (cached) {
320
+ console.error(`[Bouncer] ⚡ Cache hit: ${cached.decision} (${cached.confidence}%)`);
321
+ return cached;
322
+ }
247
323
 
248
324
  console.error('[Bouncer] Analyzing operation...');
249
325
  console.error(`[Bouncer] Operation: ${operation}`);
@@ -251,218 +327,70 @@ export async function reviewOperation(request: BouncerReviewRequest): Promise<Bo
251
327
  console.error(`[Bouncer] User request: ${request.context.userRequest}`);
252
328
  }
253
329
 
254
- // ========================================
255
- // PRE-CHECK: Malformed/empty tool calls
256
- // ========================================
257
- // Empty-param Edit/Write calls are no-ops that will fail validation anyway.
258
- // Allow immediately instead of wasting ~8s on Haiku analysis.
330
+ // PRE-CHECK: Empty-param Edit/Write calls are no-ops — allow immediately
259
331
  const toolInput = request.context?.toolInput;
260
332
  if (toolInput && typeof toolInput === 'object' && Object.keys(toolInput).length === 0) {
261
333
  console.error('[Bouncer] ⚡ Fast path: Empty tool parameters (no-op)');
262
- const latencyMs = Math.round(performance.now() - startTime);
263
-
264
- const decision: BouncerDecision = {
265
- decision: 'allow',
266
- confidence: 95,
267
- reasoning: 'Empty tool parameters - operation is a no-op with no side effects.',
268
- threatLevel: 'low'
269
- };
270
-
271
- logBouncerDecision(
272
- operation,
273
- decision.decision,
274
- decision.confidence,
275
- decision.reasoning,
276
- { context: request.context, threatLevel: decision.threatLevel, layer: 'pattern-noop', latencyMs }
277
- );
278
-
279
- return decision;
334
+ return fin({ decision: 'allow', confidence: 95, reasoning: 'Empty tool parameters - operation is a no-op with no side effects.', threatLevel: 'low' }, 'pattern-noop', { skipAnalytics: true });
280
335
  }
281
336
 
282
- // ========================================
283
337
  // LAYER 1: Pattern-Based Fast Path (< 5ms)
284
- // ========================================
285
338
 
286
- // Check safe operations FIRST - allows trusted sources (e.g., brew, rustup)
339
+ // Check safe operations FIRST allows trusted sources (e.g., brew, rustup)
287
340
  // to pass before hitting critical threat patterns like curl|bash
288
341
  const safeOperation = matchesPattern(operation, SAFE_OPERATIONS);
289
342
  if (safeOperation) {
290
343
  console.error('[Bouncer] ⚡ Fast path: Safe operation approved');
291
- const latencyMs = Math.round(performance.now() - startTime);
292
-
293
- const decision: BouncerDecision = {
294
- decision: 'allow',
295
- confidence: 95,
296
- reasoning: 'Operation matches known-safe patterns. No security concerns detected.',
297
- threatLevel: 'low'
298
- };
299
-
300
- logBouncerDecision(
301
- operation,
302
- decision.decision,
303
- decision.confidence,
304
- decision.reasoning,
305
- { context: request.context, threatLevel: decision.threatLevel, layer: 'pattern-safe', latencyMs }
306
- );
307
- trackEvent(AnalyticsEvents.BOUNCER_TOOL_ALLOWED, {
308
- layer: 'pattern-safe',
309
- operation_length: operation.length,
310
- threat_level: 'low',
311
- confidence: 95,
312
- latency_ms: latencyMs,
313
- });
314
-
315
- return decision;
344
+ return fin({ decision: 'allow', confidence: 95, reasoning: 'Operation matches known-safe patterns. No security concerns detected.', threatLevel: 'low' }, 'pattern-safe');
316
345
  }
317
346
 
318
- // Check critical threats (catastrophic operations like rm -rf /, fork bombs)
319
- // These are ALWAYS denied - no context can justify them
347
+ // Critical threats (rm -rf /, fork bombs) — ALWAYS denied
320
348
  const criticalThreat = matchesPattern(operation, CRITICAL_THREATS);
321
349
  if (criticalThreat) {
322
350
  console.error('[Bouncer] ⚡ Fast path: CRITICAL THREAT detected');
323
- const latencyMs = Math.round(performance.now() - startTime);
324
-
325
- const decision: BouncerDecision = {
326
- decision: 'deny',
327
- confidence: 99,
328
- reasoning: `🚨 CRITICAL THREAT: ${criticalThreat.reason}`,
329
- threatLevel: 'critical',
351
+ return fin({
352
+ decision: 'deny', confidence: 99, reasoning: `🚨 CRITICAL THREAT: ${criticalThreat.reason}`, threatLevel: 'critical',
330
353
  alternative: 'This operation should never be performed. If you need to accomplish a specific task, please describe your goal and I can suggest safe alternatives.',
331
- enforceable: true
332
- };
333
-
334
- logBouncerDecision(
335
- operation,
336
- decision.decision,
337
- decision.confidence,
338
- decision.reasoning,
339
- { context: request.context, threatLevel: decision.threatLevel, layer: 'pattern-critical', latencyMs }
340
- );
341
- trackEvent(AnalyticsEvents.BOUNCER_TOOL_DENIED, {
342
- layer: 'pattern-critical',
343
- operation_length: operation.length,
344
- threat_level: 'critical',
345
- confidence: 99,
346
- latency_ms: latencyMs,
347
- });
348
-
349
- return decision;
354
+ enforceable: true,
355
+ }, 'pattern-critical');
350
356
  }
351
357
 
352
- // ========================================
353
358
  // LAYER 2: Haiku AI Analysis (~200-500ms)
354
- // ========================================
355
359
 
356
- // Only invoke AI for operations that truly need context
360
+ // Default allow for operations that don't need AI review
357
361
  if (!requiresAIReview(operation)) {
358
- // Default allow for operations that don't match any pattern
359
362
  console.error('[Bouncer] ⚡ Fast path: No concerning patterns, allowing');
360
- const latencyMs = Math.round(performance.now() - startTime);
361
-
362
- const decision: BouncerDecision = {
363
- decision: 'allow',
364
- confidence: 80,
365
- reasoning: 'Operation appears safe based on pattern analysis. No obvious threats detected.',
366
- threatLevel: 'low'
367
- };
368
-
369
- logBouncerDecision(
370
- operation,
371
- decision.decision,
372
- decision.confidence,
373
- decision.reasoning,
374
- { context: request.context, threatLevel: decision.threatLevel, layer: 'pattern-default', latencyMs }
375
- );
376
- trackEvent(AnalyticsEvents.BOUNCER_TOOL_ALLOWED, {
377
- layer: 'pattern-default',
378
- operation_length: operation.length,
379
- threat_level: 'low',
380
- confidence: 80,
381
- latency_ms: latencyMs,
382
- });
383
-
384
- return decision;
363
+ return fin({ decision: 'allow', confidence: 80, reasoning: 'Operation appears safe based on pattern analysis. No obvious threats detected.', threatLevel: 'low' }, 'pattern-default');
385
364
  }
386
365
 
387
- // Check if AI analysis is enabled
388
- const useAI = process.env.BOUNCER_USE_AI !== 'false';
389
-
390
- if (!useAI) {
366
+ if (process.env.BOUNCER_USE_AI === 'false') {
391
367
  console.error('[Bouncer] AI analysis disabled (BOUNCER_USE_AI=false)');
392
- const latencyMs = Math.round(performance.now() - startTime);
393
-
394
- const decision: BouncerDecision = {
395
- decision: 'warn_allow',
396
- confidence: 60,
397
- reasoning: 'Operation requires review but AI analysis is disabled. Proceeding with caution.',
398
- threatLevel: 'medium'
399
- };
400
-
401
- logBouncerDecision(
402
- operation,
403
- decision.decision,
404
- decision.confidence,
405
- decision.reasoning,
406
- { context: request.context, threatLevel: decision.threatLevel, layer: 'ai-disabled', latencyMs }
407
- );
408
-
409
- return decision;
368
+ return fin({ decision: 'warn_allow', confidence: 60, reasoning: 'Operation requires review but AI analysis is disabled. Proceeding with caution.', threatLevel: 'medium' }, 'ai-disabled', { skipCache: true, skipAnalytics: true });
410
369
  }
411
370
 
412
371
  console.error('[Bouncer] 🤖 Invoking Haiku for AI analysis...');
413
- trackEvent(AnalyticsEvents.BOUNCER_HAIKU_REVIEW, {
414
- operation_length: operation.length,
415
- });
372
+ trackEvent(AnalyticsEvents.BOUNCER_HAIKU_REVIEW, { operation_length: operation.length });
416
373
 
417
- // Get Claude command and working directory from context or use defaults
418
374
  const claudeCommand = process.env.CLAUDE_COMMAND || 'claude';
419
375
  const workingDir = request.context?.workingDirectory || process.cwd();
420
376
 
421
377
  try {
422
378
  const decision = await analyzeWithHaiku(request, claudeCommand, workingDir);
423
- const latencyMs = Math.round(performance.now() - startTime);
424
- console.error(`[Bouncer] ✓ Haiku decision: ${decision.decision} (${decision.confidence}% confidence) [${latencyMs}ms]`);
379
+ console.error(`[Bouncer] Haiku decision: ${decision.decision} (${decision.confidence}% confidence) [${Math.round(performance.now() - startTime)}ms]`);
425
380
  console.error(`[Bouncer] Reasoning: ${decision.reasoning}`);
381
+ return fin(decision, 'haiku-ai');
382
+ } catch (error: unknown) {
383
+ const errorMessage = error instanceof Error ? error.message : String(error);
384
+
385
+ if (errorMessage.includes('timed out')) {
386
+ console.error(`[Bouncer] ⚠️ Haiku analysis timed out after ${HAIKU_TIMEOUT_MS}ms — defaulting to ALLOW`);
387
+ captureException(error, { context: 'bouncer.haiku_timeout', operation });
388
+ return fin({ decision: 'allow', confidence: 50, reasoning: `Security analysis timed out after ${HAIKU_TIMEOUT_MS}ms. Defaulting to allow — user initiated the action.`, threatLevel: 'medium' }, 'haiku-timeout', { skipCache: true });
389
+ }
426
390
 
427
- logBouncerDecision(
428
- operation,
429
- decision.decision,
430
- decision.confidence,
431
- decision.reasoning,
432
- { context: request.context, threatLevel: decision.threatLevel, layer: 'haiku-ai', latencyMs }
433
- );
434
- trackEvent(decision.decision === 'deny' ? AnalyticsEvents.BOUNCER_TOOL_DENIED : AnalyticsEvents.BOUNCER_TOOL_ALLOWED, {
435
- layer: 'haiku-ai',
436
- operation_length: operation.length,
437
- threat_level: decision.threatLevel,
438
- confidence: decision.confidence,
439
- latency_ms: latencyMs,
440
- });
441
-
442
- return decision;
443
-
444
- } catch (error: any) {
445
- const latencyMs = Math.round(performance.now() - startTime);
446
- console.error(`[Bouncer] ⚠️ Haiku analysis failed: ${error.message}`);
391
+ console.error(`[Bouncer] ⚠️ Haiku analysis failed: ${errorMessage}`);
447
392
  captureException(error, { context: 'bouncer.haiku_analysis', operation });
448
-
449
- // Fail-safe: deny on AI failure
450
- const decision: BouncerDecision = {
451
- decision: 'deny',
452
- confidence: 0,
453
- reasoning: `Security analysis failed: ${error.message}. Denying for safety.`,
454
- threatLevel: 'critical'
455
- };
456
-
457
- logBouncerDecision(
458
- operation,
459
- decision.decision,
460
- decision.confidence,
461
- decision.reasoning,
462
- { context: request.context, threatLevel: decision.threatLevel, layer: 'ai-error', latencyMs, error: error.message }
463
- );
464
-
465
- return decision;
393
+ return fin({ decision: 'deny', confidence: 0, reasoning: `Security analysis failed: ${errorMessage}. Denying for safety.`, threatLevel: 'critical' }, 'ai-error', { skipCache: true, skipAnalytics: true, error: errorMessage });
466
394
  }
467
395
  }
468
396
 
@@ -19,7 +19,7 @@ export interface AuditLogEntry {
19
19
  timestamp: string;
20
20
  sessionId?: string;
21
21
  operation: string;
22
- context?: any;
22
+ context?: unknown;
23
23
  decision: 'allow' | 'deny' | 'warn_allow';
24
24
  confidence: number;
25
25
  reasoning: string;
@@ -68,7 +68,7 @@ export class SecurityAuditLogger {
68
68
  confidence: number,
69
69
  reasoning: string,
70
70
  metadata?: {
71
- context?: any;
71
+ context?: unknown;
72
72
  threatLevel?: string;
73
73
  layer?: BouncerLayer;
74
74
  latencyMs?: number;
@@ -109,14 +109,14 @@ export function logBouncerDecision(
109
109
  decision: 'allow' | 'deny' | 'warn_allow' | undefined,
110
110
  confidence: number,
111
111
  reasoning: string,
112
- metadata?: any
112
+ metadata?: Record<string, unknown>
113
113
  ): void {
114
114
  // Defensive: handle undefined or invalid decision
115
115
  const safeDecision = decision ?? 'deny';
116
116
  const validDecisions = ['allow', 'deny', 'warn_allow'];
117
117
  const normalizedDecision = validDecisions.includes(safeDecision) ? safeDecision : 'deny';
118
118
 
119
- const workingDir = metadata?.context?.workingDirectory;
119
+ const workingDir = (metadata?.context as Record<string, unknown> | undefined)?.workingDirectory as string | undefined;
120
120
  const logger = getAuditLogger(workingDir);
121
121
  logger.logDecision(operation, normalizedDecision as 'allow' | 'deny' | 'warn_allow', confidence, reasoning, metadata);
122
122
 
@@ -73,7 +73,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
73
73
 
74
74
  const { tool_name, input } = request.params.arguments as {
75
75
  tool_name: string;
76
- input: Record<string, any>;
76
+ input: Record<string, unknown>;
77
77
  };
78
78
 
79
79
  console.error(`[MCP Bouncer] Analyzing ${tool_name} request...`);
@@ -84,7 +84,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
84
84
 
85
85
  // Extract file path with multiple property name support
86
86
  // Claude Code may use file_path, filePath, or path depending on context
87
- const getFilePath = (inp: Record<string, any>) =>
87
+ const getFilePath = (inp: Record<string, unknown>) =>
88
88
  inp.file_path || inp.filePath || inp.path;
89
89
 
90
90
  if (tool_name === 'Bash' && input.command) {
@@ -141,8 +141,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
141
141
  },
142
142
  ],
143
143
  };
144
- } catch (error: any) {
145
- console.error(`[MCP Bouncer] Error: ${error.message}`);
144
+ } catch (error: unknown) {
145
+ const errorMessage = error instanceof Error ? error.message : String(error);
146
+ console.error(`[MCP Bouncer] Error: ${errorMessage}`);
146
147
 
147
148
  // Fail-safe: deny on error
148
149
  return {
@@ -151,7 +152,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
151
152
  type: 'text',
152
153
  text: JSON.stringify({
153
154
  behavior: 'deny',
154
- message: `Security analysis failed: ${error.message}. Denying for safety.`,
155
+ message: `Security analysis failed: ${errorMessage}. Denying for safety.`,
155
156
  }),
156
157
  },
157
158
  ],
@@ -158,7 +158,7 @@ function getDistinctId(): string {
158
158
  /**
159
159
  * Get common properties included with all events
160
160
  */
161
- function getCommonProperties(): Record<string, any> {
161
+ function getCommonProperties(): Record<string, unknown> {
162
162
  return {
163
163
  os: platform(),
164
164
  arch: arch(),
@@ -171,7 +171,7 @@ function getCommonProperties(): Record<string, any> {
171
171
  /**
172
172
  * Track a custom event
173
173
  */
174
- export function trackEvent(event: string, properties?: Record<string, any>): void {
174
+ export function trackEvent(event: string, properties?: Record<string, unknown>): void {
175
175
  if (!client || !isTelemetryEnabled()) return
176
176
 
177
177
  client.capture({
@@ -187,7 +187,7 @@ export function trackEvent(event: string, properties?: Record<string, any>): voi
187
187
  /**
188
188
  * Identify a user (call after login)
189
189
  */
190
- export function identifyUser(userId: string, properties?: Record<string, any>): void {
190
+ export function identifyUser(userId: string, properties?: Record<string, unknown>): void {
191
191
  if (!client || !isTelemetryEnabled()) return
192
192
 
193
193
  // Link the client ID to the user ID
@@ -332,9 +332,9 @@ export function listDirectory(
332
332
  success: true,
333
333
  entries: directoryEntries
334
334
  }
335
- } catch (error: any) {
335
+ } catch (error: unknown) {
336
336
  // Handle permission errors gracefully
337
- if (error.code === 'EACCES') {
337
+ if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'EACCES') {
338
338
  return {
339
339
  success: false,
340
340
  error: 'Permission denied'
@@ -344,7 +344,7 @@ export function listDirectory(
344
344
  console.error('[FileService] Error listing directory:', error)
345
345
  return {
346
346
  success: false,
347
- error: error.message || 'Failed to list directory'
347
+ error: error instanceof Error ? error.message : 'Failed to list directory'
348
348
  }
349
349
  }
350
350
  }
@@ -408,11 +408,11 @@ export function writeFile(
408
408
  success: true,
409
409
  path: resolvedPath.replace(`${workingDir}/`, '')
410
410
  }
411
- } catch (error: any) {
411
+ } catch (error: unknown) {
412
412
  console.error('[FileService] Error writing file:', error)
413
413
  return {
414
414
  success: false,
415
- error: error.message || 'Failed to write file'
415
+ error: error instanceof Error ? error.message : 'Failed to write file'
416
416
  }
417
417
  }
418
418
  }
@@ -471,11 +471,11 @@ export function createFile(
471
471
  success: true,
472
472
  path: resolvedPath.replace(`${workingDir}/`, '')
473
473
  }
474
- } catch (error: any) {
474
+ } catch (error: unknown) {
475
475
  console.error('[FileService] Error creating file:', error)
476
476
  return {
477
477
  success: false,
478
- error: error.message || 'Failed to create file'
478
+ error: error instanceof Error ? error.message : 'Failed to create file'
479
479
  }
480
480
  }
481
481
  }
@@ -536,11 +536,11 @@ export function createDirectory(
536
536
  success: true,
537
537
  path: resolvedPath.replace(`${workingDir}/`, '')
538
538
  }
539
- } catch (error: any) {
539
+ } catch (error: unknown) {
540
540
  console.error('[FileService] Error creating directory:', error)
541
541
  return {
542
542
  success: false,
543
- error: error.message || 'Failed to create directory'
543
+ error: error instanceof Error ? error.message : 'Failed to create directory'
544
544
  }
545
545
  }
546
546
  }
@@ -618,11 +618,11 @@ export function deleteFile(
618
618
  success: true,
619
619
  path: resolvedPath.replace(`${workingDir}/`, '')
620
620
  }
621
- } catch (error: any) {
621
+ } catch (error: unknown) {
622
622
  console.error('[FileService] Error deleting file:', error)
623
623
  return {
624
624
  success: false,
625
- error: error.message || 'Failed to delete'
625
+ error: error instanceof Error ? error.message : 'Failed to delete'
626
626
  }
627
627
  }
628
628
  }
@@ -700,11 +700,11 @@ export function renameFile(
700
700
  success: true,
701
701
  path: resolvedNewPath.replace(`${workingDir}/`, '')
702
702
  }
703
- } catch (error: any) {
703
+ } catch (error: unknown) {
704
704
  console.error('[FileService] Error renaming file:', error)
705
705
  return {
706
706
  success: false,
707
- error: error.message || 'Failed to rename'
707
+ error: error instanceof Error ? error.message : 'Failed to rename'
708
708
  }
709
709
  }
710
710
  }
@@ -71,12 +71,12 @@ export function validatePathWithinWorkingDir(
71
71
  valid: true,
72
72
  resolvedPath
73
73
  };
74
- } catch (error: any) {
74
+ } catch (error: unknown) {
75
75
  console.error('[PathUtils] Error validating path:', error);
76
76
  return {
77
77
  valid: false,
78
78
  resolvedPath: '',
79
- error: `Invalid path: ${error.message}`
79
+ error: `Invalid path: ${error instanceof Error ? error.message : String(error)}`
80
80
  };
81
81
  }
82
82
  }
@@ -121,7 +121,7 @@ interface ConnectionCallbacks {
121
121
  onError?: (error: string) => void
122
122
  onWebConnected?: () => void
123
123
  onWebDisconnected?: () => void
124
- onRelayedMessage?: (message: any) => void
124
+ onRelayedMessage?: (message: unknown) => void
125
125
  }
126
126
 
127
127
  /**
@@ -351,15 +351,15 @@ export class PlatformConnection {
351
351
  }
352
352
  }
353
353
 
354
- private handleMessage(message: any): void {
354
+ private handleMessage(message: Record<string, unknown>): void {
355
355
  switch (message.type) {
356
356
  case 'paired':
357
357
  this.isConnected = true
358
- this.connectionId = message.connectionId
358
+ this.connectionId = message.connectionId as string
359
359
  // Connection status printed by onConnected callback
360
360
  // Start heartbeat to keep server-side TTL refreshed
361
361
  this.startHeartbeat()
362
- this.callbacks.onConnected?.(message.connectionId)
362
+ this.callbacks.onConnected?.(message.connectionId as string)
363
363
  break
364
364
 
365
365
  case 'web_connected':
@@ -404,7 +404,7 @@ export class PlatformConnection {
404
404
  /**
405
405
  * Send message to platform (will be relayed to web if connected)
406
406
  */
407
- send(message: any): void {
407
+ send(message: unknown): void {
408
408
  if (this.ws && this.ws.readyState === WebSocketImpl.OPEN) {
409
409
  this.ws.send(JSON.stringify(message))
410
410
  }
@@ -65,7 +65,7 @@ export function initSentry(): void {
65
65
  })
66
66
  }
67
67
 
68
- export function captureException(error: unknown, context?: Record<string, any>): void {
68
+ export function captureException(error: unknown, context?: Record<string, unknown>): void {
69
69
  if (!initialized) return
70
70
  Sentry.captureException(error, context ? { extra: context } : undefined)
71
71
  }