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:
2026-05-09 14:32:44 -04:00
parent 696c08692e
commit 8c06c1767b
142 changed files with 2352 additions and 990 deletions
+119 -57
View File
@@ -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}