claude-mpm 0.3.0__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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (159) hide show
  1. claude_mpm/__init__.py +17 -0
  2. claude_mpm/__main__.py +14 -0
  3. claude_mpm/_version.py +32 -0
  4. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +88 -0
  5. claude_mpm/agents/INSTRUCTIONS.md +375 -0
  6. claude_mpm/agents/__init__.py +118 -0
  7. claude_mpm/agents/agent_loader.py +621 -0
  8. claude_mpm/agents/agent_loader_integration.py +229 -0
  9. claude_mpm/agents/agents_metadata.py +204 -0
  10. claude_mpm/agents/base_agent.json +27 -0
  11. claude_mpm/agents/base_agent_loader.py +519 -0
  12. claude_mpm/agents/schema/agent_schema.json +160 -0
  13. claude_mpm/agents/system_agent_config.py +587 -0
  14. claude_mpm/agents/templates/__init__.py +101 -0
  15. claude_mpm/agents/templates/data_engineer_agent.json +46 -0
  16. claude_mpm/agents/templates/documentation_agent.json +45 -0
  17. claude_mpm/agents/templates/engineer_agent.json +49 -0
  18. claude_mpm/agents/templates/ops_agent.json +46 -0
  19. claude_mpm/agents/templates/qa_agent.json +45 -0
  20. claude_mpm/agents/templates/research_agent.json +49 -0
  21. claude_mpm/agents/templates/security_agent.json +46 -0
  22. claude_mpm/agents/templates/update-optimized-specialized-agents.json +374 -0
  23. claude_mpm/agents/templates/version_control_agent.json +46 -0
  24. claude_mpm/agents/test_fix_deployment/.claude-pm/config/project.json +6 -0
  25. claude_mpm/cli.py +655 -0
  26. claude_mpm/cli_main.py +13 -0
  27. claude_mpm/cli_module/__init__.py +15 -0
  28. claude_mpm/cli_module/args.py +222 -0
  29. claude_mpm/cli_module/commands.py +203 -0
  30. claude_mpm/cli_module/migration_example.py +183 -0
  31. claude_mpm/cli_module/refactoring_guide.md +253 -0
  32. claude_mpm/cli_old/__init__.py +1 -0
  33. claude_mpm/cli_old/ticket_cli.py +102 -0
  34. claude_mpm/config/__init__.py +5 -0
  35. claude_mpm/config/hook_config.py +42 -0
  36. claude_mpm/constants.py +150 -0
  37. claude_mpm/core/__init__.py +45 -0
  38. claude_mpm/core/agent_name_normalizer.py +248 -0
  39. claude_mpm/core/agent_registry.py +627 -0
  40. claude_mpm/core/agent_registry.py.bak +312 -0
  41. claude_mpm/core/agent_session_manager.py +273 -0
  42. claude_mpm/core/base_service.py +747 -0
  43. claude_mpm/core/base_service.py.bak +406 -0
  44. claude_mpm/core/config.py +334 -0
  45. claude_mpm/core/config_aliases.py +292 -0
  46. claude_mpm/core/container.py +347 -0
  47. claude_mpm/core/factories.py +281 -0
  48. claude_mpm/core/framework_loader.py +472 -0
  49. claude_mpm/core/injectable_service.py +206 -0
  50. claude_mpm/core/interfaces.py +539 -0
  51. claude_mpm/core/logger.py +468 -0
  52. claude_mpm/core/minimal_framework_loader.py +107 -0
  53. claude_mpm/core/mixins.py +150 -0
  54. claude_mpm/core/service_registry.py +299 -0
  55. claude_mpm/core/session_manager.py +190 -0
  56. claude_mpm/core/simple_runner.py +511 -0
  57. claude_mpm/core/tool_access_control.py +173 -0
  58. claude_mpm/hooks/README.md +243 -0
  59. claude_mpm/hooks/__init__.py +5 -0
  60. claude_mpm/hooks/base_hook.py +154 -0
  61. claude_mpm/hooks/builtin/__init__.py +1 -0
  62. claude_mpm/hooks/builtin/logging_hook_example.py +165 -0
  63. claude_mpm/hooks/builtin/post_delegation_hook_example.py +124 -0
  64. claude_mpm/hooks/builtin/pre_delegation_hook_example.py +125 -0
  65. claude_mpm/hooks/builtin/submit_hook_example.py +100 -0
  66. claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +237 -0
  67. claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +239 -0
  68. claude_mpm/hooks/builtin/workflow_start_hook.py +181 -0
  69. claude_mpm/hooks/hook_client.py +264 -0
  70. claude_mpm/hooks/hook_runner.py +370 -0
  71. claude_mpm/hooks/json_rpc_executor.py +259 -0
  72. claude_mpm/hooks/json_rpc_hook_client.py +319 -0
  73. claude_mpm/hooks/tool_call_interceptor.py +204 -0
  74. claude_mpm/init.py +246 -0
  75. claude_mpm/orchestration/SUBPROCESS_DESIGN.md +66 -0
  76. claude_mpm/orchestration/__init__.py +6 -0
  77. claude_mpm/orchestration/archive/direct_orchestrator.py +195 -0
  78. claude_mpm/orchestration/archive/factory.py +215 -0
  79. claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +188 -0
  80. claude_mpm/orchestration/archive/hook_integration_example.py +178 -0
  81. claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +826 -0
  82. claude_mpm/orchestration/archive/orchestrator.py +501 -0
  83. claude_mpm/orchestration/archive/pexpect_orchestrator.py +252 -0
  84. claude_mpm/orchestration/archive/pty_orchestrator.py +270 -0
  85. claude_mpm/orchestration/archive/simple_orchestrator.py +82 -0
  86. claude_mpm/orchestration/archive/subprocess_orchestrator.py +801 -0
  87. claude_mpm/orchestration/archive/system_prompt_orchestrator.py +278 -0
  88. claude_mpm/orchestration/archive/wrapper_orchestrator.py +187 -0
  89. claude_mpm/scripts/__init__.py +1 -0
  90. claude_mpm/scripts/ticket.py +269 -0
  91. claude_mpm/services/__init__.py +10 -0
  92. claude_mpm/services/agent_deployment.py +955 -0
  93. claude_mpm/services/agent_lifecycle_manager.py +948 -0
  94. claude_mpm/services/agent_management_service.py +596 -0
  95. claude_mpm/services/agent_modification_tracker.py +841 -0
  96. claude_mpm/services/agent_profile_loader.py +606 -0
  97. claude_mpm/services/agent_registry.py +677 -0
  98. claude_mpm/services/base_agent_manager.py +380 -0
  99. claude_mpm/services/framework_agent_loader.py +337 -0
  100. claude_mpm/services/framework_claude_md_generator/README.md +92 -0
  101. claude_mpm/services/framework_claude_md_generator/__init__.py +206 -0
  102. claude_mpm/services/framework_claude_md_generator/content_assembler.py +151 -0
  103. claude_mpm/services/framework_claude_md_generator/content_validator.py +126 -0
  104. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +137 -0
  105. claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +106 -0
  106. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +582 -0
  107. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +97 -0
  108. claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +27 -0
  109. claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +23 -0
  110. claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +23 -0
  111. claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +20 -0
  112. claude_mpm/services/framework_claude_md_generator/section_generators/header.py +26 -0
  113. claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +30 -0
  114. claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +37 -0
  115. claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +111 -0
  116. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +89 -0
  117. claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +39 -0
  118. claude_mpm/services/framework_claude_md_generator/section_manager.py +106 -0
  119. claude_mpm/services/framework_claude_md_generator/version_manager.py +121 -0
  120. claude_mpm/services/framework_claude_md_generator.py +621 -0
  121. claude_mpm/services/hook_service.py +388 -0
  122. claude_mpm/services/hook_service_manager.py +223 -0
  123. claude_mpm/services/json_rpc_hook_manager.py +92 -0
  124. claude_mpm/services/parent_directory_manager/README.md +83 -0
  125. claude_mpm/services/parent_directory_manager/__init__.py +577 -0
  126. claude_mpm/services/parent_directory_manager/backup_manager.py +258 -0
  127. claude_mpm/services/parent_directory_manager/config_manager.py +210 -0
  128. claude_mpm/services/parent_directory_manager/deduplication_manager.py +279 -0
  129. claude_mpm/services/parent_directory_manager/framework_protector.py +143 -0
  130. claude_mpm/services/parent_directory_manager/operations.py +186 -0
  131. claude_mpm/services/parent_directory_manager/state_manager.py +624 -0
  132. claude_mpm/services/parent_directory_manager/template_deployer.py +579 -0
  133. claude_mpm/services/parent_directory_manager/validation_manager.py +378 -0
  134. claude_mpm/services/parent_directory_manager/version_control_helper.py +339 -0
  135. claude_mpm/services/parent_directory_manager/version_manager.py +222 -0
  136. claude_mpm/services/shared_prompt_cache.py +819 -0
  137. claude_mpm/services/ticket_manager.py +213 -0
  138. claude_mpm/services/ticket_manager_di.py +318 -0
  139. claude_mpm/services/ticketing_service_original.py +508 -0
  140. claude_mpm/services/version_control/VERSION +1 -0
  141. claude_mpm/services/version_control/__init__.py +70 -0
  142. claude_mpm/services/version_control/branch_strategy.py +670 -0
  143. claude_mpm/services/version_control/conflict_resolution.py +744 -0
  144. claude_mpm/services/version_control/git_operations.py +784 -0
  145. claude_mpm/services/version_control/semantic_versioning.py +703 -0
  146. claude_mpm/ui/__init__.py +1 -0
  147. claude_mpm/ui/rich_terminal_ui.py +295 -0
  148. claude_mpm/ui/terminal_ui.py +328 -0
  149. claude_mpm/utils/__init__.py +16 -0
  150. claude_mpm/utils/config_manager.py +468 -0
  151. claude_mpm/utils/import_migration_example.py +80 -0
  152. claude_mpm/utils/imports.py +182 -0
  153. claude_mpm/utils/path_operations.py +357 -0
  154. claude_mpm/utils/paths.py +289 -0
  155. claude_mpm-0.3.0.dist-info/METADATA +290 -0
  156. claude_mpm-0.3.0.dist-info/RECORD +159 -0
  157. claude_mpm-0.3.0.dist-info/WHEEL +5 -0
  158. claude_mpm-0.3.0.dist-info/entry_points.txt +4 -0
  159. claude_mpm-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,319 @@
1
+ """JSON-RPC based client for executing hooks without HTTP server."""
2
+
3
+ import importlib.util
4
+ import inspect
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from claude_mpm.hooks.base_hook import (
10
+ BaseHook, HookType,
11
+ SubmitHook, PreDelegationHook, PostDelegationHook, TicketExtractionHook
12
+ )
13
+ from claude_mpm.hooks.json_rpc_executor import JSONRPCHookExecutor, JSONRPCError
14
+ from claude_mpm.core.logger import get_logger
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ class JSONRPCHookClient:
20
+ """Client for executing hooks via JSON-RPC subprocess calls."""
21
+
22
+ def __init__(self, timeout: int = 30):
23
+ """Initialize JSON-RPC hook client.
24
+
25
+ Args:
26
+ timeout: Request timeout in seconds
27
+ """
28
+ self.timeout = timeout
29
+ self.executor = JSONRPCHookExecutor(timeout=timeout)
30
+ self._hook_registry = {}
31
+ self._hook_types = {}
32
+ self._discover_hooks()
33
+
34
+ def _discover_hooks(self):
35
+ """Discover available hooks from builtin directory."""
36
+ hooks_dir = Path(__file__).parent / 'builtin'
37
+ if not hooks_dir.exists():
38
+ logger.warning(f"Builtin hooks directory not found: {hooks_dir}")
39
+ return
40
+
41
+ for hook_file in hooks_dir.glob('*.py'):
42
+ if hook_file.name.startswith('_'):
43
+ continue
44
+
45
+ try:
46
+ # Load the module to discover hooks
47
+ module_name = hook_file.stem
48
+ spec = importlib.util.spec_from_file_location(module_name, hook_file)
49
+ if spec and spec.loader:
50
+ module = importlib.util.module_from_spec(spec)
51
+ spec.loader.exec_module(module)
52
+
53
+ # Find hook classes (don't instantiate, just register)
54
+ for name, obj in inspect.getmembers(module):
55
+ if (inspect.isclass(obj) and
56
+ issubclass(obj, BaseHook) and
57
+ obj not in [BaseHook, SubmitHook, PreDelegationHook,
58
+ PostDelegationHook, TicketExtractionHook] and
59
+ not name.startswith('_')):
60
+ # Create temporary instance to get name
61
+ temp_instance = obj()
62
+ hook_name = temp_instance.name
63
+
64
+ # Determine hook type
65
+ if isinstance(temp_instance, SubmitHook):
66
+ hook_type = HookType.SUBMIT
67
+ elif isinstance(temp_instance, PreDelegationHook):
68
+ hook_type = HookType.PRE_DELEGATION
69
+ elif isinstance(temp_instance, PostDelegationHook):
70
+ hook_type = HookType.POST_DELEGATION
71
+ elif isinstance(temp_instance, TicketExtractionHook):
72
+ hook_type = HookType.TICKET_EXTRACTION
73
+ else:
74
+ hook_type = HookType.CUSTOM
75
+
76
+ self._hook_registry[hook_name] = {
77
+ 'name': hook_name,
78
+ 'type': hook_type,
79
+ 'priority': temp_instance.priority,
80
+ 'class': name,
81
+ 'module': module_name
82
+ }
83
+ self._hook_types[hook_type] = self._hook_types.get(hook_type, [])
84
+ self._hook_types[hook_type].append(hook_name)
85
+
86
+ logger.debug(f"Discovered hook '{hook_name}' of type {hook_type.value}")
87
+
88
+ except Exception as e:
89
+ logger.error(f"Failed to discover hooks from {hook_file}: {e}")
90
+
91
+ def health_check(self) -> Dict[str, Any]:
92
+ """Check health of hook system.
93
+
94
+ Returns:
95
+ Health status dictionary
96
+ """
97
+ try:
98
+ hook_count = len(self._hook_registry)
99
+ return {
100
+ 'status': 'healthy',
101
+ 'hook_count': hook_count,
102
+ 'executor': 'json-rpc',
103
+ 'discovered_hooks': list(self._hook_registry.keys())
104
+ }
105
+ except Exception as e:
106
+ logger.error(f"Health check failed: {e}")
107
+ return {
108
+ 'status': 'unhealthy',
109
+ 'error': str(e)
110
+ }
111
+
112
+ def list_hooks(self) -> Dict[str, List[Dict[str, Any]]]:
113
+ """List all registered hooks.
114
+
115
+ Returns:
116
+ Dictionary mapping hook types to hook info
117
+ """
118
+ hooks_by_type = {}
119
+
120
+ for hook_type in HookType:
121
+ hooks_by_type[hook_type.value] = []
122
+
123
+ for hook_name, hook_info in self._hook_registry.items():
124
+ hook_type = hook_info['type']
125
+ hooks_by_type[hook_type.value].append({
126
+ 'name': hook_name,
127
+ 'priority': hook_info['priority'],
128
+ 'enabled': True # All discovered hooks are enabled
129
+ })
130
+
131
+ # Sort by priority
132
+ for hook_list in hooks_by_type.values():
133
+ hook_list.sort(key=lambda x: x['priority'])
134
+
135
+ return hooks_by_type
136
+
137
+ def execute_hook(self, hook_type: HookType, context_data: Dict[str, Any],
138
+ metadata: Optional[Dict[str, Any]] = None,
139
+ specific_hook: Optional[str] = None) -> List[Dict[str, Any]]:
140
+ """Execute hooks of a given type.
141
+
142
+ Args:
143
+ hook_type: Type of hooks to execute
144
+ context_data: Data to pass to hooks
145
+ metadata: Optional metadata
146
+ specific_hook: Optional specific hook name to execute
147
+
148
+ Returns:
149
+ List of execution results
150
+ """
151
+ try:
152
+ if specific_hook:
153
+ # Execute specific hook
154
+ if specific_hook not in self._hook_registry:
155
+ logger.error(f"Hook '{specific_hook}' not found")
156
+ return [{
157
+ 'hook_name': specific_hook,
158
+ 'success': False,
159
+ 'error': f"Hook '{specific_hook}' not found"
160
+ }]
161
+
162
+ try:
163
+ result = self.executor.execute_hook(
164
+ hook_name=specific_hook,
165
+ hook_type=hook_type,
166
+ context_data=context_data,
167
+ metadata=metadata
168
+ )
169
+ return [result]
170
+ except JSONRPCError as e:
171
+ return [{
172
+ 'hook_name': specific_hook,
173
+ 'success': False,
174
+ 'error': e.message,
175
+ 'error_code': e.code
176
+ }]
177
+
178
+ else:
179
+ # Execute all hooks of the given type
180
+ hook_names = self._hook_types.get(hook_type, [])
181
+ if not hook_names:
182
+ logger.debug(f"No hooks registered for type {hook_type.value}")
183
+ return []
184
+
185
+ # Sort by priority
186
+ sorted_hooks = sorted(
187
+ hook_names,
188
+ key=lambda name: self._hook_registry[name]['priority']
189
+ )
190
+
191
+ return self.executor.execute_hooks(
192
+ hook_type=hook_type,
193
+ hook_names=sorted_hooks,
194
+ context_data=context_data,
195
+ metadata=metadata
196
+ )
197
+
198
+ except Exception as e:
199
+ logger.error(f"Failed to execute hooks: {e}")
200
+ return [{
201
+ 'success': False,
202
+ 'error': str(e)
203
+ }]
204
+
205
+ def execute_submit_hook(self, prompt: str, **kwargs) -> List[Dict[str, Any]]:
206
+ """Execute submit hooks on a user prompt.
207
+
208
+ Args:
209
+ prompt: User prompt to process
210
+ **kwargs: Additional context data
211
+
212
+ Returns:
213
+ List of execution results
214
+ """
215
+ context_data = {'prompt': prompt}
216
+ context_data.update(kwargs)
217
+ return self.execute_hook(HookType.SUBMIT, context_data)
218
+
219
+ def execute_pre_delegation_hook(self, agent: str, context: Dict[str, Any],
220
+ **kwargs) -> List[Dict[str, Any]]:
221
+ """Execute pre-delegation hooks.
222
+
223
+ Args:
224
+ agent: Agent being delegated to
225
+ context: Context being passed to agent
226
+ **kwargs: Additional data
227
+
228
+ Returns:
229
+ List of execution results
230
+ """
231
+ context_data = {
232
+ 'agent': agent,
233
+ 'context': context
234
+ }
235
+ context_data.update(kwargs)
236
+ return self.execute_hook(HookType.PRE_DELEGATION, context_data)
237
+
238
+ def execute_post_delegation_hook(self, agent: str, result: Any,
239
+ **kwargs) -> List[Dict[str, Any]]:
240
+ """Execute post-delegation hooks.
241
+
242
+ Args:
243
+ agent: Agent that was delegated to
244
+ result: Result from agent
245
+ **kwargs: Additional data
246
+
247
+ Returns:
248
+ List of execution results
249
+ """
250
+ context_data = {
251
+ 'agent': agent,
252
+ 'result': result
253
+ }
254
+ context_data.update(kwargs)
255
+ return self.execute_hook(HookType.POST_DELEGATION, context_data)
256
+
257
+ def execute_ticket_extraction_hook(self, content: Any,
258
+ **kwargs) -> List[Dict[str, Any]]:
259
+ """Execute ticket extraction hooks.
260
+
261
+ Args:
262
+ content: Content to extract tickets from
263
+ **kwargs: Additional data
264
+
265
+ Returns:
266
+ List of execution results
267
+ """
268
+ context_data = {'content': content}
269
+ context_data.update(kwargs)
270
+ return self.execute_hook(HookType.TICKET_EXTRACTION, context_data)
271
+
272
+ def get_modified_data(self, results: List[Dict[str, Any]]) -> Dict[str, Any]:
273
+ """Extract modified data from hook results.
274
+
275
+ Args:
276
+ results: Hook execution results
277
+
278
+ Returns:
279
+ Combined modified data from all hooks
280
+ """
281
+ modified_data = {}
282
+
283
+ for result in results:
284
+ if result.get('modified') and result.get('data'):
285
+ modified_data.update(result['data'])
286
+
287
+ return modified_data
288
+
289
+ def get_extracted_tickets(self, results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
290
+ """Extract tickets from hook results.
291
+
292
+ Args:
293
+ results: Hook execution results
294
+
295
+ Returns:
296
+ List of extracted tickets
297
+ """
298
+ all_tickets = []
299
+
300
+ for result in results:
301
+ if result.get('success') and 'tickets' in result.get('data', {}):
302
+ tickets = result['data']['tickets']
303
+ if isinstance(tickets, list):
304
+ all_tickets.extend(tickets)
305
+
306
+ return all_tickets
307
+
308
+
309
+ # Update the convenience function to use JSON-RPC client
310
+ def get_hook_client(base_url: Optional[str] = None) -> JSONRPCHookClient:
311
+ """Get a hook client instance.
312
+
313
+ Args:
314
+ base_url: Ignored for JSON-RPC client (kept for compatibility)
315
+
316
+ Returns:
317
+ JSONRPCHookClient instance
318
+ """
319
+ return JSONRPCHookClient()
@@ -0,0 +1,204 @@
1
+ """Tool call interceptor for claude-mpm hook system."""
2
+
3
+ import asyncio
4
+ import json
5
+ from typing import Dict, Any, Optional, List
6
+ from datetime import datetime
7
+ from collections import defaultdict
8
+
9
+ from claude_mpm.hooks.base_hook import BaseHook, HookContext, HookType, HookResult
10
+ from claude_mpm.core.logger import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class SimpleHookRunner:
16
+ """Simple hook runner for direct hook execution."""
17
+
18
+ def __init__(self):
19
+ """Initialize the simple hook runner."""
20
+ self._hooks: Dict[HookType, List[BaseHook]] = defaultdict(list)
21
+ self._hook_instances: Dict[str, BaseHook] = {}
22
+
23
+ def register_hook(self, hook: BaseHook, hook_type: Optional[HookType] = None):
24
+ """Register a hook instance."""
25
+ if hook_type is None:
26
+ hook_type = HookType.CUSTOM
27
+
28
+ if hook.name in self._hook_instances:
29
+ # Remove old instance
30
+ for hook_list in self._hooks.values():
31
+ if self._hook_instances[hook.name] in hook_list:
32
+ hook_list.remove(self._hook_instances[hook.name])
33
+
34
+ self._hooks[hook_type].append(hook)
35
+ self._hook_instances[hook.name] = hook
36
+ self._hooks[hook_type].sort() # Sort by priority
37
+
38
+ async def run_hooks(self, context: HookContext) -> List[Dict[str, Any]]:
39
+ """Run all hooks for the given context."""
40
+ hooks = [h for h in self._hooks[context.hook_type] if h.enabled]
41
+ results = []
42
+
43
+ for hook in hooks:
44
+ try:
45
+ if hook.validate(context):
46
+ result = hook.execute(context)
47
+ results.append({
48
+ 'hook_name': hook.name,
49
+ 'success': result.success,
50
+ 'data': result.data,
51
+ 'error': result.error,
52
+ 'modified': result.modified,
53
+ 'metadata': result.metadata
54
+ })
55
+
56
+ # Update context if modified
57
+ if result.modified and result.data:
58
+ context.data.update(result.data)
59
+ except Exception as e:
60
+ logger.error(f"Hook '{hook.name}' execution failed: {e}")
61
+ results.append({
62
+ 'hook_name': hook.name,
63
+ 'success': False,
64
+ 'error': str(e)
65
+ })
66
+
67
+ return results
68
+
69
+
70
+ class ToolCallInterceptor:
71
+ """Intercepts and processes tool calls through the hook system."""
72
+
73
+ def __init__(self, hook_runner: Optional[SimpleHookRunner] = None):
74
+ """Initialize the tool call interceptor.
75
+
76
+ Args:
77
+ hook_runner: Optional hook runner instance. If not provided, creates a new one.
78
+ """
79
+ self.hook_runner = hook_runner or SimpleHookRunner()
80
+
81
+ async def intercept_tool_call(self, tool_name: str, parameters: Dict[str, Any],
82
+ metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
83
+ """Intercept a tool call and run it through the hook system.
84
+
85
+ Args:
86
+ tool_name: Name of the tool being called
87
+ parameters: Parameters being passed to the tool
88
+ metadata: Optional metadata for the tool call
89
+
90
+ Returns:
91
+ Dict containing:
92
+ - allowed: Whether the tool call should proceed
93
+ - parameters: Potentially modified parameters
94
+ - error: Error message if not allowed
95
+ - metadata: Additional metadata from hooks
96
+ """
97
+ # Create hook context for tool call interception
98
+ context = HookContext(
99
+ hook_type=HookType.CUSTOM,
100
+ data={
101
+ 'tool_name': tool_name,
102
+ 'parameters': parameters.copy() # Copy to avoid modifying original
103
+ },
104
+ metadata=metadata or {},
105
+ timestamp=datetime.now()
106
+ )
107
+
108
+ # Run hooks
109
+ results = await self.hook_runner.run_hooks(context)
110
+
111
+ # Process results
112
+ allowed = True
113
+ modified_params = parameters
114
+ errors = []
115
+ hook_metadata = {}
116
+
117
+ for result in results:
118
+ if not result.get('success', True):
119
+ allowed = False
120
+ if result.get('error'):
121
+ errors.append(f"[{result.get('hook_name', 'Unknown')}] {result.get('error')}")
122
+
123
+ if result.get('modified') and result.get('data'):
124
+ # Update parameters if modified
125
+ if 'parameters' in result.get('data', {}):
126
+ modified_params = result['data']['parameters']
127
+
128
+ if result.get('metadata'):
129
+ hook_metadata.update(result['metadata'])
130
+
131
+ return {
132
+ 'allowed': allowed,
133
+ 'parameters': modified_params,
134
+ 'error': '\n'.join(errors) if errors else None,
135
+ 'metadata': hook_metadata
136
+ }
137
+
138
+ def intercept_tool_call_sync(self, tool_name: str, parameters: Dict[str, Any],
139
+ metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
140
+ """Synchronous version of intercept_tool_call."""
141
+ loop = asyncio.new_event_loop()
142
+ asyncio.set_event_loop(loop)
143
+ try:
144
+ return loop.run_until_complete(
145
+ self.intercept_tool_call(tool_name, parameters, metadata)
146
+ )
147
+ finally:
148
+ loop.close()
149
+
150
+
151
+ class ToolCallHookIntegration:
152
+ """Integration helper for adding tool call interception to existing systems."""
153
+
154
+ @staticmethod
155
+ def wrap_tool_executor(original_executor, interceptor: ToolCallInterceptor):
156
+ """Wrap an existing tool executor with hook interception.
157
+
158
+ Args:
159
+ original_executor: The original tool execution function
160
+ interceptor: The tool call interceptor instance
161
+
162
+ Returns:
163
+ Wrapped executor function
164
+ """
165
+ async def wrapped_executor(tool_name: str, parameters: Dict[str, Any], **kwargs):
166
+ # Intercept the tool call
167
+ interception_result = await interceptor.intercept_tool_call(
168
+ tool_name, parameters, kwargs.get('metadata')
169
+ )
170
+
171
+ # Check if allowed
172
+ if not interception_result['allowed']:
173
+ raise ValueError(f"Tool call blocked: {interception_result['error']}")
174
+
175
+ # Execute with potentially modified parameters
176
+ return await original_executor(
177
+ tool_name,
178
+ interception_result['parameters'],
179
+ **kwargs
180
+ )
181
+
182
+ return wrapped_executor
183
+
184
+ @staticmethod
185
+ def create_tool_call_validator(valid_tools: List[str], interceptor: ToolCallInterceptor):
186
+ """Create a tool call validator that uses the hook system.
187
+
188
+ Args:
189
+ valid_tools: List of valid tool names
190
+ interceptor: The tool call interceptor instance
191
+
192
+ Returns:
193
+ Validator function
194
+ """
195
+ def validator(tool_name: str, parameters: Dict[str, Any]) -> bool:
196
+ # Basic validation
197
+ if tool_name not in valid_tools:
198
+ return False
199
+
200
+ # Hook-based validation
201
+ result = interceptor.intercept_tool_call_sync(tool_name, parameters)
202
+ return result['allowed']
203
+
204
+ return validator