refactor(sdm): Global pass with refined 'External Only' SDM tags. Pruned redundant internal references and fixed indentation logic in injector. Verified full project compilation.
This commit is contained in:
+119
-57
@@ -96,12 +96,14 @@ perf_monitor_callback: Optional[Callable[[], dict[str, Any]]] = None
|
||||
|
||||
def configure(file_items: list[dict[str, Any]], extra_base_dirs: list[str] | None = None) -> None:
|
||||
"""
|
||||
Build the allowlist from aggregate file_items.
|
||||
Called by ai_client before each send so the list reflects the current project.
|
||||
|
||||
file_items : list of dicts from aggregate.build_file_items()
|
||||
extra_base_dirs : additional directory roots to allow traversal of
|
||||
"""
|
||||
|
||||
Build the allowlist from aggregate file_items.
|
||||
Called by ai_client before each send so the list reflects the current project.
|
||||
|
||||
file_items : list of dicts from aggregate.build_file_items()
|
||||
extra_base_dirs : additional directory roots to allow traversal of
|
||||
[C: tests/conftest.py:reset_ai_client, tests/test_arch_boundary_phase1.py:TestArchBoundaryPhase1.test_mcp_client_whitelist_enforcement, tests/test_mcp_client_beads.py:test_bd_mcp_tools]
|
||||
"""
|
||||
global _allowed_paths, _base_dirs, _primary_base_dir
|
||||
_allowed_paths = set()
|
||||
_base_dirs = set()
|
||||
@@ -123,15 +125,17 @@ def configure(file_items: list[dict[str, Any]], extra_base_dirs: list[str] | Non
|
||||
|
||||
def _is_allowed(path: Path) -> bool:
|
||||
"""
|
||||
Return True if `path` is within the allowlist.
|
||||
A path is allowed if:
|
||||
- it is explicitly in _allowed_paths, OR
|
||||
- it is contained within (or equal to) one of the _base_dirs
|
||||
All paths are resolved (follows symlinks) before comparison to prevent
|
||||
symlink-based path traversal.
|
||||
|
||||
CRITICAL: Blacklisted files (history) are NEVER allowed.
|
||||
"""
|
||||
|
||||
Return True if `path` is within the allowlist.
|
||||
A path is allowed if:
|
||||
- it is explicitly in _allowed_paths, OR
|
||||
- it is contained within (or equal to) one of the _base_dirs
|
||||
All paths are resolved (follows symlinks) before comparison to prevent
|
||||
symlink-based path traversal.
|
||||
|
||||
CRITICAL: Blacklisted files (history) are NEVER allowed.
|
||||
[C: tests/test_arch_boundary_phase1.py:TestArchBoundaryPhase1.test_mcp_client_whitelist_enforcement, tests/test_history_management.py:test_mcp_blacklist]
|
||||
"""
|
||||
from src.paths import get_config_path
|
||||
from src.ai_client import get_credentials_path
|
||||
|
||||
@@ -170,9 +174,10 @@ def _is_allowed(path: Path) -> bool:
|
||||
|
||||
def _resolve_and_check(raw_path: str) -> tuple[Path | None, str]:
|
||||
"""
|
||||
Resolve raw_path and verify it passes the allowlist check.
|
||||
Returns (resolved_path, error_string). error_string is empty on success.
|
||||
"""
|
||||
|
||||
Resolve raw_path and verify it passes the allowlist check.
|
||||
Returns (resolved_path, error_string). error_string is empty on success.
|
||||
"""
|
||||
try:
|
||||
p = Path(raw_path)
|
||||
if not p.is_absolute() and _primary_base_dir:
|
||||
@@ -232,9 +237,10 @@ def list_directory(path: str) -> str:
|
||||
|
||||
def search_files(path: str, pattern: str) -> str:
|
||||
"""
|
||||
Search for files matching a glob pattern within path.
|
||||
pattern examples: '*.py', '**/*.toml', 'src/**/*.rs'
|
||||
"""
|
||||
|
||||
Search for files matching a glob pattern within path.
|
||||
pattern examples: '*.py', '**/*.toml', 'src/**/*.rs'
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err or p is None:
|
||||
return err
|
||||
@@ -262,10 +268,11 @@ def search_files(path: str, pattern: str) -> str:
|
||||
|
||||
def get_file_summary(path: str) -> str:
|
||||
"""
|
||||
Return the heuristic summary for a file (same as the initial context block).
|
||||
For .py files: imports, classes, methods, functions, constants.
|
||||
For .toml: table keys. For .md: headings. Others: line count + preview.
|
||||
"""
|
||||
|
||||
Return the heuristic summary for a file (same as the initial context block).
|
||||
For .py files: imports, classes, methods, functions, constants.
|
||||
For .toml: table keys. For .md: headings. Others: line count + preview.
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err or p is None:
|
||||
return err
|
||||
@@ -281,8 +288,9 @@ def get_file_summary(path: str) -> str:
|
||||
|
||||
def py_get_skeleton(path: str) -> str:
|
||||
"""
|
||||
Returns a skeleton of a Python file (preserving docstrings, stripping function bodies).
|
||||
"""
|
||||
|
||||
Returns a skeleton of a Python file (preserving docstrings, stripping function bodies).
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
@@ -300,7 +308,10 @@ def py_get_skeleton(path: str) -> str:
|
||||
return f"ERROR generating skeleton for '{path}': {e}"
|
||||
|
||||
def ts_c_get_skeleton(path: str) -> str:
|
||||
"""Returns a skeleton of a C file."""
|
||||
"""
|
||||
Returns a skeleton of a C file.
|
||||
[C: tests/test_ts_c_tools.py:test_ts_c_get_skeleton]
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
@@ -314,7 +325,10 @@ def ts_c_get_skeleton(path: str) -> str:
|
||||
return f"ERROR generating skeleton for '{path}': {e}"
|
||||
|
||||
def ts_cpp_get_skeleton(path: str) -> str:
|
||||
"""Returns a skeleton of a C++ file."""
|
||||
"""
|
||||
Returns a skeleton of a C++ file.
|
||||
[C: tests/test_ts_cpp_tools.py:test_exhaustive_cpp_samples, tests/test_ts_cpp_tools.py:test_exhaustive_gencpp_samples, tests/test_ts_cpp_tools.py:test_ts_cpp_get_skeleton]
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
@@ -329,8 +343,9 @@ def ts_cpp_get_skeleton(path: str) -> str:
|
||||
|
||||
def py_get_code_outline(path: str) -> str:
|
||||
"""
|
||||
Returns a hierarchical outline of a code file (classes, functions, methods with line ranges).
|
||||
"""
|
||||
|
||||
Returns a hierarchical outline of a code file (classes, functions, methods with line ranges).
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
@@ -346,7 +361,10 @@ def py_get_code_outline(path: str) -> str:
|
||||
return f"ERROR generating outline for '{path}': {e}"
|
||||
|
||||
def ts_c_get_code_outline(path: str) -> str:
|
||||
"""Returns a hierarchical outline of a C file."""
|
||||
"""
|
||||
Returns a hierarchical outline of a C file.
|
||||
[C: tests/test_ts_c_tools.py:test_ts_c_get_code_outline]
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
@@ -360,7 +378,10 @@ def ts_c_get_code_outline(path: str) -> str:
|
||||
return f"ERROR generating outline for '{path}': {e}"
|
||||
|
||||
def ts_cpp_get_code_outline(path: str) -> str:
|
||||
"""Returns a hierarchical outline of a C++ file."""
|
||||
"""
|
||||
Returns a hierarchical outline of a C++ file.
|
||||
[C: tests/test_ts_cpp_tools.py:test_exhaustive_cpp_samples, tests/test_ts_cpp_tools.py:test_exhaustive_gencpp_samples, tests/test_ts_cpp_tools.py:test_ts_cpp_get_code_outline]
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
@@ -388,7 +409,10 @@ def ts_c_get_definition(path: str, name: str) -> str:
|
||||
return f"ERROR retrieving definition '{name}' from '{path}': {e}"
|
||||
|
||||
def ts_cpp_get_definition(path: str, name: str) -> str:
|
||||
"""Returns the source code for a specific definition in a C++ file."""
|
||||
"""
|
||||
Returns the source code for a specific definition in a C++ file.
|
||||
[C: tests/test_ts_cpp_tools.py:test_exhaustive_cpp_samples, tests/test_ts_cpp_tools.py:test_exhaustive_gencpp_samples, tests/test_ts_cpp_tools.py:test_ts_cpp_update_definition, tests/test_ts_cpp_tools.py:test_ts_cpp_update_definition_gencpp]
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
@@ -448,7 +472,10 @@ def ts_c_update_definition(path: str, name: str, new_content: str) -> str:
|
||||
return f"ERROR updating definition '{name}' in '{path}': {e}"
|
||||
|
||||
def ts_cpp_update_definition(path: str, name: str, new_content: str) -> str:
|
||||
"""Surgically replace the definition of a class or function in a C++ file."""
|
||||
"""
|
||||
Surgically replace the definition of a class or function in a C++ file.
|
||||
[C: tests/test_ts_cpp_tools.py:test_ts_cpp_update_definition, tests/test_ts_cpp_tools.py:test_ts_cpp_update_definition_gencpp]
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
@@ -504,8 +531,9 @@ def set_file_slice(path: str, start_line: int, end_line: int, new_content: str)
|
||||
|
||||
def edit_file(path: str, old_string: str, new_string: str, replace_all: bool = False) -> str:
|
||||
"""
|
||||
Replace exact string match in a file. Preserves indentation and line endings.
|
||||
Drop-in replacement for native edit tool that destroys 1-space indentation.
|
||||
|
||||
Replace exact string match in a file. Preserves indentation and line endings.
|
||||
Drop-in replacement for native edit tool that destroys 1-space indentation.
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
@@ -561,9 +589,10 @@ def _get_symbol_node(tree: ast.AST, name: str) -> Optional[ast.AST]:
|
||||
|
||||
def py_get_symbol_info(path: str, name: str) -> tuple[str, int] | str:
|
||||
"""
|
||||
Returns (source_code, line_number) for a specific class, function, or method definition.
|
||||
If not found, returns an error string.
|
||||
"""
|
||||
|
||||
Returns (source_code, line_number) for a specific class, function, or method definition.
|
||||
If not found, returns an error string.
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
@@ -587,10 +616,11 @@ def py_get_symbol_info(path: str, name: str) -> tuple[str, int] | str:
|
||||
|
||||
def py_get_definition(path: str, name: str) -> str:
|
||||
"""
|
||||
Returns the source code for a specific class, function, or method definition.
|
||||
path: Path to the code file.
|
||||
name: Name of the definition to retrieve (e.g., 'MyClass', 'my_function', 'MyClass.my_method').
|
||||
"""
|
||||
|
||||
Returns the source code for a specific class, function, or method definition.
|
||||
path: Path to the code file.
|
||||
name: Name of the definition to retrieve (e.g., 'MyClass', 'my_function', 'MyClass.my_method').
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
@@ -761,10 +791,11 @@ def py_set_var_declaration(path: str, name: str, new_declaration: str) -> str:
|
||||
|
||||
def get_git_diff(path: str, base_rev: str = "HEAD", head_rev: str = "") -> str:
|
||||
"""
|
||||
Returns the git diff for a file or directory.
|
||||
base_rev: The base revision (default: HEAD)
|
||||
head_rev: The head revision (optional)
|
||||
"""
|
||||
|
||||
Returns the git diff for a file or directory.
|
||||
base_rev: The base revision (default: HEAD)
|
||||
head_rev: The head revision (optional)
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
@@ -1114,7 +1145,10 @@ def fetch_url(url: str) -> str:
|
||||
return f"ERROR fetching URL '{url}': {e}"
|
||||
|
||||
def get_ui_performance() -> str:
|
||||
"""Returns current UI performance metrics (FPS, Frame Time, CPU, Input Lag)."""
|
||||
"""
|
||||
Returns current UI performance metrics (FPS, Frame Time, CPU, Input Lag).
|
||||
[C: tests/test_mcp_perf_tool.py:test_mcp_perf_tool_retrieval]
|
||||
"""
|
||||
if perf_monitor_callback is None:
|
||||
return "INFO: UI Performance monitor is not available (headless/CLI mode). This tool is only functional when the Manual Slop GUI is running."
|
||||
try:
|
||||
@@ -1143,6 +1177,9 @@ class StdioMCPServer:
|
||||
return self._id_counter
|
||||
|
||||
async def start(self):
|
||||
"""
|
||||
[C: src/multi_agent_conductor.py:WorkerPool.spawn, src/performance_monitor.py:PerformanceMonitor.__init__, tests/test_ai_client_concurrency.py:test_ai_client_tier_isolation, tests/test_conductor_engine_abort.py:test_kill_worker_sets_abort_and_joins_thread, tests/test_conductor_engine_v2.py:side_effect, tests/test_spawn_interception_v2.py:test_confirm_spawn_pushed_to_queue, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast]
|
||||
"""
|
||||
self.status = 'starting'
|
||||
self.proc = await asyncio.create_subprocess_exec(
|
||||
self.config.command,
|
||||
@@ -1156,6 +1193,9 @@ class StdioMCPServer:
|
||||
self.status = 'running'
|
||||
|
||||
async def stop(self):
|
||||
"""
|
||||
[C: tests/test_performance_monitor.py:test_perf_monitor_basic_timing, tests/test_performance_monitor.py:test_perf_monitor_component_timing, tests/test_performance_monitor.py:test_perf_monitor_extended_metrics, tests/test_performance_monitor.py:test_perf_monitor_scope_context_manager, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast]
|
||||
"""
|
||||
if self.proc:
|
||||
try:
|
||||
if self.proc.stdin:
|
||||
@@ -1216,7 +1256,10 @@ class ExternalMCPManager:
|
||||
self.servers = {}
|
||||
|
||||
async def add_server(self, config: models.MCPServerConfig):
|
||||
"""Add and start a new MCP server from a configuration object."""
|
||||
"""
|
||||
Add and start a new MCP server from a configuration object.
|
||||
[C: tests/test_external_mcp.py:test_external_mcp_real_process, tests/test_external_mcp.py:test_get_tool_schemas_includes_external]
|
||||
"""
|
||||
if config.url:
|
||||
# RemoteMCPServer placeholder
|
||||
return
|
||||
@@ -1225,13 +1268,19 @@ class ExternalMCPManager:
|
||||
self.servers[config.name] = server
|
||||
|
||||
async def stop_all(self):
|
||||
"""Stop all managed MCP servers and clear the registry."""
|
||||
"""
|
||||
Stop all managed MCP servers and clear the registry.
|
||||
[C: tests/test_external_mcp.py:test_external_mcp_real_process, tests/test_external_mcp.py:test_get_tool_schemas_includes_external, tests/test_external_mcp_e2e.py:test_external_mcp_e2e_refresh_and_call]
|
||||
"""
|
||||
for server in self.servers.values():
|
||||
await server.stop()
|
||||
self.servers = {}
|
||||
|
||||
def get_all_tools(self) -> dict:
|
||||
"""Retrieve a dictionary of all tools available across all managed servers."""
|
||||
"""
|
||||
Retrieve a dictionary of all tools available across all managed servers.
|
||||
[C: tests/test_external_mcp.py:test_external_mcp_real_process, tests/test_external_mcp_e2e.py:test_external_mcp_e2e_refresh_and_call]
|
||||
"""
|
||||
all_tools = {}
|
||||
for sname, server in self.servers.items():
|
||||
for tname, tool in server.tools.items():
|
||||
@@ -1243,7 +1292,10 @@ class ExternalMCPManager:
|
||||
return {name: server.status for name, server in self.servers.items()}
|
||||
|
||||
async def async_dispatch(self, tool_name: str, tool_input: dict) -> str:
|
||||
"""Dispatch a tool call to the appropriate external MCP server asynchronously."""
|
||||
"""
|
||||
Dispatch a tool call to the appropriate external MCP server asynchronously.
|
||||
[C: src/rag_engine.py:RAGEngine._async_search_mcp, tests/test_external_mcp.py:test_external_mcp_real_process]
|
||||
"""
|
||||
for server in self.servers.values():
|
||||
if tool_name in server.tools:
|
||||
return await server.call_tool(tool_name, tool_input)
|
||||
@@ -1252,14 +1304,19 @@ class ExternalMCPManager:
|
||||
_external_mcp_manager = ExternalMCPManager()
|
||||
|
||||
def get_external_mcp_manager() -> ExternalMCPManager:
|
||||
"""Retrieve the global ExternalMCPManager instance."""
|
||||
"""
|
||||
Retrieve the global ExternalMCPManager instance.
|
||||
[C: tests/test_external_mcp.py:test_get_tool_schemas_includes_external, tests/test_external_mcp_e2e.py:test_external_mcp_e2e_refresh_and_call]
|
||||
"""
|
||||
global _external_mcp_manager
|
||||
return _external_mcp_manager
|
||||
|
||||
def dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
"""
|
||||
Dispatch an MCP tool call by name. Returns the result as a string.
|
||||
"""
|
||||
|
||||
Dispatch an MCP tool call by name. Returns the result as a string.
|
||||
[C: tests/test_gemini_cli_edge_cases.py:test_gemini_cli_parameter_resilience, tests/test_mcp_client_beads.py:test_bd_mcp_tools, tests/test_mcp_ts_integration.py:test_ts_c_get_code_outline_dispatch, tests/test_mcp_ts_integration.py:test_ts_c_get_definition_dispatch, tests/test_mcp_ts_integration.py:test_ts_c_get_signature_dispatch, tests/test_mcp_ts_integration.py:test_ts_c_get_skeleton_dispatch, tests/test_mcp_ts_integration.py:test_ts_c_update_definition_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_get_code_outline_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_get_definition_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_get_signature_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_get_skeleton_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_update_definition_dispatch]
|
||||
"""
|
||||
# Handle aliases
|
||||
path = str(tool_input.get("path", tool_input.get("file_path", tool_input.get("dir_path", ""))))
|
||||
if tool_name == "read_file":
|
||||
@@ -1377,6 +1434,9 @@ def dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
|
||||
async def async_dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
# Check native tools
|
||||
"""
|
||||
[C: src/rag_engine.py:RAGEngine._async_search_mcp, tests/test_external_mcp.py:test_external_mcp_real_process]
|
||||
"""
|
||||
native_names = {t['name'] for t in MCP_TOOL_SPECS}
|
||||
if tool_name in native_names:
|
||||
return await asyncio.to_thread(dispatch, tool_name, tool_input)
|
||||
@@ -1390,6 +1450,9 @@ async def async_dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
|
||||
|
||||
def get_tool_schemas() -> list[dict[str, Any]]:
|
||||
"""
|
||||
[C: tests/test_arch_boundary_phase2.py:TestArchBoundaryPhase2.test_mcp_client_dispatch_completeness, tests/test_external_mcp.py:test_get_tool_schemas_includes_external, tests/test_mcp_client_beads.py:test_bd_mcp_tools]
|
||||
"""
|
||||
res = list(MCP_TOOL_SPECS)
|
||||
manager = get_external_mcp_manager()
|
||||
for tname, tinfo in manager.get_all_tools().items():
|
||||
@@ -2126,4 +2189,3 @@ TOOL_NAMES: set[str] = {t['name'] for t in MCP_TOOL_SPECS}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user