test(aiguard): cover multi-turn sensitive data redaction scenarios - #19871
test(aiguard): cover multi-turn sensitive data redaction scenarios#19871avara1986 wants to merge 2 commits into
Conversation
The redaction corpus and tests only exercised single-response shapes where the replacements were adjacent or targeted the last message, so the RFC's multi-turn requirements were untested: SDS scans the complete provider-bound context while attack analysis targets the latest message, so one response can carry replacements for any mix of historical and latest messages, and paths are request-local. Add an apply/conversation group to the corpus covering the RFC multi-turn example (non-adjacent messages[1] and messages[3] with an already-redacted assistant turn in between), sensitive data in every role, a history-only leak, several turns, a tool round trip kept in history, multimodal turns, and a partial failure on the latest message. Add three sequential tests for what a single response cannot express: history redacted again on every turn, paths never reused across turns, and per-turn reporting. Test only, no behaviour change. APPSEC-69387 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🎉 All green!🧪 All tests passed 🔗 Commit SHA: 3d4ceeb | Docs | View more details | Give us feedback! |
BenchmarksBenchmark execution time: 2026-08-26 13:21:21 Comparing candidate commit 2e2802d in PR branch Found 0 performance improvements and 8 performance regressions! Performance is the same for 576 metrics, 10 unstable metrics, 2 known flaky benchmarks, 16 flaky benchmarks without significant changes.
|
Codeowners resolved asResolved from the full PR diff against |
Circular import analysis
|
Dependency direction analysis
|
Description
Jira: APPSEC-69387 (epic APPSEC-68806)
RFC: AI Guard Sensitive Data Redaction (v1.2) and AI Guard Sensitive Data Redaction. Backend (v1.1)
Test-only change. No behaviour change, no production code touched.
AI Guard attack analysis targets the latest logical message, but SDS scans every model-visible string in the complete provider-bound context. Because request redaction is copy-on-write, the customer-owned list may still hold an original sensitive value from an earlier turn, so a single
/evaluateresponse can carryredaction_replacementsfor any mix of historical and latest messages, and every path is local to the array it was returned for.The existing corpus and tests only covered single-response shapes where the replacements were adjacent or targeted the last message, so these requirements were untested:
/evaluatecall.This PR closes those gaps. Everything passed against the current
ddtrace/aiguard/_redaction.pyunchanged — this is coverage for previously untested RFC requirements, not a fix.Corpus:
apply/conversation/*intests/aiguard/api/redaction_scenarios.json(73 → 80 cases)Each case is driven twice by the existing runners: once through
redact_messages, once end to end throughevaluate().apply/conversation/history-and-latest[system, user(original SSN), assistant("Your SSN is <REDACTED>."), user(new email)]with replacements onmessages[1]andmessages[3]. Asserts the already-redacted assistant turn and the system prompt are untouched.apply/conversation/every-roleapply/conversation/history-onlyapply/conversation/several-turnsapply/conversation/tool-turn-in-historyfunction.argumentsand therole:toolresult replaced alongside the latest message.apply/conversation/content-parts-across-turnsimage_url.urlnot.mixed/conversation/history-applied-latest-skippedSequential tests in
tests/aiguard/api/test_redaction.pyThe corpus is single-response by construction, so these cover what it cannot express:
test_history_is_redacted_again_on_every_turn— two consecutiveevaluate()calls on the same customer-owned list. Asserts the caller's list still holds the original SSN after turn 1 (copy-on-write), that turn 2's outgoing payload still carries the original history (the service needs the raw text to compute a replacement), and that turn 2's result and meta struct redact both the historical SSN and the new email.test_paths_are_not_reused_across_turns— the application rebuilds the context so the sensitive message moves to index 0 and a benign one takes over index 1, the index turn 1 redacted. Asserts index 1 is not touched. Regression guard for the RFC's "MUST NOT cache or reuse message indexes"; it holds today only becauseredact_messagesis stateless.test_redacted_is_reported_per_turn—ai_guard.redactedand theredactedtelemetry tag gotruethenfalseacross turns, so a redacting turn does not mark the next one.Testing
scripts/run-tests --venv f63a4f0(aiguard::ai_guard_api, Python 3.12): 184 passed. The 17 tests matching the new work (14 corpus runs + 3 sequential tests) were also run in isolation and pass.scripts/lint fmtandscripts/lint spellingare clean on both files.scripts/lint typingreports one pre-existingno-any-returnintests/aiguard/utils.py:27, which this PR does not touch.Risks
None. Test-only, additive: no existing case or assertion was modified, and no production code was touched.
Additional Notes
changelog/no-changelog.REDACT_MIXED(historical + latest atmessages[0]/messages[2]), but no non-adjacentmessages[1]+messages[3]case, no already-redacted-assistant-in-history shape, and no sequential turn test — its weblog endpoint takes one message list per POST. Portinghistory-and-latestandhistory-onlyover needs a cassette regeneration viautils/scripts/gen_redaction_cassettes.py.apply/…/skip/…with aredaction_replacementsfield; 25 there keyedREDACT_*/SKIP_*with areplacementsfield). Worth deciding whether one should be generated from the other to guarantee cross-language parity.🤖 Generated with Claude Code