dd-trace 6.12.0 → 6.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/ci/test-optimization-validation/framework-adapters/cypress.js +9 -11
  2. package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
  3. package/index.d.ts +127 -26
  4. package/package.json +10 -10
  5. package/packages/datadog-instrumentations/src/bunyan.js +4 -2
  6. package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
  7. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +1 -0
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +28 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  10. package/packages/datadog-instrumentations/src/jest.js +140 -18
  11. package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
  12. package/packages/datadog-instrumentations/src/mariadb.js +7 -0
  13. package/packages/datadog-instrumentations/src/pino.js +15 -4
  14. package/packages/datadog-instrumentations/src/playwright.js +34 -20
  15. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +7 -4
  16. package/packages/datadog-instrumentations/src/vitest-worker.js +11 -0
  17. package/packages/datadog-instrumentations/src/webdriverio.js +26 -0
  18. package/packages/datadog-instrumentations/src/winston.js +6 -3
  19. package/packages/datadog-plugin-cypress/src/support.js +1 -0
  20. package/packages/datadog-plugin-http/src/client.js +2 -15
  21. package/packages/datadog-plugin-http2/src/client.js +2 -15
  22. package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
  23. package/packages/datadog-plugin-playwright/src/index.js +9 -1
  24. package/packages/datadog-plugin-undici/src/index.js +2 -15
  25. package/packages/dd-trace/src/aiguard/evaluation.js +94 -20
  26. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
  27. package/packages/dd-trace/src/aiguard/messages/anthropic.js +21 -9
  28. package/packages/dd-trace/src/aiguard/noop.js +9 -1
  29. package/packages/dd-trace/src/aiguard/redaction.js +183 -0
  30. package/packages/dd-trace/src/aiguard/sdk.js +7 -2
  31. package/packages/dd-trace/src/aiguard/tags.js +2 -0
  32. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
  33. package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
  34. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +51 -26
  35. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -11
  36. package/packages/dd-trace/src/ci-visibility/final-flush.js +20 -0
  37. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +196 -29
  38. package/packages/dd-trace/src/config/generated-config-types.d.ts +6 -0
  39. package/packages/dd-trace/src/config/index.js +2 -1
  40. package/packages/dd-trace/src/config/supported-configurations.json +27 -0
  41. package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -3
  42. package/packages/dd-trace/src/dogstatsd.js +45 -18
  43. package/packages/dd-trace/src/encode/agentless-json.js +5 -4
  44. package/packages/dd-trace/src/exporters/agent/index.js +22 -2
  45. package/packages/dd-trace/src/exporters/agent/writer.js +4 -4
  46. package/packages/dd-trace/src/exporters/common/writer.js +24 -2
  47. package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
  48. package/packages/dd-trace/src/external-logger/src/index.js +4 -3
  49. package/packages/dd-trace/src/flush.js +102 -0
  50. package/packages/dd-trace/src/llmobs/experiments/client.js +4 -0
  51. package/packages/dd-trace/src/llmobs/experiments/dataset.js +51 -2
  52. package/packages/dd-trace/src/llmobs/experiments/experiment.js +420 -105
  53. package/packages/dd-trace/src/llmobs/experiments/index.js +54 -42
  54. package/packages/dd-trace/src/llmobs/experiments/noop.js +39 -8
  55. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  56. package/packages/dd-trace/src/llmobs/experiments/util.js +21 -0
  57. package/packages/dd-trace/src/llmobs/index.js +80 -17
  58. package/packages/dd-trace/src/llmobs/noop.js +2 -1
  59. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
  60. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
  61. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
  62. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
  63. package/packages/dd-trace/src/llmobs/writers/base.js +60 -24
  64. package/packages/dd-trace/src/noop/span.js +5 -0
  65. package/packages/dd-trace/src/openfeature/writers/exposures.js +8 -1
  66. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
  67. package/packages/dd-trace/src/opentelemetry/logs/index.js +4 -1
  68. package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
  69. package/packages/dd-trace/src/opentelemetry/metrics/index.js +3 -1
  70. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
  71. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +4 -2
  72. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +4 -2
  73. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
  74. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
  75. package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
  76. package/packages/dd-trace/src/opentracing/propagation/text_map.js +410 -342
  77. package/packages/dd-trace/src/opentracing/span.js +28 -0
  78. package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
  79. package/packages/dd-trace/src/plugin_manager.js +2 -0
  80. package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
  81. package/packages/dd-trace/src/plugins/util/test.js +74 -21
  82. package/packages/dd-trace/src/plugins/util/web.js +2 -65
  83. package/packages/dd-trace/src/profiling/oom.js +4 -3
  84. package/packages/dd-trace/src/profiling/profiler.js +7 -0
  85. package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
  86. package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
  87. package/packages/dd-trace/src/proxy.js +36 -8
  88. package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
  89. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -0
  90. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +17 -6
  91. package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +55 -0
  92. package/packages/dd-trace/src/serverless/vercel.js +122 -0
  93. package/packages/dd-trace/src/serverless.js +37 -20
  94. package/packages/dd-trace/src/span_stats.js +40 -3
  95. package/packages/dd-trace/src/tracer.js +22 -0
  96. package/packages/dd-trace/src/web-tags-cache.js +67 -13
  97. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
@@ -8,11 +8,14 @@ const { keepTrace } = require('../priority_sampler')
8
8
  const { extractIp } = require('../plugins/util/ip_extractor')
9
9
  const { AI_GUARD } = require('../standalone/product')
10
10
  const telemetryMetrics = require('../telemetry/metrics')
11
+ const { normalizeRedactionReplacements, redactMessages } = require('./redaction')
11
12
  const TAGS = require('./tags')
12
13
 
13
14
  const ALLOW = 'ALLOW'
14
15
 
16
+ /** @typedef {import('../../../../index').aiguard.ContentPart} ContentPart */
15
17
  /** @typedef {import('../../../../index').aiguard.Message} Message */
18
+ /** @typedef {import('../../../../index').aiguard.RedactionReplacement} RedactionReplacement */
16
19
  /** @typedef {import('../opentracing/span')} Span */
17
20
 
18
21
  /**
@@ -24,19 +27,22 @@ const ALLOW = 'ALLOW'
24
27
  * @property {Record<string, unknown>} tagProbabilities
25
28
  * @property {boolean} hasTagProbabilities
26
29
  * @property {boolean} blockingEnabled
30
+ * @property {unknown} redactionReplacements
27
31
  */
28
32
 
29
33
  /**
30
34
  * @typedef {object} EvaluationOutcome
31
35
  * @property {{ action: string, reason: string|undefined, tags: Array<unknown>,
32
- * tagProbabilities: Record<string, unknown>, sds: Array<unknown> }} result
36
+ * tagProbabilities: Record<string, unknown>, sds: Array<unknown>, messages: Message[],
37
+ * redactionReplacements: RedactionReplacement[] }} result
33
38
  * @property {boolean} shouldBlock
34
39
  * @property {boolean} hasTagProbabilities
40
+ * @property {{ enabled: boolean, applied: boolean, failures: number }} redaction
35
41
  */
36
42
 
37
43
  /**
38
44
  * @typedef {object} EvaluationMetaStruct
39
- * @property {Message[]} messages
45
+ * @property {Message[]} [messages]
40
46
  * @property {Array<unknown>} [attack_categories]
41
47
  * @property {Array<unknown>} [sds]
42
48
  * @property {Record<string, unknown>} [tag_probs]
@@ -45,6 +51,7 @@ const ALLOW = 'ALLOW'
45
51
  /**
46
52
  * @typedef {object} EvaluationReport
47
53
  * @property {Span} span
54
+ * @property {Message[]} messages
48
55
  * @property {EvaluationMetaStruct} metaStruct
49
56
  * @property {{ source: string, integration: string }} telemetryTags
50
57
  */
@@ -79,6 +86,7 @@ function parseEvaluationResponse (body) {
79
86
  tagProbabilities: isRecord(attributes.tag_probs) ? attributes.tag_probs : {},
80
87
  hasTagProbabilities: isRecord(attributes.tag_probs),
81
88
  blockingEnabled: attributes.is_blocking_enabled === true,
89
+ redactionReplacements: attributes.redaction_replacements,
82
90
  }
83
91
  }
84
92
 
@@ -96,11 +104,16 @@ function shouldBlockEvaluation (block, evaluation) {
96
104
  /**
97
105
  * Applies local evaluation policy and creates the result returned to the caller.
98
106
  *
107
+ * @param {Message[]} messages
99
108
  * @param {EvaluationResponse} evaluation
100
- * @param {boolean} block
109
+ * @param {{ block: boolean, redactionEnabled: boolean }} options
101
110
  * @returns {EvaluationOutcome}
102
111
  */
103
- function createEvaluationOutcome (evaluation, block) {
112
+ function createEvaluationOutcome (messages, evaluation, options) {
113
+ const redaction = options.redactionEnabled
114
+ ? redactMessages(messages, evaluation.redactionReplacements)
115
+ : { messages, redacted: false, failures: 0 }
116
+
104
117
  return {
105
118
  result: {
106
119
  action: evaluation.action,
@@ -108,9 +121,16 @@ function createEvaluationOutcome (evaluation, block) {
108
121
  tags: evaluation.tags,
109
122
  tagProbabilities: evaluation.tagProbabilities,
110
123
  sds: evaluation.sdsFindings,
124
+ messages: redaction.messages,
125
+ redactionReplacements: normalizeRedactionReplacements(evaluation.redactionReplacements),
111
126
  },
112
- shouldBlock: shouldBlockEvaluation(block, evaluation),
127
+ shouldBlock: shouldBlockEvaluation(options.block, evaluation),
113
128
  hasTagProbabilities: evaluation.hasTagProbabilities,
129
+ redaction: {
130
+ enabled: options.redactionEnabled,
131
+ applied: redaction.redacted,
132
+ failures: redaction.failures,
133
+ },
114
134
  }
115
135
  }
116
136
 
@@ -130,6 +150,7 @@ class EvaluationReporter {
130
150
  #config
131
151
  #maxMessagesLength
132
152
  #maxContentSize
153
+ #redactionEnabled
133
154
 
134
155
  /**
135
156
  * @param {import('../config/config-base')} config
@@ -138,6 +159,7 @@ class EvaluationReporter {
138
159
  this.#config = config
139
160
  this.#maxMessagesLength = config.experimental.aiguard.maxMessagesLength
140
161
  this.#maxContentSize = config.experimental.aiguard.maxContentSize
162
+ this.#redactionEnabled = config.experimental.aiguard.redactionEnabled
141
163
  }
142
164
 
143
165
  /**
@@ -150,19 +172,18 @@ class EvaluationReporter {
150
172
  */
151
173
  start (span, messages, options) {
152
174
  const telemetryTags = { source: options.source, integration: options.integration }
153
- const last = messages.at(-1)
175
+ const evaluatedMessages = this.#redactionEnabled ? clone(messages) : messages
176
+ const last = evaluatedMessages.at(-1)
154
177
  const target = this.#isToolCall(last) ? 'tool' : 'prompt'
155
178
  span.setTag(TAGS.TARGET_TAG_KEY, target)
156
179
  if (target === 'tool') {
157
- const name = this.#getToolName(last, messages)
180
+ const name = this.#getToolName(last, evaluatedMessages)
158
181
  if (name) {
159
182
  span.setTag(TAGS.TOOL_NAME_TAG_KEY, name)
160
183
  }
161
184
  }
162
185
 
163
- const metaStruct = {
164
- messages: this.#buildMessagesForMetaStruct(messages, telemetryTags),
165
- }
186
+ const metaStruct = {}
166
187
  span.meta_struct = {
167
188
  [TAGS.META_STRUCT_KEY]: metaStruct,
168
189
  }
@@ -178,6 +199,7 @@ class EvaluationReporter {
178
199
 
179
200
  return {
180
201
  span,
202
+ messages: evaluatedMessages,
181
203
  metaStruct,
182
204
  telemetryTags,
183
205
  }
@@ -191,6 +213,7 @@ class EvaluationReporter {
191
213
  * @returns {void}
192
214
  */
193
215
  fail (report, errorType) {
216
+ report.metaStruct.messages = this.#buildMessagesForMetaStruct(report.messages, report.telemetryTags)
194
217
  aiguardMetrics.count(TAGS.TELEMETRY_REQUESTS, { error: true, ...report.telemetryTags }).inc(1)
195
218
  aiguardMetrics.count(TAGS.TELEMETRY_ERROR, { type: errorType, ...report.telemetryTags }).inc(1)
196
219
  }
@@ -203,18 +226,38 @@ class EvaluationReporter {
203
226
  * @returns {void}
204
227
  */
205
228
  finish (report, outcome) {
206
- const { result, shouldBlock } = outcome
229
+ const { result, redaction, shouldBlock } = outcome
207
230
 
208
231
  if (result.tags.length > 0) report.metaStruct.attack_categories = result.tags
209
232
  if (result.sds.length > 0) report.metaStruct.sds = result.sds
210
233
  if (outcome.hasTagProbabilities) report.metaStruct.tag_probs = result.tagProbabilities
211
234
 
212
- const requestTelemetryTags = {
213
- action: result.action,
214
- error: false,
215
- block: shouldBlock,
216
- ...report.telemetryTags,
235
+ if (redaction.enabled) {
236
+ report.span.setTag(TAGS.REDACTED_TAG_KEY, redaction.applied ? 'true' : 'false')
237
+ if (redaction.failures > 0) {
238
+ aiguardMetrics.count(TAGS.TELEMETRY_ERROR, {
239
+ type: TAGS.ERROR_TYPE_REDACTION,
240
+ ...report.telemetryTags,
241
+ }).inc(redaction.failures)
242
+ }
217
243
  }
244
+
245
+ report.metaStruct.messages = this.#buildMessagesForMetaStruct(result.messages, report.telemetryTags)
246
+
247
+ const requestTelemetryTags = redaction.enabled
248
+ ? {
249
+ action: result.action,
250
+ error: false,
251
+ block: shouldBlock,
252
+ redacted: redaction.applied,
253
+ ...report.telemetryTags,
254
+ }
255
+ : {
256
+ action: result.action,
257
+ error: false,
258
+ block: shouldBlock,
259
+ ...report.telemetryTags,
260
+ }
218
261
  aiguardMetrics.count(TAGS.TELEMETRY_REQUESTS, requestTelemetryTags).inc(1)
219
262
 
220
263
  report.span.setTag(TAGS.ACTION_TAG_KEY, result.action)
@@ -242,10 +285,7 @@ class EvaluationReporter {
242
285
  let contentTruncated = false
243
286
  for (let i = messages.length - size; i < messages.length; i++) {
244
287
  const message = clone(messages[i])
245
- if (message.content?.length > this.#maxContentSize) {
246
- contentTruncated = true
247
- message.content = message.content.slice(0, this.#maxContentSize)
248
- }
288
+ if (this.#truncateMessageContent(message)) contentTruncated = true
249
289
  result.push(message)
250
290
  }
251
291
  if (contentTruncated) {
@@ -254,6 +294,40 @@ class EvaluationReporter {
254
294
  return result
255
295
  }
256
296
 
297
+ /**
298
+ * Truncates text in a cloned message to one shared content-size limit.
299
+ *
300
+ * @param {{ content?: string|ContentPart[] }} message
301
+ * @returns {boolean}
302
+ */
303
+ #truncateMessageContent (message) {
304
+ const { content } = message
305
+ if (typeof content === 'string') {
306
+ if (content.length <= this.#maxContentSize) return false
307
+
308
+ message.content = content.slice(0, this.#maxContentSize)
309
+ return true
310
+ }
311
+
312
+ if (!Array.isArray(content)) return false
313
+
314
+ let remainingContentSize = this.#maxContentSize
315
+ let truncated = false
316
+ for (const part of content) {
317
+ const text = part?.text
318
+ if (typeof text !== 'string') continue
319
+
320
+ if (text.length > remainingContentSize) {
321
+ part.text = text.slice(0, remainingContentSize)
322
+ truncated = true
323
+ remainingContentSize = 0
324
+ } else {
325
+ remainingContentSize -= text.length
326
+ }
327
+ }
328
+ return truncated
329
+ }
330
+
257
331
  /**
258
332
  * Returns whether a message represents a tool call or tool output.
259
333
  *
@@ -74,18 +74,17 @@ function onStreamAfter (ctx) {
74
74
  */
75
75
  function getStreamContent (chunks) {
76
76
  const toolCalls = []
77
- const textParts = []
77
+ let text = ''
78
78
 
79
79
  for (const chunk of chunks) {
80
80
  if (chunk?.type === 'tool-call') {
81
81
  toolCalls.push(chunk)
82
82
  } else if (chunk?.type === 'text-delta') {
83
- textParts.push(chunk.textDelta)
83
+ text += chunk.delta ?? chunk.textDelta ?? ''
84
84
  }
85
85
  }
86
86
 
87
87
  if (toolCalls.length) return toolCalls
88
- const text = textParts.join('')
89
88
  return text ? [{ type: 'text', text }] : []
90
89
  }
91
90
 
@@ -269,27 +269,39 @@ function convertServerToolResultContent (content) {
269
269
  return content.error_message ? `${content.error_code}: ${content.error_message}` : content.error_code
270
270
  }
271
271
 
272
- const lines = []
272
+ let lines
273
273
  if (Array.isArray(content)) {
274
274
  for (const item of content) {
275
275
  if (!item || typeof item !== 'object') continue
276
276
  // text blocks (MCP / generic), then web-search title + url.
277
- if (typeof item.text === 'string') lines.push(item.text)
278
- if (typeof item.title === 'string') lines.push(item.title)
279
- if (typeof item.url === 'string') lines.push(item.url)
277
+ if (typeof item.text === 'string') {
278
+ lines = lines === undefined ? item.text : `${lines}\n${item.text}`
279
+ }
280
+ if (typeof item.title === 'string') {
281
+ lines = lines === undefined ? item.title : `${lines}\n${item.title}`
282
+ }
283
+ if (typeof item.url === 'string') {
284
+ lines = lines === undefined ? item.url : `${lines}\n${item.url}`
285
+ }
280
286
  }
281
287
  } else {
282
- if (typeof content.stdout === 'string' && content.stdout) lines.push(content.stdout)
283
- if (typeof content.stderr === 'string' && content.stderr) lines.push(content.stderr)
284
- if (typeof content.content === 'string' && content.content) lines.push(content.content)
288
+ if (typeof content.stdout === 'string' && content.stdout) lines = content.stdout
289
+ if (typeof content.stderr === 'string' && content.stderr) {
290
+ lines = lines === undefined ? content.stderr : `${lines}\n${content.stderr}`
291
+ }
292
+ if (typeof content.content === 'string' && content.content) {
293
+ lines = lines === undefined ? content.content : `${lines}\n${content.content}`
294
+ }
285
295
  if (Array.isArray(content.lines)) {
286
296
  for (const line of content.lines) {
287
- if (typeof line === 'string') lines.push(line)
297
+ if (typeof line === 'string') {
298
+ lines = lines === undefined ? line : `${lines}\n${line}`
299
+ }
288
300
  }
289
301
  }
290
302
  }
291
303
 
292
- return lines.join('\n') || '[tool result]'
304
+ return lines || '[tool result]'
293
305
  }
294
306
 
295
307
  /**
@@ -2,7 +2,15 @@
2
2
 
3
3
  class NoopAIGuard {
4
4
  evaluate (messages, opts) {
5
- return Promise.resolve({ action: 'ALLOW', reason: 'AI Guard is not enabled', tags: [], sds: [] })
5
+ return Promise.resolve({
6
+ action: 'ALLOW',
7
+ reason: 'AI Guard is not enabled',
8
+ tags: [],
9
+ tagProbabilities: {},
10
+ sds: [],
11
+ messages,
12
+ redactionReplacements: [],
13
+ })
6
14
  }
7
15
  }
8
16
 
@@ -0,0 +1,183 @@
1
+ 'use strict'
2
+
3
+ /** @typedef {import('../../../../index').aiguard.RedactionReplacement} RedactionReplacement */
4
+
5
+ const SEGMENT_PATTERN = /^([A-Za-z0-9_]+)(?:\[([0-9]+)\])?$/
6
+
7
+ /**
8
+ * Converts the raw backend replacements into the public, typed contract.
9
+ *
10
+ * @param {unknown} replacements
11
+ * @returns {RedactionReplacement[]}
12
+ */
13
+ function normalizeRedactionReplacements (replacements) {
14
+ if (!Array.isArray(replacements)) return []
15
+
16
+ const result = []
17
+ for (const entry of replacements) {
18
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) continue
19
+
20
+ const { path, replacement } = entry
21
+ if (typeof path !== 'string' || path.length === 0 || typeof replacement !== 'string') continue
22
+
23
+ result.push({ path, replacement })
24
+ }
25
+ return result
26
+ }
27
+
28
+ /**
29
+ * Parses an AI Guard location path into named segments and optional indexes.
30
+ *
31
+ * @param {string} path
32
+ * @returns {Array<{ name: string, index: number|undefined }>|undefined}
33
+ */
34
+ function parsePath (path) {
35
+ const segments = []
36
+ for (const rawSegment of path.split('.')) {
37
+ const match = SEGMENT_PATTERN.exec(rawSegment)
38
+ if (!match) return
39
+
40
+ segments.push({
41
+ name: match[1],
42
+ index: match[2] === undefined ? undefined : Number(match[2]),
43
+ })
44
+ }
45
+ return segments
46
+ }
47
+
48
+ /**
49
+ * Reports whether a parsed path targets an allowed message string.
50
+ *
51
+ * @param {Array<{ name: string, index: number|undefined }>} segments
52
+ * @returns {boolean}
53
+ */
54
+ function isRedactablePath (segments) {
55
+ const first = segments[0]
56
+ if (first?.name !== 'messages' || first.index === undefined) return false
57
+
58
+ if (segments.length === 2) {
59
+ const content = segments[1]
60
+ return content.name === 'content' && content.index === undefined
61
+ }
62
+
63
+ if (segments.length === 3) {
64
+ const content = segments[1]
65
+ const text = segments[2]
66
+ return content.name === 'content' && content.index !== undefined &&
67
+ text.name === 'text' && text.index === undefined
68
+ }
69
+
70
+ if (segments.length === 4) {
71
+ const toolCalls = segments[1]
72
+ const functionSegment = segments[2]
73
+ const argumentsSegment = segments[3]
74
+ return toolCalls.name === 'tool_calls' && toolCalls.index !== undefined &&
75
+ functionSegment.name === 'function' && functionSegment.index === undefined &&
76
+ argumentsSegment.name === 'arguments' && argumentsSegment.index === undefined
77
+ }
78
+
79
+ return false
80
+ }
81
+
82
+ /**
83
+ * Resolves a location path to a writable string container and key.
84
+ *
85
+ * @param {{ messages: Array<object> }} root
86
+ * @param {string} path
87
+ * @returns {{ container: object|Array<unknown>, key: string|number }|undefined}
88
+ */
89
+ function resolveWritableString (root, path) {
90
+ const segments = parsePath(path)
91
+ if (!segments || !isRedactablePath(segments)) return
92
+
93
+ const terminal = segments.at(-1)
94
+ let node = root
95
+ for (let i = 0; i < segments.length - 1; i++) {
96
+ const { name, index } = segments[i]
97
+ if (!node || typeof node !== 'object' || !Object.hasOwn(node, name)) return
98
+
99
+ node = node[name]
100
+ if (index !== undefined) {
101
+ if (!Array.isArray(node) || index >= node.length) return
102
+ node = node[index]
103
+ }
104
+ }
105
+
106
+ if (!node || typeof node !== 'object') return
107
+ if (!Object.hasOwn(node, terminal.name) || typeof node[terminal.name] !== 'string') return
108
+ return { container: node, key: terminal.name }
109
+ }
110
+
111
+ /**
112
+ * Applies redaction replacements to an AI Guard message list.
113
+ * The caller transfers ownership of a private snapshot. Successful replacements mutate that snapshot in place.
114
+ *
115
+ * @param {Array<object>} messages
116
+ * @param {unknown} replacements
117
+ * @returns {{ messages: Array<object>, redacted: boolean, failures: number }}
118
+ */
119
+ function redactMessages (messages, replacements) {
120
+ if (!Array.isArray(messages) || replacements === undefined || replacements === null) {
121
+ return { messages, redacted: false, failures: 0 }
122
+ }
123
+
124
+ try {
125
+ if (!Array.isArray(replacements)) {
126
+ return { messages, redacted: false, failures: 1 }
127
+ }
128
+ if (replacements.length === 0) return { messages, redacted: false, failures: 0 }
129
+
130
+ const replacementsByPath = new Map()
131
+ const conflictingPaths = new Set()
132
+ let failures = 0
133
+ for (const entry of replacements) {
134
+ if (!entry || typeof entry !== 'object') {
135
+ failures++
136
+ continue
137
+ }
138
+
139
+ const { path, replacement } = entry
140
+ if (typeof path !== 'string' || path.length === 0 || typeof replacement !== 'string') {
141
+ failures++
142
+ continue
143
+ }
144
+
145
+ if (conflictingPaths.has(path)) continue
146
+ if (replacementsByPath.has(path) && replacementsByPath.get(path) !== replacement) {
147
+ replacementsByPath.delete(path)
148
+ conflictingPaths.add(path)
149
+ failures++
150
+ continue
151
+ }
152
+ replacementsByPath.set(path, replacement)
153
+ }
154
+
155
+ if (replacementsByPath.size === 0) return { messages, redacted: false, failures }
156
+
157
+ const root = { messages }
158
+ const targets = []
159
+
160
+ for (const [path, replacement] of replacementsByPath) {
161
+ const resolved = resolveWritableString(root, path)
162
+ if (!resolved) {
163
+ failures++
164
+ continue
165
+ }
166
+
167
+ targets.push({ ...resolved, replacement })
168
+ }
169
+
170
+ for (const { container, key, replacement } of targets) {
171
+ container[key] = replacement
172
+ }
173
+
174
+ return { messages, redacted: targets.length > 0, failures }
175
+ } catch {
176
+ return { messages, redacted: false, failures: 1 }
177
+ }
178
+ }
179
+
180
+ module.exports = {
181
+ normalizeRedactionReplacements,
182
+ redactMessages,
183
+ }
@@ -12,6 +12,7 @@ class AIGuard extends NoopAIGuard {
12
12
  #tracer
13
13
  #client
14
14
  #reporter
15
+ #redactionEnabled
15
16
  #meta
16
17
 
17
18
  /**
@@ -29,6 +30,7 @@ class AIGuard extends NoopAIGuard {
29
30
  this.#tracer = tracer
30
31
  this.#client = new AIGuardClient(config)
31
32
  this.#reporter = new EvaluationReporter(config)
33
+ this.#redactionEnabled = config.experimental.aiguard.redactionEnabled
32
34
  this.#meta = { service: config.service, env: config.env }
33
35
  this.#initialized = true
34
36
  }
@@ -45,13 +47,16 @@ class AIGuard extends NoopAIGuard {
45
47
  const report = this.#reporter.start(span, messages, { source, integration })
46
48
  let evaluation
47
49
  try {
48
- evaluation = await this.#client.evaluate(messages, this.#meta)
50
+ evaluation = await this.#client.evaluate(report.messages, this.#meta)
49
51
  } catch (error) {
50
52
  this.#reporter.fail(report, error.telemetryType ?? TAGS.ERROR_TYPE_CLIENT)
51
53
  throw error
52
54
  }
53
55
 
54
- const outcome = createEvaluationOutcome(evaluation, block)
56
+ const outcome = createEvaluationOutcome(report.messages, evaluation, {
57
+ block,
58
+ redactionEnabled: this.#redactionEnabled,
59
+ })
55
60
  this.#reporter.finish(report, outcome)
56
61
 
57
62
  if (outcome.shouldBlock) {
@@ -7,6 +7,7 @@ module.exports = {
7
7
  ACTION_TAG_KEY: 'ai_guard.action',
8
8
  REASON_TAG_KEY: 'ai_guard.reason',
9
9
  BLOCKED_TAG_KEY: 'ai_guard.blocked',
10
+ REDACTED_TAG_KEY: 'ai_guard.redacted',
10
11
  EVENT_TAG_KEY: 'ai_guard.event',
11
12
  META_STRUCT_KEY: 'ai_guard',
12
13
 
@@ -27,4 +28,5 @@ module.exports = {
27
28
  ERROR_TYPE_CLIENT: 'client_error',
28
29
  ERROR_TYPE_STATUS: 'bad_status',
29
30
  ERROR_TYPE_RESPONSE: 'bad_response',
31
+ ERROR_TYPE_REDACTION: 'redaction_error',
30
32
  }
@@ -108,7 +108,7 @@ function stringifyWithRanges (obj, objRanges, loadSensitiveRanges = false) {
108
108
  value = JSON.stringify(cloneObj, null, 2)
109
109
 
110
110
  if (counter > 0) {
111
- const segments = []
111
+ let formattedValue = ''
112
112
  let outputLength = 0
113
113
  let pos = 0
114
114
  let rangeKeyIndex = value.indexOf(STRINGIFY_RANGE_KEY)
@@ -133,13 +133,14 @@ function stringifyWithRanges (obj, objRanges, loadSensitiveRanges = false) {
133
133
  start,
134
134
  end: start + originalValue.length,
135
135
  })
136
- segments.push(value.slice(pos, rangeKeyIndex), originalValue)
136
+ formattedValue += value.slice(pos, rangeKeyIndex)
137
+ formattedValue += originalValue
137
138
  outputLength += cleanLength + originalValue.length
138
139
  pos = rangeKeyIndex + matchValue.length
139
140
  } else {
140
141
  // can't happen, the only way to this to happen is
141
142
  // if the JSON has a value starting with the value of STRINGIFY_SENSITIVE_NOT_STRING_KEY
142
- segments.push(value.slice(pos, rangeKeyIndex + STRINGIFY_SENSITIVE_NOT_STRING_KEY.length + 1))
143
+ formattedValue += value.slice(pos, rangeKeyIndex + STRINGIFY_SENSITIVE_NOT_STRING_KEY.length + 1)
143
144
  outputLength += cleanLength + STRINGIFY_SENSITIVE_NOT_STRING_KEY.length + 1
144
145
  pos = rangeKeyIndex + STRINGIFY_SENSITIVE_NOT_STRING_KEY.length + 1
145
146
  }
@@ -152,13 +153,13 @@ function stringifyWithRanges (obj, objRanges, loadSensitiveRanges = false) {
152
153
  start,
153
154
  end: start + Number.parseInt(regexRes[1], 10),
154
155
  })
155
- segments.push(value.slice(pos, rangeKeyIndex))
156
+ formattedValue += value.slice(pos, rangeKeyIndex)
156
157
  outputLength += cleanLength
157
158
  pos = rangeKeyIndex + regexRes[0].length
158
159
  } else {
159
160
  // can't happen, the only way to this to happen is
160
161
  // if the JSON has a value starting with the value of STRINGIFY_SENSITIVE_KEY
161
- segments.push(value.slice(pos, rangeKeyIndex + STRINGIFY_SENSITIVE_KEY.length))
162
+ formattedValue += value.slice(pos, rangeKeyIndex + STRINGIFY_SENSITIVE_KEY.length)
162
163
  outputLength += cleanLength + STRINGIFY_SENSITIVE_KEY.length
163
164
  pos = rangeKeyIndex + STRINGIFY_SENSITIVE_KEY.length
164
165
  }
@@ -175,13 +176,13 @@ function stringifyWithRanges (obj, objRanges, loadSensitiveRanges = false) {
175
176
  }))
176
177
  ranges.push(...updatedRanges)
177
178
 
178
- segments.push(value.slice(pos, rangeKeyIndex))
179
+ formattedValue += value.slice(pos, rangeKeyIndex)
179
180
  outputLength += cleanLength
180
181
  pos = rangeKeyIndex + regexRes[0].length
181
182
  } else {
182
183
  // can't happen, the only way to this to happen is
183
184
  // if the JSON has a value starting with the value of STRINGIFY_RANGE_KEY
184
- segments.push(value.slice(pos, rangeKeyIndex + STRINGIFY_RANGE_KEY.length))
185
+ formattedValue += value.slice(pos, rangeKeyIndex + STRINGIFY_RANGE_KEY.length)
185
186
  outputLength += cleanLength + STRINGIFY_RANGE_KEY.length
186
187
  pos = rangeKeyIndex + STRINGIFY_RANGE_KEY.length
187
188
  }
@@ -190,8 +191,8 @@ function stringifyWithRanges (obj, objRanges, loadSensitiveRanges = false) {
190
191
  rangeKeyIndex = value.indexOf(STRINGIFY_RANGE_KEY, pos)
191
192
  }
192
193
 
193
- segments.push(value.slice(pos))
194
- value = segments.join('')
194
+ formattedValue += value.slice(pos)
195
+ value = formattedValue
195
196
  }
196
197
  } else {
197
198
  value = JSON.stringify(obj, null, 2)
@@ -82,6 +82,8 @@ function updateWafFromRC (transaction) {
82
82
  const asmFile = /** @type {AsmConfigFile} */ (item.file)
83
83
  if (asmFile?.actions?.length) {
84
84
  newActions.set(item.id, asmFile.actions)
85
+ } else {
86
+ newActions.delete(item.id)
85
87
  }
86
88
  }
87
89
  } catch (e) {