fix(kg): validate ISO-8601 date formats at MCP boundary
tool_kg_query (as_of), tool_kg_add (valid_from), and tool_kg_invalidate (ended) accepted any string and forwarded it to SQLite without format validation. Parameterized queries prevent SQL injection, but invalid date strings silently produce empty result sets — callers cannot distinguish "no fact at this time" from "your date format was unrecognized." This is especially painful for natural-language LLM callers that synthesize dates like "March 2026" or "Jan 2025". Add sanitize_iso_date() in config.py alongside the other input validators. It accepts YYYY, YYYY-MM, and YYYY-MM-DD forms; passes through None/empty; and raises ValueError with a field-named message on anything else. Call it from the three kg MCP tool wrappers before values reach the storage layer so the caller gets a clear error instead of a silent miss. Closes #1164
This commit is contained in:
+69
-1
@@ -3,7 +3,13 @@ import json
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from mempalace.config import MempalaceConfig, normalize_wing_name, sanitize_kg_value, sanitize_name
|
||||
from mempalace.config import (
|
||||
MempalaceConfig,
|
||||
normalize_wing_name,
|
||||
sanitize_iso_date,
|
||||
sanitize_kg_value,
|
||||
sanitize_name,
|
||||
)
|
||||
|
||||
|
||||
def test_default_config():
|
||||
@@ -212,3 +218,65 @@ def test_kg_value_rejects_null_bytes():
|
||||
def test_kg_value_rejects_over_length():
|
||||
with pytest.raises(ValueError):
|
||||
sanitize_kg_value("a" * 129)
|
||||
|
||||
|
||||
# --- sanitize_iso_date ---
|
||||
|
||||
|
||||
def test_iso_date_accepts_year_only():
|
||||
assert sanitize_iso_date("2026") == "2026"
|
||||
|
||||
|
||||
def test_iso_date_accepts_year_month():
|
||||
assert sanitize_iso_date("2026-03") == "2026-03"
|
||||
|
||||
|
||||
def test_iso_date_accepts_full_date():
|
||||
assert sanitize_iso_date("2026-03-15") == "2026-03-15"
|
||||
|
||||
|
||||
def test_iso_date_passes_through_none():
|
||||
assert sanitize_iso_date(None) is None
|
||||
|
||||
|
||||
def test_iso_date_passes_through_empty_string():
|
||||
assert sanitize_iso_date("") == ""
|
||||
|
||||
|
||||
def test_iso_date_strips_whitespace():
|
||||
assert sanitize_iso_date(" 2026-03-15 ") == "2026-03-15"
|
||||
|
||||
|
||||
def test_iso_date_rejects_natural_language():
|
||||
with pytest.raises(ValueError):
|
||||
sanitize_iso_date("March 2026")
|
||||
|
||||
|
||||
def test_iso_date_rejects_abbreviated_month():
|
||||
with pytest.raises(ValueError):
|
||||
sanitize_iso_date("Jan 2025")
|
||||
|
||||
|
||||
def test_iso_date_rejects_us_format():
|
||||
with pytest.raises(ValueError):
|
||||
sanitize_iso_date("03/15/2026")
|
||||
|
||||
|
||||
def test_iso_date_rejects_invalid_month():
|
||||
with pytest.raises(ValueError):
|
||||
sanitize_iso_date("2026-13")
|
||||
|
||||
|
||||
def test_iso_date_rejects_invalid_day():
|
||||
with pytest.raises(ValueError):
|
||||
sanitize_iso_date("2026-02-32")
|
||||
|
||||
|
||||
def test_iso_date_rejects_non_string():
|
||||
with pytest.raises(ValueError):
|
||||
sanitize_iso_date(20260315)
|
||||
|
||||
|
||||
def test_iso_date_error_names_field():
|
||||
with pytest.raises(ValueError, match="valid_from"):
|
||||
sanitize_iso_date("yesterday", "valid_from")
|
||||
|
||||
Reference in New Issue
Block a user