Skip to content

Commit 020fdfe

Browse files
committed
refactor: enforce strict config loading, improve MCP argument validation, and update error handling to catch specific OS exceptions
1 parent 7c51609 commit 020fdfe

5 files changed

Lines changed: 39 additions & 29 deletions

File tree

src/knowcode/config.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,9 @@ def _load_from_yaml(cls, path: Path, strict: bool = False) -> "AppConfig":
185185
)
186186
except Exception as e:
187187
message = f"Failed to load config from {path}: {e}"
188-
if strict:
189-
raise ValueError(message) from e
190-
logger.warning(message)
191-
return cls.default()
188+
# RF-001-D004: NEVER swallow errors silently.
189+
# If a config file is present but malformed, fail explicitly.
190+
raise ValueError(message) from e
192191

193192
@classmethod
194193
def _validate_keys(cls, data: dict[str, Any], *, path: Path, strict: bool) -> None:

src/knowcode/indexing/monitor.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
from watchdog.events import FileSystemEventHandler
1313
except ImportError:
1414
Observer = None # type: ignore[assignment]
15-
class FileSystemEventHandler:
16-
pass # type: ignore[no-redef]
15+
FileSystemEventHandler = object # type: ignore[misc, assignment]
1716

1817
if TYPE_CHECKING:
1918
from knowcode.indexing.background_indexer import BackgroundIndexer

src/knowcode/mcp/server.py

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,9 @@ def handle_tool_call(self, name: str, arguments: dict[str, Any]) -> str:
312312
Returns:
313313
JSON string result.
314314
"""
315+
if not isinstance(arguments, dict):
316+
return json.dumps({"error": "arguments must be a dictionary"})
317+
315318
try:
316319
from knowcode.telemetry import log_event
317320
log_event(
@@ -322,37 +325,47 @@ def handle_tool_call(self, name: str, arguments: dict[str, Any]) -> str:
322325
"arguments": arguments,
323326
}
324327
)
325-
except Exception as e:
328+
except OSError as e:
326329
import logging
327-
logging.getLogger(__name__).warning("Ignored exception: %s", e)
330+
logging.getLogger(__name__).warning("Ignored telemetry OS error: %s", e)
328331

329332
try:
330333
if name == "search_codebase":
331-
334+
if "query" not in arguments or not isinstance(arguments["query"], str):
335+
raise ValueError("search_codebase requires 'query' as a string")
336+
limit = arguments.get("limit", 10)
337+
if not isinstance(limit, int):
338+
raise ValueError("'limit' must be an integer")
332339
result = self.search_codebase(
333340
query=arguments["query"],
334-
limit=arguments.get("limit", 10),
341+
limit=limit,
335342
)
336343
elif name == "get_entity_context":
344+
if "entity_id" not in arguments or not isinstance(arguments["entity_id"], str):
345+
raise ValueError("get_entity_context requires 'entity_id' as a string")
337346
result = self.get_entity_context( # type: ignore
338347
entity_id=arguments["entity_id"],
339-
task_type=arguments.get("task_type", "general"),
340-
max_tokens=arguments.get("max_tokens", 2000),
348+
task_type=str(arguments.get("task_type", "general")),
349+
max_tokens=int(arguments.get("max_tokens", 2000)),
341350
)
342351
elif name == "trace_calls":
352+
if "entity_id" not in arguments or not isinstance(arguments["entity_id"], str):
353+
raise ValueError("trace_calls requires 'entity_id' as a string")
343354
result = self.trace_calls(
344355
entity_id=arguments["entity_id"],
345-
direction=arguments.get("direction", "callees"),
346-
depth=arguments.get("depth", 1),
356+
direction=str(arguments.get("direction", "callees")),
357+
depth=int(arguments.get("depth", 1)),
347358
)
348359
elif name == "retrieve_context_for_query":
360+
if "query" not in arguments or not isinstance(arguments["query"], str):
361+
raise ValueError("retrieve_context_for_query requires 'query' as a string")
349362
result = self.retrieve_context_for_query( # type: ignore
350363
query=arguments["query"],
351-
task_type=arguments.get("task_type", "auto"),
352-
max_tokens=arguments.get("max_tokens", 4000),
353-
limit_entities=arguments.get("limit_entities", 3),
354-
expand_deps=arguments.get("expand_deps", True),
355-
verbosity=arguments.get("verbosity", "minimal"),
364+
task_type=str(arguments.get("task_type", "auto")),
365+
max_tokens=int(arguments.get("max_tokens", 4000)),
366+
limit_entities=int(arguments.get("limit_entities", 3)),
367+
expand_deps=bool(arguments.get("expand_deps", True)),
368+
verbosity=str(arguments.get("verbosity", "minimal")),
356369
)
357370
else:
358371
result = {"error": f"Unknown tool: {name}"} # type: ignore
@@ -363,6 +376,8 @@ def handle_tool_call(self, name: str, arguments: dict[str, Any]) -> str:
363376
{"error": str(e), "code": e.code, "hint": e.hint},
364377
separators=(",", ":"),
365378
)
379+
except ValueError as e:
380+
return json.dumps({"error": f"Validation Error: {e}"}, separators=(',', ':'))
366381
except Exception as e:
367382
return json.dumps({"error": str(e)}, separators=(',', ':'))
368383

src/knowcode/service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,9 +309,9 @@ def get_freshness_metadata(self) -> dict[str, Any]:
309309
files = scanner.scan_all()
310310
if files:
311311
latest_source_change = max(os.path.getmtime(f.path) for f in files)
312-
except Exception as e:
312+
except OSError as e:
313313
import logging
314-
logging.getLogger(__name__).warning("Failed to check source staleness: %s", e)
314+
logging.getLogger(__name__).warning("Failed to check source staleness (OS error): %s", e)
315315

316316
if last_store_rebuild == 0.0:
317317
is_stale = True

tests/unit/test_config_loading.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,12 @@ def test_load_strict_rejects_invalid_root_type(tmp_path: Path) -> None:
105105
AppConfig.load(str(config_file), strict=True)
106106

107107

108-
def test_load_non_strict_invalid_file_falls_back_to_default(
109-
tmp_path: Path, caplog: pytest.LogCaptureFixture
108+
def test_load_non_strict_invalid_file_raises_error(
109+
tmp_path: Path
110110
) -> None:
111-
"""Non-strict mode should warn and return defaults on invalid config."""
111+
"""Non-strict mode should still raise on invalid config to prevent silent failures."""
112112
config_file = tmp_path / "aimodels.yaml"
113113
_write(config_file, "config: [")
114114

115-
with caplog.at_level(logging.WARNING, logger="knowcode.config"):
116-
cfg = AppConfig.load(str(config_file))
117-
118-
assert cfg.models # default models loaded
119-
assert any("Failed to load config" in rec.message for rec in caplog.records)
115+
with pytest.raises(ValueError, match="Failed to load config from"):
116+
AppConfig.load(str(config_file))

0 commit comments

Comments
 (0)