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.
- package/README.md +3 -19
- package/bin/mstro.js +62 -174
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +4 -3
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/mcp-config.js +2 -2
- package/dist/server/cli/headless/mcp-config.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts +6 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +36 -4
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts +1 -1
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +2 -2
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +3 -2
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/index.js +6 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +85 -114
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-audit.d.ts +3 -3
- package/dist/server/mcp/security-audit.d.ts.map +1 -1
- package/dist/server/mcp/security-audit.js.map +1 -1
- package/dist/server/mcp/server.js +3 -2
- package/dist/server/mcp/server.js.map +1 -1
- package/dist/server/services/analytics.d.ts +2 -2
- package/dist/server/services/analytics.d.ts.map +1 -1
- package/dist/server/services/analytics.js.map +1 -1
- package/dist/server/services/files.js +7 -7
- package/dist/server/services/files.js.map +1 -1
- package/dist/server/services/pathUtils.js +1 -1
- package/dist/server/services/pathUtils.js.map +1 -1
- package/dist/server/services/platform.d.ts +2 -2
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/sentry.d.ts +1 -1
- package/dist/server/services/sentry.d.ts.map +1 -1
- package/dist/server/services/sentry.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts +10 -0
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +32 -4
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
- package/dist/server/services/websocket/file-utils.d.ts +4 -0
- package/dist/server/services/websocket/file-utils.d.ts.map +1 -1
- package/dist/server/services/websocket/file-utils.js +48 -23
- package/dist/server/services/websocket/file-utils.js.map +1 -1
- package/dist/server/services/websocket/git-handlers.js +17 -17
- package/dist/server/services/websocket/git-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-pr-handlers.js +3 -3
- package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-worktree-handlers.js +10 -10
- package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler.js +1 -1
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +12 -11
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/terminal-handlers.js +1 -1
- package/dist/server/services/websocket/terminal-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/utils/agent-manager.d.ts +22 -2
- package/dist/server/utils/agent-manager.d.ts.map +1 -1
- package/dist/server/utils/agent-manager.js +2 -2
- package/dist/server/utils/agent-manager.js.map +1 -1
- package/dist/server/utils/paths.d.ts +0 -12
- package/dist/server/utils/paths.d.ts.map +1 -1
- package/dist/server/utils/paths.js +0 -12
- package/dist/server/utils/paths.js.map +1 -1
- package/dist/server/utils/port-manager.js.map +1 -1
- package/package.json +4 -3
- package/server/README.md +0 -1
- package/server/cli/headless/claude-invoker.ts +21 -16
- package/server/cli/headless/mcp-config.ts +8 -8
- package/server/cli/headless/runner.ts +32 -4
- package/server/cli/headless/types.ts +1 -1
- package/server/cli/improvisation-session-manager.ts +8 -7
- package/server/index.ts +15 -9
- package/server/mcp/README.md +0 -5
- package/server/mcp/bouncer-integration.ts +116 -188
- package/server/mcp/security-audit.ts +4 -4
- package/server/mcp/server.ts +6 -5
- package/server/services/analytics.ts +3 -3
- package/server/services/files.ts +13 -13
- package/server/services/pathUtils.ts +2 -2
- package/server/services/platform.ts +5 -5
- package/server/services/sentry.ts +1 -1
- package/server/services/terminal/pty-manager.ts +36 -9
- package/server/services/websocket/file-explorer-handlers.ts +1 -1
- package/server/services/websocket/file-utils.ts +52 -28
- package/server/services/websocket/git-handlers.ts +34 -34
- package/server/services/websocket/git-pr-handlers.ts +6 -6
- package/server/services/websocket/git-worktree-handlers.ts +20 -20
- package/server/services/websocket/handler.ts +2 -2
- package/server/services/websocket/session-handlers.ts +31 -30
- package/server/services/websocket/tab-handlers.ts +1 -1
- package/server/services/websocket/terminal-handlers.ts +2 -2
- package/server/services/websocket/types.ts +2 -0
- package/server/utils/agent-manager.ts +6 -6
- package/server/utils/paths.ts +0 -14
- package/server/utils/port-manager.ts +1 -1
- package/bin/configure-claude.js +0 -298
- package/dist/server/mcp/bouncer-cli.d.ts +0 -3
- package/dist/server/mcp/bouncer-cli.d.ts.map +0 -1
- package/dist/server/mcp/bouncer-cli.js +0 -99
- package/dist/server/mcp/bouncer-cli.js.map +0 -1
- package/hooks/bouncer.sh +0 -145
- package/server/cli/headless/output-utils.test.ts +0 -225
- package/server/cli/headless/stall-assessor.test.ts +0 -165
- package/server/cli/headless/tool-watchdog.test.ts +0 -429
- package/server/mcp/bouncer-cli.ts +0 -127
- package/server/mcp/bouncer-integration.test.ts +0 -161
- package/server/mcp/security-patterns.test.ts +0 -258
- package/server/services/platform.test.ts +0 -1304
- package/server/services/websocket/autocomplete.test.ts +0 -194
- 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]:
|
|
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:
|
|
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
|
-
},
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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
|
|
package/server/mcp/server.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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:
|
|
145
|
-
|
|
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: ${
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
package/server/services/files.ts
CHANGED
|
@@ -332,9 +332,9 @@ export function listDirectory(
|
|
|
332
332
|
success: true,
|
|
333
333
|
entries: directoryEntries
|
|
334
334
|
}
|
|
335
|
-
} catch (error:
|
|
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
|
|
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:
|
|
411
|
+
} catch (error: unknown) {
|
|
412
412
|
console.error('[FileService] Error writing file:', error)
|
|
413
413
|
return {
|
|
414
414
|
success: false,
|
|
415
|
-
error: error.message
|
|
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:
|
|
474
|
+
} catch (error: unknown) {
|
|
475
475
|
console.error('[FileService] Error creating file:', error)
|
|
476
476
|
return {
|
|
477
477
|
success: false,
|
|
478
|
-
error: error.message
|
|
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:
|
|
539
|
+
} catch (error: unknown) {
|
|
540
540
|
console.error('[FileService] Error creating directory:', error)
|
|
541
541
|
return {
|
|
542
542
|
success: false,
|
|
543
|
-
error: error.message
|
|
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:
|
|
621
|
+
} catch (error: unknown) {
|
|
622
622
|
console.error('[FileService] Error deleting file:', error)
|
|
623
623
|
return {
|
|
624
624
|
success: false,
|
|
625
|
-
error: error.message
|
|
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:
|
|
703
|
+
} catch (error: unknown) {
|
|
704
704
|
console.error('[FileService] Error renaming file:', error)
|
|
705
705
|
return {
|
|
706
706
|
success: false,
|
|
707
|
-
error: error.message
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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
|
}
|