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.
- package/ci/test-optimization-validation/framework-adapters/cypress.js +9 -11
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
- package/index.d.ts +127 -26
- package/package.json +10 -10
- package/packages/datadog-instrumentations/src/bunyan.js +4 -2
- package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
- package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +28 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
- package/packages/datadog-instrumentations/src/jest.js +140 -18
- package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
- package/packages/datadog-instrumentations/src/mariadb.js +7 -0
- package/packages/datadog-instrumentations/src/pino.js +15 -4
- package/packages/datadog-instrumentations/src/playwright.js +34 -20
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +7 -4
- package/packages/datadog-instrumentations/src/vitest-worker.js +11 -0
- package/packages/datadog-instrumentations/src/webdriverio.js +26 -0
- package/packages/datadog-instrumentations/src/winston.js +6 -3
- package/packages/datadog-plugin-cypress/src/support.js +1 -0
- package/packages/datadog-plugin-http/src/client.js +2 -15
- package/packages/datadog-plugin-http2/src/client.js +2 -15
- package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
- package/packages/datadog-plugin-playwright/src/index.js +9 -1
- package/packages/datadog-plugin-undici/src/index.js +2 -15
- package/packages/dd-trace/src/aiguard/evaluation.js +94 -20
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +21 -9
- package/packages/dd-trace/src/aiguard/noop.js +9 -1
- package/packages/dd-trace/src/aiguard/redaction.js +183 -0
- package/packages/dd-trace/src/aiguard/sdk.js +7 -2
- package/packages/dd-trace/src/aiguard/tags.js +2 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
- package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +51 -26
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -11
- package/packages/dd-trace/src/ci-visibility/final-flush.js +20 -0
- package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +196 -29
- package/packages/dd-trace/src/config/generated-config-types.d.ts +6 -0
- package/packages/dd-trace/src/config/index.js +2 -1
- package/packages/dd-trace/src/config/supported-configurations.json +27 -0
- package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -3
- package/packages/dd-trace/src/dogstatsd.js +45 -18
- package/packages/dd-trace/src/encode/agentless-json.js +5 -4
- package/packages/dd-trace/src/exporters/agent/index.js +22 -2
- package/packages/dd-trace/src/exporters/agent/writer.js +4 -4
- package/packages/dd-trace/src/exporters/common/writer.js +24 -2
- package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
- package/packages/dd-trace/src/external-logger/src/index.js +4 -3
- package/packages/dd-trace/src/flush.js +102 -0
- package/packages/dd-trace/src/llmobs/experiments/client.js +4 -0
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +51 -2
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +420 -105
- package/packages/dd-trace/src/llmobs/experiments/index.js +54 -42
- package/packages/dd-trace/src/llmobs/experiments/noop.js +39 -8
- package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
- package/packages/dd-trace/src/llmobs/experiments/util.js +21 -0
- package/packages/dd-trace/src/llmobs/index.js +80 -17
- package/packages/dd-trace/src/llmobs/noop.js +2 -1
- package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
- package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
- package/packages/dd-trace/src/llmobs/writers/base.js +60 -24
- package/packages/dd-trace/src/noop/span.js +5 -0
- package/packages/dd-trace/src/openfeature/writers/exposures.js +8 -1
- package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
- package/packages/dd-trace/src/opentelemetry/logs/index.js +4 -1
- package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
- package/packages/dd-trace/src/opentelemetry/metrics/index.js +3 -1
- package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +4 -2
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +4 -2
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
- package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +410 -342
- package/packages/dd-trace/src/opentracing/span.js +28 -0
- package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
- package/packages/dd-trace/src/plugin_manager.js +2 -0
- package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
- package/packages/dd-trace/src/plugins/util/test.js +74 -21
- package/packages/dd-trace/src/plugins/util/web.js +2 -65
- package/packages/dd-trace/src/profiling/oom.js +4 -3
- package/packages/dd-trace/src/profiling/profiler.js +7 -0
- package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
- package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
- package/packages/dd-trace/src/proxy.js +36 -8
- package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
- package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +17 -6
- package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +55 -0
- package/packages/dd-trace/src/serverless/vercel.js +122 -0
- package/packages/dd-trace/src/serverless.js +37 -20
- package/packages/dd-trace/src/span_stats.js +40 -3
- package/packages/dd-trace/src/tracer.js +22 -0
- package/packages/dd-trace/src/web-tags-cache.js +67 -13
- 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
|
|
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}
|
|
109
|
+
* @param {{ block: boolean, redactionEnabled: boolean }} options
|
|
101
110
|
* @returns {EvaluationOutcome}
|
|
102
111
|
*/
|
|
103
|
-
function createEvaluationOutcome (evaluation,
|
|
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
|
|
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,
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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')
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
283
|
-
if (typeof content.stderr === 'string' && content.stderr)
|
|
284
|
-
|
|
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')
|
|
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
|
|
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({
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
value =
|
|
194
|
+
formattedValue += value.slice(pos)
|
|
195
|
+
value = formattedValue
|
|
195
196
|
}
|
|
196
197
|
} else {
|
|
197
198
|
value = JSON.stringify(obj, null, 2)
|