mstro-app 0.1.58 → 0.3.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.
- package/PRIVACY.md +126 -0
- package/README.md +24 -23
- package/bin/commands/login.js +85 -42
- package/bin/commands/logout.js +35 -1
- package/bin/commands/status.js +1 -1
- package/bin/mstro.js +231 -131
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +550 -115
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/index.d.ts +2 -1
- package/dist/server/cli/headless/index.d.ts.map +1 -1
- package/dist/server/cli/headless/index.js +2 -0
- package/dist/server/cli/headless/index.js.map +1 -1
- package/dist/server/cli/headless/prompt-utils.d.ts +5 -8
- package/dist/server/cli/headless/prompt-utils.d.ts.map +1 -1
- package/dist/server/cli/headless/prompt-utils.js +40 -5
- package/dist/server/cli/headless/prompt-utils.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +52 -7
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts +79 -1
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +355 -20
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +70 -0
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -0
- package/dist/server/cli/headless/tool-watchdog.js +302 -0
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -0
- package/dist/server/cli/headless/types.d.ts +98 -1
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +136 -2
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +929 -132
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/index.js +5 -13
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +18 -0
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-audit.d.ts +2 -2
- package/dist/server/mcp/security-audit.d.ts.map +1 -1
- package/dist/server/mcp/security-audit.js +12 -8
- package/dist/server/mcp/security-audit.js.map +1 -1
- package/dist/server/mcp/security-patterns.d.ts.map +1 -1
- package/dist/server/mcp/security-patterns.js +9 -4
- package/dist/server/mcp/security-patterns.js.map +1 -1
- package/dist/server/routes/improvise.js +6 -6
- package/dist/server/routes/improvise.js.map +1 -1
- package/dist/server/services/analytics.d.ts +2 -0
- package/dist/server/services/analytics.d.ts.map +1 -1
- package/dist/server/services/analytics.js +26 -4
- package/dist/server/services/analytics.js.map +1 -1
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +17 -10
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/sandbox-utils.d.ts +6 -0
- package/dist/server/services/sandbox-utils.d.ts.map +1 -0
- package/dist/server/services/sandbox-utils.js +72 -0
- package/dist/server/services/sandbox-utils.js.map +1 -0
- package/dist/server/services/settings.d.ts +6 -0
- package/dist/server/services/settings.d.ts.map +1 -1
- package/dist/server/services/settings.js +21 -0
- package/dist/server/services/settings.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts +5 -51
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +63 -102
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.d.ts +5 -0
- package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/file-explorer-handlers.js +518 -0
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-handlers.d.ts +36 -0
- package/dist/server/services/websocket/git-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-handlers.js +797 -0
- package/dist/server/services/websocket/git-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-pr-handlers.d.ts +4 -0
- package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-pr-handlers.js +299 -0
- package/dist/server/services/websocket/git-pr-handlers.js.map +1 -0
- package/dist/server/services/websocket/git-worktree-handlers.d.ts +4 -0
- package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/git-worktree-handlers.js +353 -0
- package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -0
- package/dist/server/services/websocket/handler-context.d.ts +32 -0
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -0
- package/dist/server/services/websocket/handler-context.js +4 -0
- package/dist/server/services/websocket/handler-context.js.map +1 -0
- package/dist/server/services/websocket/handler.d.ts +27 -338
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +74 -2106
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/index.d.ts +1 -1
- package/dist/server/services/websocket/index.d.ts.map +1 -1
- package/dist/server/services/websocket/index.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts +10 -0
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/session-handlers.js +507 -0
- package/dist/server/services/websocket/session-handlers.js.map +1 -0
- package/dist/server/services/websocket/settings-handlers.d.ts +6 -0
- package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/settings-handlers.js +125 -0
- package/dist/server/services/websocket/settings-handlers.js.map +1 -0
- package/dist/server/services/websocket/tab-handlers.d.ts +10 -0
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-handlers.js +131 -0
- package/dist/server/services/websocket/tab-handlers.js.map +1 -0
- package/dist/server/services/websocket/terminal-handlers.d.ts +9 -0
- package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/terminal-handlers.js +220 -0
- package/dist/server/services/websocket/terminal-handlers.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +67 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/hooks/bouncer.sh +11 -4
- package/package.json +7 -2
- package/server/README.md +176 -159
- package/server/cli/headless/claude-invoker.ts +740 -133
- package/server/cli/headless/index.ts +7 -1
- package/server/cli/headless/output-utils.test.ts +225 -0
- package/server/cli/headless/prompt-utils.ts +37 -5
- package/server/cli/headless/runner.ts +55 -8
- package/server/cli/headless/stall-assessor.test.ts +165 -0
- package/server/cli/headless/stall-assessor.ts +478 -22
- package/server/cli/headless/tool-watchdog.test.ts +429 -0
- package/server/cli/headless/tool-watchdog.ts +398 -0
- package/server/cli/headless/types.ts +93 -1
- package/server/cli/improvisation-session-manager.ts +1133 -145
- package/server/index.ts +5 -14
- package/server/mcp/README.md +59 -67
- package/server/mcp/bouncer-integration.test.ts +161 -0
- package/server/mcp/bouncer-integration.ts +28 -0
- package/server/mcp/security-audit.ts +12 -8
- package/server/mcp/security-patterns.test.ts +258 -0
- package/server/mcp/security-patterns.ts +8 -2
- package/server/routes/improvise.ts +6 -6
- package/server/services/analytics.ts +26 -4
- package/server/services/platform.test.ts +0 -10
- package/server/services/platform.ts +16 -11
- package/server/services/sandbox-utils.ts +78 -0
- package/server/services/settings.ts +25 -0
- package/server/services/terminal/pty-manager.ts +68 -129
- package/server/services/websocket/autocomplete.test.ts +194 -0
- package/server/services/websocket/file-explorer-handlers.ts +587 -0
- package/server/services/websocket/git-handlers.ts +924 -0
- package/server/services/websocket/git-pr-handlers.ts +363 -0
- package/server/services/websocket/git-worktree-handlers.ts +403 -0
- package/server/services/websocket/handler-context.ts +44 -0
- package/server/services/websocket/handler.test.ts +1 -1
- package/server/services/websocket/handler.ts +90 -2421
- package/server/services/websocket/index.ts +1 -1
- package/server/services/websocket/session-handlers.ts +574 -0
- package/server/services/websocket/settings-handlers.ts +150 -0
- package/server/services/websocket/tab-handlers.ts +150 -0
- package/server/services/websocket/terminal-handlers.ts +277 -0
- package/server/services/websocket/types.ts +145 -4
- package/bin/release.sh +0 -110
- package/dist/server/services/terminal/tmux-manager.d.ts +0 -82
- package/dist/server/services/terminal/tmux-manager.d.ts.map +0 -1
- package/dist/server/services/terminal/tmux-manager.js +0 -352
- package/dist/server/services/terminal/tmux-manager.js.map +0 -1
- package/server/services/terminal/tmux-manager.ts +0 -426
package/server/index.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { randomBytes } from 'node:crypto'
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
10
10
|
import type { IncomingMessage } from 'node:http'
|
|
11
|
+
import { homedir } from 'node:os'
|
|
11
12
|
import { basename, join } from 'node:path'
|
|
12
13
|
import { serve } from '@hono/node-server'
|
|
13
14
|
import { Hono } from 'hono'
|
|
@@ -213,7 +214,6 @@ app.route('/api/notifications', createNotificationRoutes(WORKING_DIR))
|
|
|
213
214
|
if (IS_PRODUCTION) {
|
|
214
215
|
// For production static file serving, use a reverse proxy like nginx
|
|
215
216
|
// or implement a simple static file middleware if needed
|
|
216
|
-
console.log('Production mode: serve static files via nginx or similar')
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
// ========================================
|
|
@@ -290,10 +290,6 @@ async function startServer() {
|
|
|
290
290
|
|
|
291
291
|
const PORT = await findAvailablePort(REQUESTED_PORT, 20)
|
|
292
292
|
|
|
293
|
-
if (PORT !== REQUESTED_PORT) {
|
|
294
|
-
console.log(`⚠️ Port ${REQUESTED_PORT} in use, using port ${PORT}`)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
293
|
_currentInstance = instanceRegistry.register(PORT, WORKING_DIR)
|
|
298
294
|
|
|
299
295
|
// Create HTTP server with Hono
|
|
@@ -342,10 +338,9 @@ async function startServer() {
|
|
|
342
338
|
})
|
|
343
339
|
})
|
|
344
340
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
console.log(`
|
|
348
|
-
console.log(`Framework: Hono`)
|
|
341
|
+
const home = homedir()
|
|
342
|
+
const displayDir = WORKING_DIR.startsWith(home) ? `~${WORKING_DIR.slice(home.length)}` : WORKING_DIR
|
|
343
|
+
console.log(`Machine: ${displayDir}`)
|
|
349
344
|
|
|
350
345
|
// Track server started event
|
|
351
346
|
trackEvent(AnalyticsEvents.SERVER_STARTED, {
|
|
@@ -364,7 +359,7 @@ async function startServer() {
|
|
|
364
359
|
// Connect to platform
|
|
365
360
|
const platformConnection = new PlatformConnection(WORKING_DIR, {
|
|
366
361
|
onConnected: (_connectionId) => {
|
|
367
|
-
console.log(
|
|
362
|
+
console.log(`Connected: https://mstro.app`)
|
|
368
363
|
|
|
369
364
|
// Set up usage reporter to send token usage to platform
|
|
370
365
|
wsHandler.setUsageReporter((report) => {
|
|
@@ -429,8 +424,6 @@ async function startServer() {
|
|
|
429
424
|
await Promise.all([shutdownAnalytics(), flushSentry()])
|
|
430
425
|
platformConnection.disconnect()
|
|
431
426
|
instanceRegistry.unregister()
|
|
432
|
-
// Close all non-persistent terminal sessions (PTY processes)
|
|
433
|
-
// Note: Persistent (tmux) sessions are intentionally left running
|
|
434
427
|
getPTYManager().closeAll()
|
|
435
428
|
wss.close()
|
|
436
429
|
console.log('\n\n👋 Shutting down gracefully...\n')
|
|
@@ -442,8 +435,6 @@ async function startServer() {
|
|
|
442
435
|
await Promise.all([shutdownAnalytics(), flushSentry()])
|
|
443
436
|
platformConnection.disconnect()
|
|
444
437
|
instanceRegistry.unregister()
|
|
445
|
-
// Close all non-persistent terminal sessions (PTY processes)
|
|
446
|
-
// Note: Persistent (tmux) sessions are intentionally left running
|
|
447
438
|
getPTYManager().closeAll()
|
|
448
439
|
wss.close()
|
|
449
440
|
console.log('\n\n👋 Shutting down gracefully...\n')
|
package/server/mcp/README.md
CHANGED
|
@@ -1,105 +1,97 @@
|
|
|
1
|
-
# Mstro MCP Bouncer
|
|
1
|
+
# Mstro MCP Bouncer
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
The MCP bouncer server provides permission approval/denial for Claude Code tool use via the MCP protocol. It integrates with Mstro's security analysis system to review potentially risky operations before they execute.
|
|
3
|
+
MCP (Model Context Protocol) server that provides tool approval decisions for Claude Code. Intercepts tool calls and applies a 2-layer security system to allow or deny operations.
|
|
8
4
|
|
|
9
5
|
## Architecture
|
|
10
6
|
|
|
11
|
-
|
|
7
|
+
### Layer 1: Pattern Matching (<5ms, ~95% of operations)
|
|
12
8
|
|
|
13
|
-
### Layer 1: Pattern-Based Fast Path (~95% of operations, <5ms)
|
|
14
9
|
- **Critical threats** → Immediate DENY (99% confidence)
|
|
15
10
|
- **Known-safe operations** → Immediate ALLOW (95% confidence)
|
|
16
|
-
-
|
|
11
|
+
- Regex-based pattern matching against consolidated threat/safe lists
|
|
12
|
+
|
|
13
|
+
### Layer 2: AI Analysis (~200-500ms, ~5% of operations)
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
- Uses Claude Code headless pattern (spawn + stdin)
|
|
22
|
-
- Variable confidence (50-90%)
|
|
15
|
+
- Spawns `claude --print --model haiku` for ambiguous cases
|
|
16
|
+
- Evaluates whether the operation looks like user-requested work or malicious injection
|
|
17
|
+
- Defaults to ALLOW — the assumption is the user is actively working with Claude
|
|
23
18
|
|
|
24
19
|
## Files
|
|
25
20
|
|
|
26
|
-
- **server.ts**
|
|
27
|
-
- **bouncer-integration.ts**
|
|
28
|
-
- **security-patterns.ts**
|
|
29
|
-
- **security-audit.ts**
|
|
21
|
+
- **server.ts** — MCP server entry point. Exposes single `approval_prompt` tool via stdio transport.
|
|
22
|
+
- **bouncer-integration.ts** — Core 2-layer security review logic. Orchestrates pattern check → AI analysis flow.
|
|
23
|
+
- **security-patterns.ts** — Pattern definitions: CRITICAL_THREATS, SAFE_OPERATIONS, NEEDS_AI_REVIEW, SENSITIVE_PATHS.
|
|
24
|
+
- **security-audit.ts** — Audit logging to `~/.mstro/logs/bouncer-audit.jsonl` (JSON Lines format).
|
|
25
|
+
- **bouncer-cli.ts** — Shell-callable wrapper invoked by `~/.claude/hooks/bouncer.sh`. Reads JSON from stdin, outputs decision to stdout.
|
|
30
26
|
|
|
31
27
|
## Usage
|
|
32
28
|
|
|
33
29
|
### Starting the Server
|
|
34
30
|
|
|
35
31
|
```bash
|
|
36
|
-
# From
|
|
32
|
+
# From cli/ directory
|
|
37
33
|
npm run dev:mcp
|
|
38
34
|
|
|
39
|
-
# Or directly
|
|
40
|
-
|
|
35
|
+
# Or directly
|
|
36
|
+
npx tsx server/mcp/server.ts
|
|
41
37
|
```
|
|
42
38
|
|
|
43
|
-
###
|
|
44
|
-
|
|
45
|
-
The server is configured via `mstro-bouncer-mcp.json` in the project root:
|
|
46
|
-
|
|
47
|
-
```json
|
|
48
|
-
{
|
|
49
|
-
"mcpServers": {
|
|
50
|
-
"mstro-bouncer": {
|
|
51
|
-
"command": "bun",
|
|
52
|
-
"args": ["run", "server/mcp/server.ts"],
|
|
53
|
-
"description": "Mstro security bouncer for approving/denying Claude Code tool use",
|
|
54
|
-
"env": {
|
|
55
|
-
"BOUNCER_USE_AI": "true"
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
```
|
|
39
|
+
### Integration with Claude Code
|
|
61
40
|
|
|
62
|
-
|
|
41
|
+
The bouncer integrates via Claude Code's `--permission-prompt-tool` flag:
|
|
63
42
|
|
|
64
43
|
```bash
|
|
65
|
-
claude --print
|
|
66
|
-
--mcp-config mstro-
|
|
44
|
+
claude --print \
|
|
45
|
+
--mcp-config ~/.mstro/mcp-config.json \
|
|
46
|
+
--permission-prompt-tool mcp__mstro-bouncer__approval_prompt \
|
|
67
47
|
"your prompt here"
|
|
68
48
|
```
|
|
69
49
|
|
|
50
|
+
The MCP config is auto-generated by the headless runner at `~/.mstro/mcp-config.json`. It includes the bouncer server plus any user-configured MCP servers from `~/.claude.json`.
|
|
51
|
+
|
|
52
|
+
### Hook Integration
|
|
53
|
+
|
|
54
|
+
When installed via `mstro configure-hooks`, a shell script at `~/.claude/hooks/bouncer.sh` calls `bouncer-cli.ts` as a PreToolUse hook. This provides security for both mstro sessions and standalone Claude Code usage.
|
|
55
|
+
|
|
70
56
|
## Environment Variables
|
|
71
57
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
58
|
+
| Variable | Default | Description |
|
|
59
|
+
|----------|---------|-------------|
|
|
60
|
+
| `BOUNCER_USE_AI` | `true` | Enable AI analysis layer. Set `false` for pattern-only checks. |
|
|
61
|
+
| `CLAUDE_COMMAND` | `claude` | Claude CLI command path |
|
|
75
62
|
|
|
76
63
|
## Security Patterns
|
|
77
64
|
|
|
78
65
|
### Critical Threats (Auto-deny)
|
|
79
|
-
|
|
66
|
+
|
|
67
|
+
- Root/home directory deletion (`rm -rf /` or `rm -rf ~`)
|
|
80
68
|
- Fork bombs
|
|
81
|
-
- Disk device overwrites
|
|
82
|
-
- Filesystem formatting
|
|
69
|
+
- Disk device overwrites (`dd if=/dev/zero of=/dev/sd*`)
|
|
70
|
+
- Filesystem formatting (`mkfs`)
|
|
83
71
|
- Obfuscated code execution
|
|
72
|
+
- System directory permission removal
|
|
84
73
|
|
|
85
74
|
### Safe Operations (Auto-allow)
|
|
75
|
+
|
|
86
76
|
- Read/Glob/Grep operations
|
|
87
|
-
-
|
|
88
|
-
- Git operations (
|
|
89
|
-
-
|
|
77
|
+
- Package manager commands (npm, yarn, pnpm, bun, cargo, go)
|
|
78
|
+
- Git operations (status, log, diff, branch, clone, pull, checkout)
|
|
79
|
+
- Docker operations
|
|
80
|
+
- File operations within home/tmp directories
|
|
81
|
+
- Safe artifact deletions (node_modules, dist, build, .cache)
|
|
90
82
|
|
|
91
83
|
### Requires AI Review
|
|
92
|
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
84
|
+
|
|
85
|
+
- Pipe-to-shell from remote sources (`curl | bash`)
|
|
86
|
+
- sudo operations
|
|
87
|
+
- `rm -rf` (except safe artifacts)
|
|
88
|
+
- Write/Edit operations (except /tmp)
|
|
89
|
+
- Variable expansion/globs in Bash
|
|
97
90
|
|
|
98
91
|
## Audit Logging
|
|
99
92
|
|
|
100
|
-
All
|
|
93
|
+
All decisions are logged to `~/.mstro/logs/bouncer-audit.jsonl`:
|
|
101
94
|
|
|
102
|
-
Example log entry:
|
|
103
95
|
```json
|
|
104
96
|
{
|
|
105
97
|
"timestamp": "2025-11-15T12:00:00.000Z",
|
|
@@ -107,16 +99,16 @@ Example log entry:
|
|
|
107
99
|
"decision": "allow",
|
|
108
100
|
"confidence": 95,
|
|
109
101
|
"reasoning": "Operation matches known-safe patterns",
|
|
110
|
-
"threatLevel": "low"
|
|
102
|
+
"threatLevel": "low",
|
|
103
|
+
"layer": "pattern-safe",
|
|
104
|
+
"latencyMs": 2
|
|
111
105
|
}
|
|
112
106
|
```
|
|
113
107
|
|
|
114
|
-
##
|
|
115
|
-
|
|
116
|
-
- 95%+ operations resolve in <5ms (Layer 1)
|
|
117
|
-
- 5% require AI analysis (~200-500ms)
|
|
118
|
-
- No ANTHROPIC_API_KEY required - uses existing Claude installation
|
|
119
|
-
|
|
120
|
-
## Integration
|
|
108
|
+
## Design Principles
|
|
121
109
|
|
|
122
|
-
|
|
110
|
+
- **Protect against injection, not dangerous commands** — operations are user-requested by default
|
|
111
|
+
- **Fast path first** — 95%+ decisions resolve in <5ms via pattern matching
|
|
112
|
+
- **No API key required** — uses existing `claude` CLI installation for AI layer
|
|
113
|
+
- **Fail-safe** — denies on analysis errors (conservative)
|
|
114
|
+
- **Graceful degradation** — falls back to pattern-only if AI unavailable
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { BouncerReviewRequest } from './bouncer-integration.js';
|
|
3
|
+
import { reviewOperation } from './bouncer-integration.js';
|
|
4
|
+
|
|
5
|
+
// ========== Internal function tests via reviewOperation fast paths ==========
|
|
6
|
+
// The parsing helpers (tryExtractFromWrapper, tryExtractJsonBlock, validateDecision,
|
|
7
|
+
// parseHaikuResponse) are not exported, so we test them indirectly through reviewOperation
|
|
8
|
+
// for pattern-based fast paths, and directly test the parsing logic below.
|
|
9
|
+
|
|
10
|
+
describe('reviewOperation - pattern fast paths', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Suppress console.error from bouncer logging
|
|
13
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('allows safe read operations immediately', async () => {
|
|
21
|
+
const result = await reviewOperation({ operation: 'Read: /home/user/file.ts' });
|
|
22
|
+
expect(result.decision).toBe('allow');
|
|
23
|
+
expect(result.confidence).toBe(95);
|
|
24
|
+
expect(result.threatLevel).toBe('low');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('allows safe bash commands immediately', async () => {
|
|
28
|
+
const result = await reviewOperation({ operation: 'Bash: npm test' });
|
|
29
|
+
expect(result.decision).toBe('allow');
|
|
30
|
+
expect(result.confidence).toBe(95);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('allows Glob operations immediately', async () => {
|
|
34
|
+
const result = await reviewOperation({ operation: 'Glob: **/*.ts' });
|
|
35
|
+
expect(result.decision).toBe('allow');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('allows Grep operations immediately', async () => {
|
|
39
|
+
const result = await reviewOperation({ operation: 'Grep: function' });
|
|
40
|
+
expect(result.decision).toBe('allow');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('allows safe rm of build artifacts', async () => {
|
|
44
|
+
const result = await reviewOperation({ operation: 'Bash: rm -rf node_modules' });
|
|
45
|
+
expect(result.decision).toBe('allow');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('denies critical threats with enforceable flag', async () => {
|
|
49
|
+
const result = await reviewOperation({ operation: 'rm -rf /' });
|
|
50
|
+
expect(result.decision).toBe('deny');
|
|
51
|
+
expect(result.confidence).toBe(99);
|
|
52
|
+
expect(result.threatLevel).toBe('critical');
|
|
53
|
+
expect(result.enforceable).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('denies fork bombs', async () => {
|
|
57
|
+
const result = await reviewOperation({ operation: ':(){ :|:& };:' });
|
|
58
|
+
expect(result.decision).toBe('deny');
|
|
59
|
+
expect(result.threatLevel).toBe('critical');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('denies disk overwrite attempts', async () => {
|
|
63
|
+
const result = await reviewOperation({ operation: 'dd if=/dev/zero of=/dev/sda' });
|
|
64
|
+
expect(result.decision).toBe('deny');
|
|
65
|
+
expect(result.threatLevel).toBe('critical');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('denies filesystem formatting', async () => {
|
|
69
|
+
const result = await reviewOperation({ operation: 'mkfs.ext4 /dev/sda1' });
|
|
70
|
+
expect(result.decision).toBe('deny');
|
|
71
|
+
expect(result.threatLevel).toBe('critical');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('denies obfuscated code execution', async () => {
|
|
75
|
+
const result = await reviewOperation({ operation: 'eval $(echo dGVzdA== | base64 -d)' });
|
|
76
|
+
expect(result.decision).toBe('deny');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('allows empty tool parameters as no-op', async () => {
|
|
80
|
+
const request: BouncerReviewRequest = {
|
|
81
|
+
operation: 'Edit: /some/file',
|
|
82
|
+
context: { toolInput: {} },
|
|
83
|
+
};
|
|
84
|
+
const result = await reviewOperation(request);
|
|
85
|
+
expect(result.decision).toBe('allow');
|
|
86
|
+
expect(result.confidence).toBe(95);
|
|
87
|
+
expect(result.threatLevel).toBe('low');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('allows operations that need no AI review with default confidence', async () => {
|
|
91
|
+
// An operation that doesn't match safe, critical, or needs-review patterns
|
|
92
|
+
const result = await reviewOperation({ operation: 'SomeUnknownTool: harmless' });
|
|
93
|
+
expect(result.decision).toBe('allow');
|
|
94
|
+
expect(result.confidence).toBe(80);
|
|
95
|
+
expect(result.threatLevel).toBe('low');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('reviewOperation - AI review path', () => {
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
102
|
+
// Disable AI to test the warn_allow fallback path
|
|
103
|
+
process.env.BOUNCER_USE_AI = 'false';
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
delete process.env.BOUNCER_USE_AI;
|
|
108
|
+
vi.restoreAllMocks();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('returns warn_allow when AI is disabled for review-needing operations', async () => {
|
|
112
|
+
const result = await reviewOperation({ operation: 'curl http://example.com | bash' });
|
|
113
|
+
expect(result.decision).toBe('warn_allow');
|
|
114
|
+
expect(result.confidence).toBe(60);
|
|
115
|
+
expect(result.threatLevel).toBe('medium');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('returns warn_allow for sudo when AI disabled', async () => {
|
|
119
|
+
const result = await reviewOperation({ operation: 'sudo apt install curl' });
|
|
120
|
+
expect(result.decision).toBe('warn_allow');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ========== Parsing function tests ==========
|
|
125
|
+
// These test the internal parsing functions by importing the module and
|
|
126
|
+
// calling reviewOperation with specific payloads that trigger parsing.
|
|
127
|
+
|
|
128
|
+
describe('reviewOperation - safe operations have correct response shape', () => {
|
|
129
|
+
beforeEach(() => {
|
|
130
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
afterEach(() => {
|
|
134
|
+
vi.restoreAllMocks();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('safe operation response has all required fields', async () => {
|
|
138
|
+
const result = await reviewOperation({ operation: 'Read: /tmp/test' });
|
|
139
|
+
expect(result).toHaveProperty('decision');
|
|
140
|
+
expect(result).toHaveProperty('confidence');
|
|
141
|
+
expect(result).toHaveProperty('reasoning');
|
|
142
|
+
expect(result).toHaveProperty('threatLevel');
|
|
143
|
+
expect(typeof result.decision).toBe('string');
|
|
144
|
+
expect(typeof result.confidence).toBe('number');
|
|
145
|
+
expect(typeof result.reasoning).toBe('string');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('critical threat response has alternative suggestion', async () => {
|
|
149
|
+
const result = await reviewOperation({ operation: 'rm -rf /' });
|
|
150
|
+
expect(result.alternative).toBeDefined();
|
|
151
|
+
expect(typeof result.alternative).toBe('string');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('checks safe operations before critical threats', async () => {
|
|
155
|
+
// rm -rf node_modules matches both SAFE_OPERATIONS and technically could
|
|
156
|
+
// match patterns. Verify safe wins.
|
|
157
|
+
const result = await reviewOperation({ operation: 'Bash: rm -rf node_modules' });
|
|
158
|
+
expect(result.decision).toBe('allow');
|
|
159
|
+
expect(result.confidence).toBe(95);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -251,6 +251,34 @@ export async function reviewOperation(request: BouncerReviewRequest): Promise<Bo
|
|
|
251
251
|
console.error(`[Bouncer] User request: ${request.context.userRequest}`);
|
|
252
252
|
}
|
|
253
253
|
|
|
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.
|
|
259
|
+
const toolInput = request.context?.toolInput;
|
|
260
|
+
if (toolInput && typeof toolInput === 'object' && Object.keys(toolInput).length === 0) {
|
|
261
|
+
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;
|
|
280
|
+
}
|
|
281
|
+
|
|
254
282
|
// ========================================
|
|
255
283
|
// LAYER 1: Pattern-Based Fast Path (< 5ms)
|
|
256
284
|
// ========================================
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
import { appendFileSync, existsSync, mkdirSync, } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
|
|
13
|
-
// Default log
|
|
14
|
-
const
|
|
13
|
+
// Default log subdirectory inside .mstro/
|
|
14
|
+
const DEFAULT_LOG_SUBDIR = '.mstro/logs';
|
|
15
15
|
|
|
16
16
|
export type BouncerLayer = 'pattern-critical' | 'pattern-safe' | 'pattern-default' | 'haiku-ai' | 'ai-disabled' | 'ai-error';
|
|
17
17
|
|
|
@@ -33,7 +33,8 @@ export interface AuditLogEntry {
|
|
|
33
33
|
export class SecurityAuditLogger {
|
|
34
34
|
private logFile: string;
|
|
35
35
|
|
|
36
|
-
constructor(
|
|
36
|
+
constructor(workingDir?: string) {
|
|
37
|
+
const logDir = join(workingDir || process.cwd(), DEFAULT_LOG_SUBDIR);
|
|
37
38
|
this.logFile = join(logDir, 'bouncer-audit.jsonl');
|
|
38
39
|
|
|
39
40
|
// Ensure log directory exists
|
|
@@ -88,12 +89,14 @@ export class SecurityAuditLogger {
|
|
|
88
89
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
// Singleton instance
|
|
92
|
+
// Singleton instance (keyed by workingDir to support multiple projects)
|
|
92
93
|
let auditLogger: SecurityAuditLogger | null = null;
|
|
94
|
+
let auditLoggerWorkingDir: string | undefined;
|
|
93
95
|
|
|
94
|
-
export function getAuditLogger(): SecurityAuditLogger {
|
|
95
|
-
if (!auditLogger) {
|
|
96
|
-
auditLogger = new SecurityAuditLogger();
|
|
96
|
+
export function getAuditLogger(workingDir?: string): SecurityAuditLogger {
|
|
97
|
+
if (!auditLogger || (workingDir && workingDir !== auditLoggerWorkingDir)) {
|
|
98
|
+
auditLogger = new SecurityAuditLogger(workingDir);
|
|
99
|
+
auditLoggerWorkingDir = workingDir;
|
|
97
100
|
}
|
|
98
101
|
return auditLogger;
|
|
99
102
|
}
|
|
@@ -113,7 +116,8 @@ export function logBouncerDecision(
|
|
|
113
116
|
const validDecisions = ['allow', 'deny', 'warn_allow'];
|
|
114
117
|
const normalizedDecision = validDecisions.includes(safeDecision) ? safeDecision : 'deny';
|
|
115
118
|
|
|
116
|
-
const
|
|
119
|
+
const workingDir = metadata?.context?.workingDirectory;
|
|
120
|
+
const logger = getAuditLogger(workingDir);
|
|
117
121
|
logger.logDecision(operation, normalizedDecision as 'allow' | 'deny' | 'warn_allow', confidence, reasoning, metadata);
|
|
118
122
|
|
|
119
123
|
// Also log to console for real-time monitoring
|