Skip to content

AI Guard: redaction tests for the full provider-bound context (multi-turn history) - #7595

Merged
cbeauchesne merged 5 commits into
mainfrom
avara/APPSEC-68867-ai-guard-redaction-multi-turn
Aug 28, 2026
Merged

AI Guard: redaction tests for the full provider-bound context (multi-turn history)#7595
cbeauchesne merged 5 commits into
mainfrom
avara/APPSEC-68867-ai-guard-redaction-multi-turn

Conversation

@avara1986

@avara1986 avara1986 commented Aug 26, 2026

Copy link
Copy Markdown
Member

Motivation

Jira: APPSEC-68867
RFCs: RFC AI Guard Sensitive Data Redaction (v1.2) and RFC AI Guard Sensitive Data Redaction. Backend (v1.1)
Follow-up to #7385, which introduced the redaction corpus.

Both RFCs were revised to make one point normative:

AI Guard attack analysis evaluates the latest logical message. SDS sensitive-data scanning MUST inspect every model-visible string in every message included in the current /evaluate request. redaction_replacements MUST therefore contain an entry for every path in the current provider-bound context that SDS mutates, not only paths belonging to the last message.

This matters because redaction is copy-on-write: the tracer sends a redacted copy to the provider and leaves the caller's list alone, so an earlier message still holds its original value on the next turn.

The gap

Of the 24 redaction scenarios on main, only REDACT_MIXED redacted a non-last message alongside the latest one, and REDACT_SYSTEM_PROMPT redacted a two-message system prompt. A tracer that redacted only the latest message passed 22 of 24 and would still ship the entire conversation history to the provider on turn 2 — the exact failure the RFC revision was written to prevent.

What this PR adds

Test_RedactionMultiTurnContext (9 tests) and 9 scenarios in gen_redaction_cassettes.py:

Scenario What it pins down
REDACT_TURN_1 + REDACT_HISTORY_AND_LATEST The RFC multi-turn example verbatim: historical SSN at messages[1] plus a new email at messages[3], with the already-redacted assistant message at messages[2] surviving byte for byte (no double redaction)
REDACT_HISTORY_ONLY Sensitive data only in the history, latest message benign — the case a last-message-only tracer cannot fake
REDACT_EVERY_ROLE_IN_HISTORY Five replacements in one call: system, historical user, tool-call arguments, tool result, latest user. Also asserts the redacted arguments still parse as JSON
REDACT_HISTORICAL_TOOL_CALL Tool call and result several turns back, latest message benign
REDACT_HISTORY_CONTENT_PART messages[1].content[0].text in a historical multimodal message
REDACT_DEEP_HISTORY Non-contiguous replacements at indices 1/3/5/7 of an eight-message conversation, each with benign neighbours so an off-by-one shows up as a mismatch
REDACT_SAME_VALUE_ACROSS_TURNS One value restated in a later turn: one entry per path, each applied on its own
REDACT_REORDERED_CONTEXT Drives test_redact_paths_are_request_local, which replays turn 1 → turn 2 → reordered turn 2 across three calls. The reorder swaps the strings behind messages[1]/messages[3], so a tracer reusing the previous response's paths writes the SSN replacement over the email message and fails

Plus test_redacted_history_in_sdk_response, asserting the list handed back to the caller — and therefore sent to the provider — has no history left. That is the guarantee redaction exists for.

Keeping fixtures in sync

Unchanged from #7385: scenarios live in SCENARIOS in utils/scripts/gen_redaction_cassettes.py, which writes both the cassettes and tests/ai_guard/redaction_scenarios.json, and cross-checks every declared expect_redacted/expect_removed against its reference implementation of the RFC algorithm. ./format.sh runs it, so the fixtures in the repo are always the fixtures the script produces.

Test results

  • ./run.sh AI_GUARD tests/ai_guard/test_ai_guard_sdk.py::Test_RedactionMultiTurnContext9 passed against python@4.14.0-rc1. Python's redact_messages is index- and cardinality-agnostic, so it passes unchanged — the new tests document a contract it already satisfies rather than asking for new work.
  • Full AI Guard suite → 49 passed, 2 xfailed (the 2 are the pre-existing Test_RedactionInSDKResponse gap).
  • TEST_THE_TEST437 passed.
  • ./format.sh → mypy, ruff, yamlfmt, yamllint, parser checks and the redaction-fixture drift check all clean.

Notes for reviewers

  • Test_RedactionMultiTurnContext is activated for python (flask-poc: v4.14.0-dev) and declared at the existing redaction ref for nodejs (express4: *ref_6_13_0); missing_feature for cpp_httpd, cpp_nginx, dotnet, golang, java, php, ruby.
  • It is a separate class rather than more methods on Test_Redaction on purpose: full-context scope is its own normative concern, so a tracer that implements last-message redaction first can be declared separately.
  • docs/understand/scenarios/ai_guard.md gains a subsection explaining the scope distinction and why the class exists.

🤖 Generated with Claude Code

…message

APPSEC-68867

Backend RFC v1.1 and RFC v1.2 make the scope distinction normative: attack
analysis targets the latest logical message, but SDS scans every model-visible
string in the messages array of the current /evaluate call, so
redaction_replacements may target the system prompt and historical user,
assistant and tool messages as well as the latest one.

Only REDACT_MIXED redacted a non-last message alongside the latest, so a tracer
that redacted only the latest message passed 22 of the 24 existing scenarios
while still shipping the whole history to the provider on turn 2.

Adds Test_RedactionMultiTurnContext and nine generator scenarios: the RFC
multi-turn example (historical SSN plus a new email, with an already redacted
assistant message that must survive byte for byte), history-only redaction with
a benign latest message, one replacement per role in a single call, historical
tool call/result and content part, non-contiguous replacements in an
eight-message conversation, a value restated across turns, and request-local
paths replayed over three calls that grow then reorder the conversation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

CODEOWNERS have been resolved as:

utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_0acfa129.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_1c682a42.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_2fb44ba7.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_4f716ee4.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_5302857d.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_55c3d321.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_5784ecb9.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_942c4dec.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_cd60ada5.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
manifests/cpp_httpd.yml                                                 @DataDog/system-tests-reviewers
manifests/cpp_nginx.yml                                                 @DataDog/system-tests-reviewers
manifests/dotnet.yml                                                    @DataDog/system-tests-reviewers
manifests/golang.yml                                                    @DataDog/system-tests-reviewers
manifests/java.yml                                                      @DataDog/system-tests-reviewers
manifests/nodejs.yml                                                    @DataDog/system-tests-reviewers
manifests/php.yml                                                       @DataDog/system-tests-reviewers
manifests/python.yml                                                    @DataDog/system-tests-reviewers
manifests/ruby.yml                                                      @DataDog/system-tests-reviewers
tests/ai_guard/redaction_scenarios.json                                 @DataDog/k9-ai-guard @DataDog/system-tests-core
tests/ai_guard/test_ai_guard_sdk.py                                     @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/scripts/gen_redaction_cassettes.py                                @DataDog/system-tests-core

@datadog-prod-us1-6

datadog-prod-us1-6 Bot commented Aug 26, 2026

Copy link
Copy Markdown

Pipelines  Tests

⚠️ Warnings

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 1 Pipeline job failed

Testing the test | all-jobs-are-green

View more details · View in GitHub Actions

Socket hang up while attempting to check commit statuses from GitHub API.

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

🔄 Datadog auto-retried 2 jobs - 1 passed on retry View in Datadog

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 4c37572 | Docs | View more details | Give us feedback!

@avara1986
avara1986 marked this pull request as ready for review August 27, 2026 06:39
@avara1986
avara1986 requested review from a team as code owners August 27, 2026 06:39
@avara1986
avara1986 requested review from Leiyks and removed request for a team August 27, 2026 06:39

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c0b0b6518b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread utils/scripts/gen_redaction_cassettes.py
Comment thread tests/ai_guard/test_ai_guard_sdk.py
Comment thread docs/understand/scenarios/ai_guard.md Outdated
Comment thread tests/ai_guard/test_ai_guard_sdk.py
Consolidate assertion logic duplicated across the redaction tests, with no
change to what any test asserts.

- Extract _assert_sdk_response_redacted: Test_RedactedMessagesInSDKResponse
  and Test_RedactionMultiTurnContext carried a byte-for-byte copy of the same
  "messages are the redacted ones and hold no sensitive value" block.
- Extract _assert_tool_arguments_still_parse, shared by the two tests that
  check a redacted tool call keeps parseable arguments. It selects the
  .arguments replacement from the corpus, replacing a hardcoded path literal
  in test_redact_every_role_in_history that duplicated corpus data and would
  have drifted silently if the scenario were reindexed.
- Dedupe the declared sensitive values in the generator. removed/retained are
  substring searches over the whole payload, so a value declared at two paths
  only repeated the identical assertion; REDACT_SAME_VALUE_ACROSS_TURNS was
  the only scenario affected.

Cassettes are byte-identical and the test-ID inventory is unchanged, so no
manifest entry is affected.
…-write claim

Address review feedback on #7595.

Historical assistant content was the one model-visible surface the corpus
never redacted: the only assistant content replacement was
REDACT_ASSISTANT_RESPONSE, where the assistant reply is the latest message.
A tracer redacting the latest assistant output and the other historical roles
but skipping historical assistant content passed the whole suite while still
leaking it to the provider. The historical assistant message in
REDACT_EVERY_ROLE_IN_HISTORY now carries content with its own SSN alongside
its tool_calls, so one message exercises both surfaces.

Scope the class docstring to what the tests actually assert. Every weblog
deserializes a fresh message list per request and returns the evaluation
rather than the caller's list, so an SDK mutating that list in place is
invisible here. The suite verifies what the tracer sends and reports per
call; the non-mutation half of copy-on-write would need a weblog endpoint
holding one list across turns and echoing the original back.

Drop the ai_guard.md subsection: it restated the class docstring and
enumerated the class's test methods, which the docstrings already carry in
context. The file is now identical to main.
@avara1986
avara1986 enabled auto-merge (squash) August 27, 2026 15:40
@cbeauchesne
cbeauchesne disabled auto-merge August 28, 2026 13:30
@cbeauchesne
cbeauchesne merged commit 54fd66d into main Aug 28, 2026
3553 of 3558 checks passed
@cbeauchesne
cbeauchesne deleted the avara/APPSEC-68867-ai-guard-redaction-multi-turn branch August 28, 2026 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants