claude-mpm 3.1.1__py3-none-any.whl → 3.1.3__py3-none-any.whl

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 (47) hide show
  1. claude_mpm/__main__.py +27 -3
  2. claude_mpm/agents/INSTRUCTIONS.md +17 -2
  3. claude_mpm/agents/templates/test-integration-agent.md +34 -0
  4. claude_mpm/cli/README.md +109 -0
  5. claude_mpm/cli/__init__.py +172 -0
  6. claude_mpm/cli/commands/__init__.py +20 -0
  7. claude_mpm/cli/commands/agents.py +202 -0
  8. claude_mpm/cli/commands/info.py +94 -0
  9. claude_mpm/cli/commands/run.py +95 -0
  10. claude_mpm/cli/commands/tickets.py +70 -0
  11. claude_mpm/cli/commands/ui.py +79 -0
  12. claude_mpm/cli/parser.py +337 -0
  13. claude_mpm/cli/utils.py +190 -0
  14. claude_mpm/cli_enhancements.py +19 -0
  15. claude_mpm/core/agent_registry.py +4 -4
  16. claude_mpm/core/factories.py +1 -1
  17. claude_mpm/core/service_registry.py +1 -1
  18. claude_mpm/core/simple_runner.py +17 -27
  19. claude_mpm/hooks/claude_hooks/hook_handler.py +53 -4
  20. claude_mpm/models/__init__.py +106 -0
  21. claude_mpm/models/agent_definition.py +196 -0
  22. claude_mpm/models/common.py +41 -0
  23. claude_mpm/models/lifecycle.py +97 -0
  24. claude_mpm/models/modification.py +126 -0
  25. claude_mpm/models/persistence.py +57 -0
  26. claude_mpm/models/registry.py +91 -0
  27. claude_mpm/security/__init__.py +8 -0
  28. claude_mpm/security/bash_validator.py +393 -0
  29. claude_mpm/services/agent_lifecycle_manager.py +206 -94
  30. claude_mpm/services/agent_modification_tracker.py +27 -100
  31. claude_mpm/services/agent_persistence_service.py +74 -0
  32. claude_mpm/services/agent_registry.py +43 -82
  33. claude_mpm/services/agent_versioning.py +37 -0
  34. claude_mpm/services/{ticketing_service_original.py → legacy_ticketing_service.py} +16 -9
  35. claude_mpm/services/ticket_manager.py +5 -4
  36. claude_mpm/services/{ticket_manager_di.py → ticket_manager_dependency_injection.py} +39 -12
  37. claude_mpm/services/version_control/semantic_versioning.py +10 -9
  38. claude_mpm/utils/path_operations.py +20 -0
  39. {claude_mpm-3.1.1.dist-info → claude_mpm-3.1.3.dist-info}/METADATA +9 -1
  40. {claude_mpm-3.1.1.dist-info → claude_mpm-3.1.3.dist-info}/RECORD +45 -25
  41. claude_mpm/cli_main.py +0 -13
  42. claude_mpm/utils/import_migration_example.py +0 -80
  43. /claude_mpm/{cli.py → cli_old.py} +0 -0
  44. {claude_mpm-3.1.1.dist-info → claude_mpm-3.1.3.dist-info}/WHEEL +0 -0
  45. {claude_mpm-3.1.1.dist-info → claude_mpm-3.1.3.dist-info}/entry_points.txt +0 -0
  46. {claude_mpm-3.1.1.dist-info → claude_mpm-3.1.3.dist-info}/licenses/LICENSE +0 -0
  47. {claude_mpm-3.1.1.dist-info → claude_mpm-3.1.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,393 @@
1
+ """Bash command security validator for file access restrictions.
2
+
3
+ This module provides comprehensive validation for bash commands to ensure
4
+ agents cannot perform file operations outside their working directory.
5
+
6
+ Security patterns blocked:
7
+ - File writes via redirects (>, >>)
8
+ - File writes via commands (echo/cat > file, cp/mv to external paths)
9
+ - Directory operations outside working directory (mkdir, rmdir)
10
+ - Dangerous command patterns (rm -rf, sudo, etc)
11
+ """
12
+
13
+ import re
14
+ import shlex
15
+ from pathlib import Path
16
+ from typing import List, Tuple, Optional, Dict, Set
17
+ import logging
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class BashSecurityValidator:
23
+ """Validates bash commands for security violations.
24
+
25
+ WHY: We need to prevent agents from writing outside their working directory
26
+ through bash commands, which bypass the normal tool validation. This class
27
+ parses bash commands and identifies file operations that would violate
28
+ security boundaries.
29
+
30
+ DESIGN DECISION: We parse commands rather than execute in a sandbox because:
31
+ - Faster validation without subprocess overhead
32
+ - Can provide detailed error messages about violations
33
+ - Prevents any execution of potentially harmful commands
34
+ """
35
+
36
+ # Commands that can write/modify files
37
+ WRITE_COMMANDS = {
38
+ 'echo', 'cat', 'printf', 'tee', 'sed', 'awk', 'perl', 'python',
39
+ 'python3', 'node', 'ruby', 'sh', 'bash', 'zsh', 'fish'
40
+ }
41
+
42
+ # Commands that copy/move files
43
+ FILE_TRANSFER_COMMANDS = {
44
+ 'cp', 'mv', 'scp', 'rsync', 'install'
45
+ }
46
+
47
+ # Commands that create/modify directories
48
+ DIR_COMMANDS = {
49
+ 'mkdir', 'rmdir', 'rm'
50
+ }
51
+
52
+ # Commands that are always dangerous
53
+ DANGEROUS_COMMANDS = {
54
+ 'sudo', 'su', 'chmod', 'chown', 'chgrp', 'mkfs', 'dd',
55
+ 'format', 'fdisk', 'mount', 'umount'
56
+ }
57
+
58
+ # Redirect operators that can write files
59
+ REDIRECT_OPERATORS = {'>', '>>', '>&', '&>', '>|'}
60
+
61
+ def __init__(self, working_dir: Path):
62
+ """Initialize validator with working directory.
63
+
64
+ Args:
65
+ working_dir: The directory agents are restricted to
66
+ """
67
+ self.working_dir = working_dir.resolve()
68
+
69
+ def validate_command(self, command: str) -> Tuple[bool, Optional[str]]:
70
+ """Validate a bash command for security violations.
71
+
72
+ Args:
73
+ command: The bash command to validate
74
+
75
+ Returns:
76
+ Tuple of (is_valid, error_message)
77
+ - is_valid: True if command is safe, False if it violates security
78
+ - error_message: Detailed error message if validation fails
79
+ """
80
+ try:
81
+ # Check for dangerous commands first
82
+ danger_check = self._check_dangerous_commands(command)
83
+ if danger_check:
84
+ return False, danger_check
85
+
86
+ # Check for file redirects
87
+ redirect_check = self._check_redirects(command)
88
+ if redirect_check:
89
+ return False, redirect_check
90
+
91
+ # Check for file operations in commands
92
+ file_op_check = self._check_file_operations(command)
93
+ if file_op_check:
94
+ return False, file_op_check
95
+
96
+ # Check for directory operations
97
+ dir_op_check = self._check_directory_operations(command)
98
+ if dir_op_check:
99
+ return False, dir_op_check
100
+
101
+ # Check for pipe operations that could write files
102
+ pipe_check = self._check_pipe_operations(command)
103
+ if pipe_check:
104
+ return False, pipe_check
105
+
106
+ return True, None
107
+
108
+ except Exception as e:
109
+ logger.error(f"Error validating bash command: {e}")
110
+ # On error, be conservative and block
111
+ return False, f"Command validation error: {str(e)}"
112
+
113
+ def _check_dangerous_commands(self, command: str) -> Optional[str]:
114
+ """Check for inherently dangerous commands.
115
+
116
+ Args:
117
+ command: The command to check
118
+
119
+ Returns:
120
+ Error message if dangerous command found, None otherwise
121
+ """
122
+ # Split by common separators to handle command chains
123
+ parts = re.split(r'[;&|]', command)
124
+
125
+ for part in parts:
126
+ tokens = part.strip().split()
127
+ if not tokens:
128
+ continue
129
+
130
+ cmd = tokens[0]
131
+
132
+ # Check absolute dangerous commands
133
+ if cmd in self.DANGEROUS_COMMANDS:
134
+ return (f"Security Policy: Command '{cmd}' is not allowed.\n"
135
+ f"This command could compromise system security.")
136
+
137
+ # Check for sudo/su with any command
138
+ if cmd in ['sudo', 'su'] and len(tokens) > 1:
139
+ return (f"Security Policy: Privileged command execution is not allowed.\n"
140
+ f"Command '{cmd}' cannot be used to escalate privileges.")
141
+
142
+ # Check for rm -rf patterns
143
+ if cmd == 'rm' and any(arg in tokens for arg in ['-rf', '-fr', '--force']):
144
+ # Check if targeting root or system directories
145
+ for token in tokens[1:]:
146
+ if token.startswith('/') and not token.startswith(str(self.working_dir)):
147
+ return (f"Security Policy: Dangerous rm command detected.\n"
148
+ f"Cannot remove files outside working directory.")
149
+
150
+ return None
151
+
152
+ def _check_redirects(self, command: str) -> Optional[str]:
153
+ """Check for file redirects that write outside working directory.
154
+
155
+ Args:
156
+ command: The command to check
157
+
158
+ Returns:
159
+ Error message if unsafe redirect found, None otherwise
160
+ """
161
+ # Pattern to find redirects: > or >> followed by a file path
162
+ # Handles cases like: echo "test" > /etc/passwd
163
+ redirect_pattern = r'(' + '|'.join(re.escape(op) for op in self.REDIRECT_OPERATORS) + r')\s*([\'"]?)([^\s\'"]+)\2'
164
+
165
+ for match in re.finditer(redirect_pattern, command):
166
+ operator = match.group(1)
167
+ file_path = match.group(3)
168
+
169
+ # Skip descriptors like >&2
170
+ if file_path.isdigit():
171
+ continue
172
+
173
+ # Validate the target path
174
+ validation = self._validate_path(file_path)
175
+ if not validation[0]:
176
+ return (f"Security Policy: File redirect outside working directory not allowed.\n"
177
+ f"Redirect operator '{operator}' targeting: {file_path}\n"
178
+ f"{validation[1]}")
179
+
180
+ # Also check for here-documents that might write files
181
+ if '<<' in command:
182
+ # Check if it's followed by a file write
183
+ heredoc_write = re.search(r'<<.*?\|.*?>\s*([^\s]+)', command)
184
+ if heredoc_write:
185
+ file_path = heredoc_write.group(1)
186
+ validation = self._validate_path(file_path)
187
+ if not validation[0]:
188
+ return (f"Security Policy: Here-document redirect outside working directory.\n"
189
+ f"Target file: {file_path}")
190
+
191
+ return None
192
+
193
+ def _check_file_operations(self, command: str) -> Optional[str]:
194
+ """Check for file operations that write outside working directory.
195
+
196
+ Args:
197
+ command: The command to check
198
+
199
+ Returns:
200
+ Error message if unsafe file operation found, None otherwise
201
+ """
202
+ try:
203
+ # Parse command to handle quotes properly
204
+ tokens = shlex.split(command)
205
+ except ValueError:
206
+ # If shlex fails, fall back to simple split
207
+ tokens = command.split()
208
+
209
+ i = 0
210
+ while i < len(tokens):
211
+ token = tokens[i]
212
+
213
+ # Check copy/move commands
214
+ if token in self.FILE_TRANSFER_COMMANDS:
215
+ # These commands typically have source and destination
216
+ # We need to check the destination
217
+ dest_idx = None
218
+
219
+ if token in ['cp', 'mv', 'install']:
220
+ # Skip options
221
+ j = i + 1
222
+ while j < len(tokens) and tokens[j].startswith('-'):
223
+ j += 1
224
+ # Last argument is typically destination
225
+ if j < len(tokens):
226
+ dest_idx = len(tokens) - 1
227
+
228
+ elif token == 'rsync':
229
+ # rsync can have complex syntax, look for paths
230
+ for j in range(i + 1, len(tokens)):
231
+ if not tokens[j].startswith('-') and ':' not in tokens[j]:
232
+ # Potential local path
233
+ validation = self._validate_path(tokens[j])
234
+ if not validation[0]:
235
+ return (f"Security Policy: {token} operation outside working directory.\n"
236
+ f"Target path: {tokens[j]}\n{validation[1]}")
237
+
238
+ if dest_idx and dest_idx < len(tokens):
239
+ dest_path = tokens[dest_idx]
240
+ validation = self._validate_path(dest_path)
241
+ if not validation[0]:
242
+ return (f"Security Policy: {token} destination outside working directory.\n"
243
+ f"Destination: {dest_path}\n{validation[1]}")
244
+
245
+ # Check for write operations via command substitution
246
+ if token in self.WRITE_COMMANDS and i + 1 < len(tokens):
247
+ # Look for patterns like: echo "data" > file
248
+ for j in range(i + 1, len(tokens)):
249
+ if tokens[j] in self.REDIRECT_OPERATORS and j + 1 < len(tokens):
250
+ file_path = tokens[j + 1]
251
+ validation = self._validate_path(file_path)
252
+ if not validation[0]:
253
+ return (f"Security Policy: {token} write outside working directory.\n"
254
+ f"Target file: {file_path}\n{validation[1]}")
255
+
256
+ i += 1
257
+
258
+ return None
259
+
260
+ def _check_directory_operations(self, command: str) -> Optional[str]:
261
+ """Check for directory operations outside working directory.
262
+
263
+ Args:
264
+ command: The command to check
265
+
266
+ Returns:
267
+ Error message if unsafe directory operation found, None otherwise
268
+ """
269
+ try:
270
+ tokens = shlex.split(command)
271
+ except ValueError:
272
+ tokens = command.split()
273
+
274
+ i = 0
275
+ while i < len(tokens):
276
+ token = tokens[i]
277
+
278
+ if token in self.DIR_COMMANDS:
279
+ # Check all arguments after the command
280
+ for j in range(i + 1, len(tokens)):
281
+ arg = tokens[j]
282
+ # Skip options
283
+ if arg.startswith('-'):
284
+ continue
285
+
286
+ # Validate the path
287
+ validation = self._validate_path(arg)
288
+ if not validation[0]:
289
+ return (f"Security Policy: {token} operation outside working directory.\n"
290
+ f"Target path: {arg}\n{validation[1]}")
291
+
292
+ i += 1
293
+
294
+ return None
295
+
296
+ def _check_pipe_operations(self, command: str) -> Optional[str]:
297
+ """Check for pipe operations that could write files.
298
+
299
+ Args:
300
+ command: The command to check
301
+
302
+ Returns:
303
+ Error message if unsafe pipe operation found, None otherwise
304
+ """
305
+ # Check for tee command which can write to files
306
+ if 'tee' in command:
307
+ tee_pattern = r'tee\s+(?:-[a-zA-Z]+\s+)*([^\s|]+)'
308
+ for match in re.finditer(tee_pattern, command):
309
+ file_path = match.group(1)
310
+ validation = self._validate_path(file_path)
311
+ if not validation[0]:
312
+ return (f"Security Policy: tee write outside working directory.\n"
313
+ f"Target file: {file_path}\n{validation[1]}")
314
+
315
+ # Check for dd command which can write to files/devices
316
+ if 'dd' in command:
317
+ dd_pattern = r'of=([^\s]+)'
318
+ match = re.search(dd_pattern, command)
319
+ if match:
320
+ file_path = match.group(1)
321
+ validation = self._validate_path(file_path)
322
+ if not validation[0]:
323
+ return (f"Security Policy: dd write outside working directory.\n"
324
+ f"Target file: {file_path}\n{validation[1]}")
325
+
326
+ return None
327
+
328
+ def _validate_path(self, path_str: str) -> Tuple[bool, str]:
329
+ """Validate a path is within working directory.
330
+
331
+ Args:
332
+ path_str: The path string to validate
333
+
334
+ Returns:
335
+ Tuple of (is_valid, message)
336
+ """
337
+ # Handle empty or special paths
338
+ if not path_str or path_str in ['-', '/dev/null', '/dev/stdout', '/dev/stderr']:
339
+ return True, ""
340
+
341
+ # Remove quotes if present
342
+ path_str = path_str.strip('\'"')
343
+
344
+ # Check for environment variables that typically point outside working directory
345
+ # Common patterns: $HOME, ${HOME}, $USER, ${USER}, etc.
346
+ env_patterns = [
347
+ r'\$HOME', r'\${HOME}', r'~/',
348
+ r'\$USER', r'\${USER}',
349
+ r'\$TMPDIR', r'\${TMPDIR}',
350
+ r'/tmp/', r'/var/', r'/etc/', r'/usr/', r'/opt/',
351
+ r'/System/', r'/Library/', r'/Applications/', # macOS
352
+ r'C:\\', r'D:\\', # Windows
353
+ ]
354
+
355
+ for pattern in env_patterns:
356
+ if re.search(pattern, path_str, re.IGNORECASE):
357
+ return False, (f"Security Policy: Path '{path_str}' contains environment variable "
358
+ f"or system path that likely points outside working directory.\n"
359
+ f"Please use relative paths or absolute paths within '{self.working_dir}'")
360
+
361
+ try:
362
+ # Convert to Path object
363
+ if path_str.startswith('/'):
364
+ # Absolute path
365
+ path = Path(path_str).resolve()
366
+ else:
367
+ # Relative path - resolve relative to working directory
368
+ path = (self.working_dir / path_str).resolve()
369
+
370
+ # Check if path is within working directory
371
+ try:
372
+ path.relative_to(self.working_dir)
373
+ return True, ""
374
+ except ValueError:
375
+ # Path is outside working directory
376
+ return False, (f"Path '{path_str}' resolves to '{path}' which is outside "
377
+ f"the working directory '{self.working_dir}'")
378
+
379
+ except Exception as e:
380
+ # If we can't resolve the path, be conservative and block
381
+ return False, f"Cannot validate path '{path_str}': {str(e)}"
382
+
383
+
384
+ def create_validator(working_dir: Path) -> BashSecurityValidator:
385
+ """Factory function to create a bash security validator.
386
+
387
+ Args:
388
+ working_dir: The working directory to restrict operations to
389
+
390
+ Returns:
391
+ Configured BashSecurityValidator instance
392
+ """
393
+ return BashSecurityValidator(working_dir)