fix(security): F14 (Astra P1) — KI-Datenrichtlinie deckt JSON-Strings, Provider-Compliance und Tool-Antworten ab
Check Cross-Plugin Imports / check (push) Has been cancelled

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:
Agent Zero
2026-09-18 12:04:58 +02:00
parent c9a5a6e198
commit fdc4e36d14
3 changed files with 106 additions and 6 deletions
+46
View File
@@ -82,6 +82,14 @@ async def enforce_data_policy(
else c
for c in content
]
elif isinstance(content, str):
# F14 (Astra P1): JSON-serialized strings passed through
# UNFILTERED before — a payload like '{"smtp_password": ...}'
# reached the provider verbatim. Parse, filter, re-serialize;
# non-JSON strings stay unchanged (plain prose is fine).
content = _filter_json_string_content(
content, metadata, compliance, agent_definition
)
new_msg = dict(msg)
new_msg["content"] = content
filtered.append(new_msg)
@@ -89,6 +97,44 @@ async def enforce_data_policy(
return filtered
def _filter_json_string_content(
content: str,
metadata: AIUseCaseMetadata,
compliance: dict[str, Any] | None,
agent_definition: Any,
) -> str:
"""Filter a JSON-serialized string payload (F14).
Tries to parse the string as a JSON object/array and runs the SAME
dict-level filtering on it. Non-JSON strings are returned unchanged.
"""
stripped = content.strip()
if not stripped.startswith(("{", "[")):
return content
try:
import json
parsed = json.loads(stripped)
except (json.JSONDecodeError, ValueError):
return content # not JSON — plain string content is not a leak vector
if isinstance(parsed, dict):
filtered = _filter_dict_content(parsed, metadata, compliance, agent_definition)
import json
return json.dumps(filtered)
if isinstance(parsed, list):
filtered = [
_filter_dict_content(item, metadata, compliance, agent_definition)
if isinstance(item, dict)
else item
for item in parsed
]
import json
return json.dumps(filtered)
return content
def _filter_dict_content(
data: dict[str, Any],
metadata: AIUseCaseMetadata,