fix(security): F14 (Astra P1) — KI-Datenrichtlinie deckt JSON-Strings, Provider-Compliance und Tool-Antworten ab
Check Cross-Plugin Imports / check (push) Waiting to run
Check Cross-Plugin Imports / check (push) Waiting to run
Vorher (Astra): (1) enforce_data_policy filterte nur dict-Inhalte — ein JSON-String mit smtp_password passierte ungefiltert (Astra-Repro). (2) agent_runner rief die Policy mit db=None auf — Provider-Compliance (Datenresidenz/erlaubte Datenklassen) wurde NIE geladen. (3) Werkzeugantworten entstehen INNERHALB der ReAct-Schleife — die Policy lief nur davor, Tool-Ergebnisse erreichten den Provider ungefiltert. Fix: - data_policy.py: _filter_json_string_content — JSON-serialisierte Strings werden geparst, durch dieselbe dict-Filterung geleitet und zurueckserialisiert; Nicht-JSON-Strings bleiben unveraendert - agent_runner.py: echte DB-Session (Factory + Tenant-Kontext) statt db=None — Provider-Compliance wird tatsaechlich geladen - agent_loop.py: _filter_observation — jede Tool-Observation wird VOR dem Feed-Back in die LLM-Konversation durch die SENSITIVE_FIELDS-Filterung geleitet (JSON geparst, sensible Felder entfernt, zurueckserialisiert) Abnahme (Astra): Gesperrte Felder fehlen am Provider-Eingang sowohl im Startkontext (durch echte Compliance-Session) als auch nach Werkzeugaufrufen (Observation-Filter) — erfuellt. Verifikation: test_agent_loop + test_phase_f_agents 57 passed/3 skipped (dokumentierte F11-Verweise), Syntax + ruff clean.
This commit is contained in:
@@ -139,6 +139,48 @@ async def _execute_tool(
|
||||
return f"Error: {exc}"
|
||||
|
||||
|
||||
def _filter_observation(observation: str) -> str:
|
||||
"""F14 (Astra P1): sanitize a tool observation before it re-enters
|
||||
the LLM conversation.
|
||||
|
||||
Tool responses are raw data (CRM records, mail payloads, settings) and
|
||||
may contain sensitive fields (smtp_password, api keys, ...). The data
|
||||
policy runs BEFORE the loop — observations arise INSIDE it and used to
|
||||
reach the provider verbatim. Parse JSON observations and strip
|
||||
sensitive fields (same SENSITIVE_FIELDS set as the data policy).
|
||||
"""
|
||||
stripped = observation.strip()
|
||||
if not stripped.startswith(("{", "[")):
|
||||
return observation
|
||||
try:
|
||||
import json
|
||||
|
||||
parsed = json.loads(stripped)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return observation
|
||||
|
||||
from app.core.sensitive_data import SENSITIVE_FIELDS
|
||||
|
||||
sensitive_names: set[str] = set()
|
||||
for fields in SENSITIVE_FIELDS.values():
|
||||
sensitive_names |= fields
|
||||
|
||||
def _strip(data: Any) -> Any:
|
||||
if isinstance(data, dict):
|
||||
return {
|
||||
k: _strip(v)
|
||||
for k, v in data.items()
|
||||
if k not in sensitive_names
|
||||
}
|
||||
if isinstance(data, list):
|
||||
return [_strip(v) for v in data]
|
||||
return data
|
||||
|
||||
import json
|
||||
|
||||
return json.dumps(_strip(parsed))
|
||||
|
||||
|
||||
def _extract_allowed_tool_names(tools: list[dict[str, Any]] | None) -> set[str]:
|
||||
"""Extract the tool names actually offered to the LLM.
|
||||
|
||||
@@ -543,6 +585,9 @@ async def run_react_loop(
|
||||
observation = json.dumps({"error": f"Approval required but failed to create request: {e}"})
|
||||
else:
|
||||
observation = await _execute_tool(tool_registry, tool_name, args, tool_context)
|
||||
# F14 (Astra P1): tool responses are raw data — sanitize the
|
||||
# observation before it re-enters the LLM conversation.
|
||||
observation = _filter_observation(observation)
|
||||
observations.append(observation)
|
||||
|
||||
# Audit every tool call (real or simulated)
|
||||
|
||||
Reference in New Issue
Block a user