Skip to content

AI Guard: tests for backend-provided extra_tags on the ai_guard span - #8130

Open
avara1986 wants to merge 7 commits into
mainfrom
avara/APPSEC-70724-ai-guard-extra-tags
Open

avara1986 wants to merge 7 commits into
mainfrom
avara/APPSEC-70724-ai-guard-extra-tags

Conversation

@avara1986

@avara1986 avara1986 commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Motivation

Jira: APPSEC-70724, AI Guard: Enrich traces with custom context from AI Guard policies.

AI Guard policies can add their own context to traces without a tracer release per tag. The evaluate response carries two optional attributes, both lists of {key, value} objects (dotted keys such as ai_guard.evaluate_id are not valid JSON:API member names, so they cannot be map keys):

What the backend sends today (the evaluation ID, as a span tag only; extra_metastruct is empty for now):

"extra_tags": [{ "key": "ai_guard.evaluate_id", "value": "<same UUID as data.id>" }]

Illustrative extra_metastruct shape tracers must implement (examples only, not sent today):

"extra_metastruct": [
  { "key": "policy_match", "value": { "rule": "credit_card", "categories": ["pii", "financial"] } },
  { "key": "matched_rules", "value": [{ "id": "r1", "action": "DENY" }, { "id": "r2", "action": "ALLOW" }] },
  { "key": "policy_version", "value": "v3" }
]
  • extra_tags: string values, each set as a tag on the ai_guard span under the key exactly as returned. Non-string values are skipped.
  • extra_metastruct: any non-null value (object, array or scalar), each added to the ai_guard meta_struct under the key exactly as returned.
  • The backend owns the ai_guard. prefix and the namespace. Tracers don't add the prefix and don't filter keys.
  • A malformed field or entry is skipped and never changes the evaluation.

Implementations:

Changes

  • Test_ExtraTags:
    • extra_tags entries land as string tags on the span
    • extra_metastruct entries (objects, arrays, scalars) land in the meta_struct
    • the evaluation ID lands only as the ai_guard.evaluate_id tag, matches data.id, and never reaches the meta_struct
    • keys land verbatim, even outside the ai_guard. namespace (no prefix added, no filtering)
    • both kinds are still present on the blocking path (DENY, 403)
    • nothing crosses over: a tag never reaches the meta_struct, a meta_struct value is never flattened into a tag
  • Test_ExtraTagsFailSafe: the action, the span (no error) and the response stay unchanged, while every well-formed part is still applied:
    • extra_tags sent as an object (the first draft's map shape) is skipped, extra_metastruct still lands
    • extra_metastruct sent as an object is skipped, extra_tags still lands
    • malformed entries (not an object, missing / non-string / empty key, non-string extra_tags value including a boolean, null extra_metastruct value) are skipped one by one, and a bad key is never stringified or kept
  • Fixtures: the cassettes are written by hand. utils/scripts/gen_extra_tags_cassettes.py generates them together with tests/ai_guard/extra_tags_scenarios.json, which also records what the tracer must apply for each scenario. It reuses the request hashing of gen_redaction_cassettes.py and refuses to overwrite another test's cassette. format.sh checks it for drift, the same way it checks the redaction fixtures.
  • Manifests:
    • Python flask-poc: enabled at v4.17.0-dev.
    • Java, Node.js, Ruby, .NET, Go, PHP, cpp_httpd, cpp_nginx: missing_feature (APPSEC-70724).

Testing

Ran the AI_GUARD scenario limited to Test_ExtraTags and Test_ExtraTagsFailSafe:

  • Python flask-poc with dd-trace-py main (4.17.0rc1, fd1721f1, includes the #21252 merge): all 8 new tests pass.
  • Node.js express5 with the dd-trace-js#10865 branch (c3f32458), forced with -F since the manifest still marks it missing_feature: all 8 new tests pass, and the whole AI_GUARD scenario (61 tests) passes.
  • Earlier, against dd-trace-py main with the map contract from #21145 only (ed54859b): all 8 new tests fail, the fail-safe ones included, so none passes vacuously.

DataDog/dd-trace-py#21252 is merged, so the Python dev CI runs these tests against the list contract. The Node.js manifest gets flipped once dd-trace-js#10865 is released.

Workflow

  1. ⚠️ Create your PR as draft ⚠️
  2. Work on you PR until the CI passes
  3. Mark it as ready for review
    • Tests, manifest, weblog are modified -> you'll need a review from system-tests-reviewers: ask to one of youre co-worker familiar with the tested feature.
    • Framework is modified, or non obvious usage of it -> get a review from system-tests-core (slack)

🚀 Once your PR is reviewed and the CI green, you can merge it!

🛟 #apm-shared-testing 🛟

🤖 Generated with Claude Code

The evaluate response can carry an extra_tags object so AI Guard policies
can enrich traces without a tracer release per tag (APPSEC-70724). Scalars
must land as ai_guard span tags under the key exactly as returned, and
structured values in the ai_guard meta struct; the backend owns the
ai_guard prefix, so tracers neither add it nor filter by namespace. A
malformed payload must be ignored without changing the evaluation.

Cassettes are hand-crafted because the backend does not return the field
yet; gen_extra_tags_cassettes.py generates them together with the scenario
sidecar and is checked by format.sh. Enabled for python flask-poc
(dd-trace-py#21145), missing_feature for every other tracer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

CODEOWNERS have been resolved as:

tests/ai_guard/extra_tags_scenarios.json                                @DataDog/k9-ai-guard
utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_19cfc9ce.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_28917f95.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_2b336647.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_354106da.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_827d1449.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_bf6868f9.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_e2c13e67.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/build/docker/vcr/cassettes/aiguard/aiguard_evaluate_post_f0ef2bd8.json  @DataDog/k9-ai-guard @DataDog/system-tests-core
utils/scripts/gen_extra_tags_cassettes.py                               @DataDog/system-tests-core
format.sh                                                               @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/test_ai_guard_sdk.py                                     @DataDog/k9-ai-guard

@datadog-official

datadog-official Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Pipelines  Tests

✨ Unblock PR with BitsAI

❌ Errors

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

🚦 8 Pipeline jobs failed

Testing the test | System Tests (php, dev) / End-to-end #1 / apache-mod-7.0-zts 1 — ❌ 1 test failed · 🔧 Needs a code fix, caused by this PR

View more details · View in GitHub Actions

❌ tests.debugger.test_debugger_expression_language.Test_Debugger_Expression_Language.test_expression_language_access_variables[apache-mod-7.0-zts] from system_tests_suite
AssertionError: assert &#39;Probes did not reach INSTALLED status&#39; is None
 &#43;  where &#39;Probes did not reach INSTALLED status&#39; = &lt;built-in method join of str object at 0x7f8d4242b720&gt;([&#39;Probes did not reach INSTALLED status&#39;])
 &#43;    where &lt;built-in method join of str object at 0x7f8d4242b720&gt; = &#39;\n&#39;.join
 &#43;    and   [&#39;Probes did not reach INSTALLED status&#39;] = &lt;tests.debugger.test_debugger_expression_language.Test_Debugger_Expression_Language object at 0x7f8d2bb63c20&gt;.setup_failures

self = &lt;tests.debugger.test_debugger_expression_language.Test_Debugger_Expression_Language object at 0x7f8d2bb63c20&gt;

    def test_expression_language_access_variables(self):
&gt;       self._assert(expected_response=200)

...
Testing the test | System Tests (php, dev) / End-to-end #1 / apache-mod-7.1-zts 1 — ❌ 1 test failed · 🔧 Needs a code fix, caused by this PR

View more details · View in GitHub Actions

❌ tests.debugger.test_debugger_expression_language.Test_Debugger_Expression_Language.test_expression_language_access_variables[apache-mod-7.1-zts] from system_tests_suite
AssertionError: assert &#39;Probes did not reach INSTALLED status&#39; is None
 &#43;  where &#39;Probes did not reach INSTALLED status&#39; = &lt;built-in method join of str object at 0x7fa8cec2b720&gt;([&#39;Probes did not reach INSTALLED status&#39;])
 &#43;    where &lt;built-in method join of str object at 0x7fa8cec2b720&gt; = &#39;\n&#39;.join
 &#43;    and   [&#39;Probes did not reach INSTALLED status&#39;] = &lt;tests.debugger.test_debugger_expression_language.Test_Debugger_Expression_Language object at 0x7fa8c4f0b7a0&gt;.setup_failures

self = &lt;tests.debugger.test_debugger_expression_language.Test_Debugger_Expression_Language object at 0x7fa8c4f0b7a0&gt;

    def test_expression_language_access_variables(self):
&gt;       self._assert(expected_response=200)

...
Testing the test | System Tests (php, dev) / End-to-end #1 / apache-mod-7.2-zts 1 — ❌ 3 tests failed · 🔧 Needs a code fix, caused by this PR

View more details · View in GitHub Actions

❌ tests.debugger.test_debugger_capture_expressions.Test_Debugger_Method_Capture_Expressions.test_method_parameter_capture_expressions[apache-mod-7.2-zts] from system_tests_suite
AssertionError: assert &#39;Probes did not reach INSTALLED status&#39; is None
 &#43;  where &#39;Probes did not reach INSTALLED status&#39; = &lt;built-in method join of str object at 0x7f205ca2b720&gt;([&#39;Probes did not reach INSTALLED status&#39;])
 &#43;    where &lt;built-in method join of str object at 0x7f205ca2b720&gt; = &#39;\n&#39;.join
 &#43;    and   [&#39;Probes did not reach INSTALLED status&#39;] = &lt;tests.debugger.test_debugger_capture_expressions.Test_Debugger_Method_Capture_Expressions object at 0x7f204e2c27b0&gt;.setup_failures

self = &lt;tests.debugger.test_debugger_capture_expressions.Test_Debugger_Method_Capture_Expressions object at 0x7f204e2c27b0&gt;

    def test_method_parameter_capture_expressions(self):
&gt;       self._assert()

...
❌ tests.debugger.test_debugger_expression_language.Test_Debugger_Expression_Language.test_expression_language_access_variables[apache-mod-7.2-zts] from system_tests_suite
AssertionError: assert &#39;Probes did not reach INSTALLED status&#39; is None
 &#43;  where &#39;Probes did not reach INSTALLED status&#39; = &lt;built-in method join of str object at 0x7f36ee22b720&gt;([&#39;Probes did not reach INSTALLED status&#39;])
 &#43;    where &lt;built-in method join of str object at 0x7f36ee22b720&gt; = &#39;\n&#39;.join
 &#43;    and   [&#39;Probes did not reach INSTALLED status&#39;] = &lt;tests.debugger.test_debugger_expression_language.Test_Debugger_Expression_Language object at 0x7f36e462b830&gt;.setup_failures

self = &lt;tests.debugger.test_debugger_expression_language.Test_Debugger_Expression_Language object at 0x7f36e462b830&gt;

    def test_expression_language_access_variables(self):
&gt;       self._assert(expected_response=200)

...
❌ tests.debugger.test_debugger_probe_snapshot.Test_Debugger_Method_Probe_Snaphots_With_SCM.test_log_method_snapshot[apache-mod-7.2-zts] from system_tests_suite
AssertionError: assert &#39;Probes did not reach INSTALLED status&#39; is None
 &#43;  where &#39;Probes did not reach INSTALLED status&#39; = &lt;built-in method join of str object at 0x7fecd242b720&gt;([&#39;Probes did not reach INSTALLED status&#39;])
 &#43;    where &lt;built-in method join of str object at 0x7fecd242b720&gt; = &#39;\n&#39;.join
 &#43;    and   [&#39;Probes did not reach INSTALLED status&#39;] = &lt;tests.debugger.test_debugger_probe_snapshot.Test_Debugger_Method_Probe_Snaphots_With_SCM object at 0x7fecbb954560&gt;.setup_failures

self = &lt;tests.debugger.test_debugger_probe_snapshot.Test_Debugger_Method_Probe_Snaphots_With_SCM object at 0x7fecbb954560&gt;

    def test_log_method_snapshot(self):
&gt;       self._assert()

...

View all 8 failed jobs.

ℹ️ Info

No other issues found (see more)

❄️ No new flaky tests detected

Useful? React with 👍 / 👎

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

@avara1986
avara1986 marked this pull request as ready for review October 22, 2026 09:41
@avara1986
avara1986 requested review from a team as code owners October 22, 2026 09:41
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-22T09:42:07.280389Z 1aafd54 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Comment thread tests/ai_guard/test_ai_guard_sdk.py Outdated
avara1986 and others added 2 commits October 22, 2026 16:01
A tracer without extra_tags support also ignores a malformed payload, so
Test_ExtraTagsFailSafe XPASSes under missing_feature (verified on Node.js)
and the easy-win bot would wrongly declare the feature supported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Span metrics decode as float64, so a tracer that stores a boolean extra tag
as a metric reports 1.0/0.0, which the type guard rejected. Reuse the
redacted-tag check, which accepts exactly 1/0 from metrics, for both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
avara1986 and others added 2 commits October 23, 2026 09:07
The backend contract for APPSEC-70724 changed: the evaluate response
carries two separate attributes, both lists of {key, value} objects,
since dotted keys such as ai_guard.evaluate_id are not valid JSON:API
member names.

- extra_tags: string values, set as ai_guard span tags under the key
  exactly as returned
- extra_metastruct: any value, added to the ai_guard meta_struct under
  the key exactly as returned
- the evaluation ID is sent in both, as ai_guard.evaluate_id and
  evaluate_id, matching data.id

Regenerate the hand-written cassettes for the list shape, with a stable
UUID as data.id per scenario, and rewrite Test_ExtraTags and
Test_ExtraTagsFailSafe accordingly. The fail-safe tests now also assert
that the well-formed part of the response is still applied, so they no
longer pass vacuously and the easy-win skip entries are dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ull skips

The backend now sends the evaluation ID only as the ai_guard.evaluate_id
span tag; extra_metastruct is empty for now (ddoghq/dd-source#139566).

- EVALUATE_ID mirrors today's response: ai_guard.evaluate_id in
  extra_tags, equal to data.id, and no extra_metastruct. The test also
  asserts the ID never reaches the ai_guard meta_struct.
- ON_DENY no longer sends evaluate_id in extra_metastruct.
- EXTRA_METASTRUCT uses the illustrative backend example (objects,
  arrays of objects, a scalar).
- MALFORMED_ENTRIES adds a boolean extra_tags value and a null
  extra_metastruct value, and checks that a non-string or empty key is
  never stringified or kept.

Regenerated with utils/scripts/gen_extra_tags_cassettes.py; cassette
names are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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