diff --git a/src/powercontext/builtin/persistence/oceanbase/profile.py b/src/powercontext/builtin/persistence/oceanbase/profile.py index 497e927f8..d6f872b0b 100644 --- a/src/powercontext/builtin/persistence/oceanbase/profile.py +++ b/src/powercontext/builtin/persistence/oceanbase/profile.py @@ -86,6 +86,7 @@ async def open( engine = create_async_engine( config.url.get_secret_value(), echo=config.echo, + hide_parameters=True, pool_pre_ping=config.pool_pre_ping, ) database = AsyncDatabase.own(engine) diff --git a/src/powercontext/builtin/persistence/seekdb/profile.py b/src/powercontext/builtin/persistence/seekdb/profile.py index 8c0238210..1a78b2298 100644 --- a/src/powercontext/builtin/persistence/seekdb/profile.py +++ b/src/powercontext/builtin/persistence/seekdb/profile.py @@ -181,6 +181,7 @@ def _create_engine(config: SeekDBConfig, connection_options: Mapping[str, object url, connect_args=options, echo=config.echo, + hide_parameters=True, pool_pre_ping=config.pool_pre_ping, ) diff --git a/src/powercontext/builtin/persistence/sqlite/profile.py b/src/powercontext/builtin/persistence/sqlite/profile.py index 3d8cfbbb3..40836c739 100644 --- a/src/powercontext/builtin/persistence/sqlite/profile.py +++ b/src/powercontext/builtin/persistence/sqlite/profile.py @@ -88,7 +88,7 @@ async def open( """Create, initialize and exclusively own one SQLite engine.""" _create_database_directory(config.url) - engine_options: dict[str, object] = {"echo": config.echo} + engine_options: dict[str, object] = {"echo": config.echo, "hide_parameters": True} if config.is_in_memory: engine_options["poolclass"] = StaticPool engine = create_async_engine(config.url, **engine_options) diff --git a/tests/builtin/persistence/test_oceanbase_profile.py b/tests/builtin/persistence/test_oceanbase_profile.py index cfd9b4bb0..e7a4d679b 100644 --- a/tests/builtin/persistence/test_oceanbase_profile.py +++ b/tests/builtin/persistence/test_oceanbase_profile.py @@ -127,6 +127,29 @@ async def scenario() -> None: asyncio.run(scenario()) +def test_oceanbase_profile_hides_sql_parameters(monkeypatch: pytest.MonkeyPatch) -> None: + async def scenario() -> None: + captured: dict[str, object] = {} + engine = _Engine() + + def create_engine(_url: object, **options: object) -> AsyncEngine: + captured.update(options) + return cast(AsyncEngine, engine) + + async def create_no_tables(_connection: object, _tables: tuple[Table, ...]) -> None: + return None + + monkeypatch.setattr(oceanbase_profile_module, "create_async_engine", create_engine) + monkeypatch.setattr(oceanbase_profile_module, "create_tables", create_no_tables) + + async with OceanBaseProfile.open(OceanBaseConfig(url=SecretStr(VALID_URL)), tables=()): + pass + + assert captured["hide_parameters"] is True + + asyncio.run(scenario()) + + @pytest.mark.parametrize("row", [None, ("ob_compatibility_mode", "ORACLE")]) def test_profile_requires_an_oceanbase_mysql_tenant( row: tuple[str, str] | None, diff --git a/tests/builtin/persistence/test_seekdb_profile.py b/tests/builtin/persistence/test_seekdb_profile.py index 1ff10f0e7..19f7aee6c 100644 --- a/tests/builtin/persistence/test_seekdb_profile.py +++ b/tests/builtin/persistence/test_seekdb_profile.py @@ -129,6 +129,7 @@ def create_engine(url: object, **options: object) -> AsyncEngine: "init_command": "SET autocommit = 0", "unix_socket": "seekdb.sock", } + assert captured["hide_parameters"] is True def test_profile_closes_engine_before_instance(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/tests/e2e/test_observability.py b/tests/e2e/test_observability.py index 08a8dbb9e..bbbfd3680 100644 --- a/tests/e2e/test_observability.py +++ b/tests/e2e/test_observability.py @@ -17,6 +17,7 @@ import asyncio import json import logging +import sqlite3 from pathlib import Path import httpx @@ -286,6 +287,49 @@ async def scenario() -> str: assert scope_id not in signal_payload +def test_database_failure_log_does_not_include_memory_content(caplog, tmp_path) -> None: + database_path = tmp_path / "failure-log.db" + app = create_server_app( + settings=ServerSettings( + database=SQLiteConfig(url=f"sqlite+aiosqlite:///{database_path}"), + mcp=McpConfig(enabled=False), + ) + ) + memory_content = "PROBE-SENSITIVE-MEMORY-1318" + + with TestClient(app, raise_server_exceptions=False) as client: + with sqlite3.connect(database_path) as connection: + connection.executescript(""" + CREATE TRIGGER reject_memory_insert + BEFORE INSERT ON pc_memory_entry_versions + BEGIN + SELECT RAISE(ABORT, 'forced persistence failure'); + END; + """) + caplog.clear() + with caplog.at_level(logging.ERROR, logger="powercontext.server.app"): + response = client.post( + "/v1/memory/remember", + json={"scope_id": "project:failure-log", "kind": "fact", "text": memory_content}, + ) + + records = [ + record for record in caplog.records if getattr(record, "event", None) == "application.operation.completed" + ] + assert response.status_code == 500 + assert len(records) == 1 + record = records[0] + assert record.operation == "remember_memory" + assert record.outcome == "failure" + assert record.error_code == "internal_error" + assert record.exc_info is not None + + formatter = logging.Formatter() + rendered_records = tuple(formatter.format(record) for record in caplog.records) + assert "forced persistence failure" in formatter.format(record) + assert all(memory_content not in rendered for rendered in rendered_records) + + def test_inference_spans_join_the_operation_trace_only_when_instrumented(monkeypatch, tmp_path) -> None: # Pydantic AI also resolves already-constructed models through `infer_model`, so pass those through. monkeypatch.setattr(