dd-trace 6.14.0 → 6.16.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 (98) hide show
  1. package/README.md +15 -0
  2. package/ci/test-optimization-validation/framework-adapters/cucumber.js +4 -2
  3. package/ci/test-optimization-validation/package-check.js +4 -1
  4. package/ci/test-optimization-validation/scenarios/ci-wiring.js +1 -1
  5. package/index.d.ts +12 -0
  6. package/next.d.ts +27 -0
  7. package/next.js +3 -0
  8. package/package.json +17 -13
  9. package/packages/datadog-esbuild/index.js +14 -1
  10. package/packages/datadog-esbuild/src/resolver.js +284 -0
  11. package/packages/datadog-esbuild/src/utils.js +163 -119
  12. package/packages/datadog-instrumentations/src/ai.js +39 -97
  13. package/packages/datadog-instrumentations/src/anthropic.js +76 -134
  14. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +1 -1
  15. package/packages/datadog-instrumentations/src/azure-cosmos.js +1 -1
  16. package/packages/datadog-instrumentations/src/azure-durable-functions.js +23 -9
  17. package/packages/datadog-instrumentations/src/azure-functions.js +48 -0
  18. package/packages/datadog-instrumentations/src/bullmq.js +1 -1
  19. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  20. package/packages/datadog-instrumentations/src/dd-trace-api.js +2 -2
  21. package/packages/datadog-instrumentations/src/graphql.js +1 -1
  22. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +39 -14
  23. package/packages/datadog-instrumentations/src/helpers/instrument.js +25 -8
  24. package/packages/datadog-instrumentations/src/helpers/instrumentation-utils.js +72 -0
  25. package/packages/datadog-instrumentations/src/helpers/register.js +9 -64
  26. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +46 -14
  27. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/azure-durable-functions.js +17 -0
  28. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  29. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +1 -5
  30. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +6 -7
  31. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  32. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +99 -70
  33. package/packages/datadog-instrumentations/src/jest.js +141 -25
  34. package/packages/datadog-instrumentations/src/langchain.js +1 -1
  35. package/packages/datadog-instrumentations/src/langgraph.js +1 -1
  36. package/packages/datadog-instrumentations/src/mercurius.js +1 -1
  37. package/packages/datadog-instrumentations/src/mocha/main.js +41 -5
  38. package/packages/datadog-instrumentations/src/modelcontextprotocol-sdk.js +1 -1
  39. package/packages/datadog-instrumentations/src/openai-agents.js +1 -1
  40. package/packages/datadog-instrumentations/src/openai.js +54 -76
  41. package/packages/datadog-instrumentations/src/playwright.js +4 -0
  42. package/packages/datadog-instrumentations/src/vitest-worker.js +7 -10
  43. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +7 -1
  44. package/packages/datadog-plugin-azure-cosmos/src/index.js +1 -1
  45. package/packages/datadog-plugin-azure-durable-functions/src/index.js +96 -1
  46. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -24
  47. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +2 -36
  48. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-request-context.js +31 -0
  49. package/packages/datadog-plugin-graphql/src/resolve-error.js +33 -11
  50. package/packages/datadog-turbopack/index.js +212 -0
  51. package/packages/datadog-turbopack/src/constants.js +5 -0
  52. package/packages/datadog-turbopack/src/loader.js +500 -0
  53. package/packages/dd-trace/src/aiguard/index.js +2 -1
  54. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +53 -25
  55. package/packages/dd-trace/src/aiguard/integrations/evaluate.js +49 -25
  56. package/packages/dd-trace/src/aiguard/integrations/index.js +3 -2
  57. package/packages/dd-trace/src/aiguard/integrations/openai.js +59 -30
  58. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +116 -42
  59. package/packages/dd-trace/src/aiguard/messages/utils.js +29 -1
  60. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +25 -0
  61. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +3 -1
  62. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +531 -70
  63. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +2 -2
  64. package/packages/dd-trace/src/appsec/telemetry/api_security.js +1 -3
  65. package/packages/dd-trace/src/appsec/telemetry/common.js +4 -0
  66. package/packages/dd-trace/src/appsec/telemetry/index.js +23 -5
  67. package/packages/dd-trace/src/appsec/telemetry/rasp.js +1 -20
  68. package/packages/dd-trace/src/appsec/telemetry/user.js +1 -3
  69. package/packages/dd-trace/src/appsec/telemetry/waf.js +1 -20
  70. package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -0
  71. package/packages/dd-trace/src/ci-visibility/requests/video-request.js +4 -0
  72. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  73. package/packages/dd-trace/src/config/helper.js +10 -2
  74. package/packages/dd-trace/src/config/index.js +20 -5
  75. package/packages/dd-trace/src/config/supported-configurations.json +10 -0
  76. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -7
  77. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +14 -1
  78. package/packages/dd-trace/src/debugger/devtools_client/request-options.js +1 -6
  79. package/packages/dd-trace/src/evp_proxy/direct.js +2 -28
  80. package/packages/dd-trace/src/evp_proxy/discovery.js +3 -4
  81. package/packages/dd-trace/src/evp_proxy/path.js +12 -3
  82. package/packages/dd-trace/src/exporters/agentless/writer.js +3 -1
  83. package/packages/dd-trace/src/exporters/common/docker.js +4 -1
  84. package/packages/dd-trace/src/exporters/common/proxy.js +55 -0
  85. package/packages/dd-trace/src/exporters/common/request.js +11 -3
  86. package/packages/dd-trace/src/opentelemetry/metrics/index.js +1 -1
  87. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +5 -4
  88. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +106 -25
  89. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +12 -1
  90. package/packages/dd-trace/src/opentracing/propagation/text_map.js +8 -2
  91. package/packages/dd-trace/src/plugin_manager.js +9 -3
  92. package/packages/dd-trace/src/plugins/index.js +1 -0
  93. package/packages/dd-trace/src/plugins/util/test.js +4 -1
  94. package/packages/dd-trace/src/priority_sampler.js +4 -0
  95. package/packages/dd-trace/src/sampling_rule.js +3 -1
  96. package/packages/dd-trace/src/span_processor.js +18 -3
  97. package/packages/dd-trace/src/telemetry/send-data.js +5 -4
  98. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +0 -10
@@ -3,44 +3,68 @@
3
3
  const log = require('../../log')
4
4
 
5
5
  /**
6
- * Starts one AI Guard evaluation for a lifecycle ctx.
6
+ * Runs one evaluation per conversation and rejects if any of them says block.
7
7
  *
8
- * Async evaluations are pushed synchronously during channel publication. If
9
- * evaluation throws synchronously, the error is handled before publish returns.
8
+ * Evaluations run concurrently and all of them are awaited, so a slow one cannot hide a block
9
+ * from a fast one. Any failure that is not a verdict is logged and ignored, because AI Guard
10
+ * must never break the caller.
10
11
  *
11
- * @param {object} ctx
12
- * @param {AbortController} ctx.abortController
13
- * @param {object} [ctx.parentSpan]
14
- * @param {Array<Promise<void>>} ctx.pending
12
+ * @param {object} interceptCtx The intercept payload, whose tracing ctx parents the span.
15
13
  * @param {object} aiguard
16
- * @param {Array<object>|undefined} messages
14
+ * @param {Array<Array<object>>} conversations One complete message list per evaluation.
17
15
  * @param {object} opts
16
+ * @returns {Promise<void>|undefined} `undefined` when there is nothing to evaluate.
18
17
  */
19
- function pushEvaluation (ctx, aiguard, messages, opts) {
20
- if (!messages?.length) return
18
+ function evaluate (interceptCtx, aiguard, conversations, opts) {
19
+ if (!conversations?.length) return
21
20
 
22
- const evaluateOpts = ctx.parentSpan ? { ...opts, childOf: ctx.parentSpan } : opts
21
+ const parentSpan = interceptCtx?.tracingContext?.currentStore?.span
22
+ const evaluateOpts = parentSpan ? { ...opts, childOf: parentSpan } : opts
23
+ const evaluations = []
23
24
 
24
- try {
25
- ctx.pending.push(aiguard.evaluate(messages, evaluateOpts).catch(handleEvaluationError.bind(null, ctx)))
26
- } catch (err) {
27
- handleEvaluationError(ctx, err)
25
+ for (const messages of conversations) {
26
+ if (messages?.length) evaluations.push(evaluateOne(aiguard, messages, evaluateOpts))
28
27
  }
28
+
29
+ if (evaluations.length === 0) return
30
+
31
+ return Promise.allSettled(evaluations).then(throwFirstBlock)
29
32
  }
30
33
 
31
34
  /**
32
- * Handles an AI Guard evaluation failure.
35
+ * Turns a synchronous throw into a rejection, so every outcome is inspected the same way.
33
36
  *
34
- * @param {object} ctx
35
- * @param {AbortController} ctx.abortController
36
- * @param {Error} err
37
+ * @param {object} aiguard
38
+ * @param {Array<object>} messages
39
+ * @param {object} opts
40
+ * @returns {Promise<unknown>}
41
+ */
42
+ function evaluateOne (aiguard, messages, opts) {
43
+ try {
44
+ return aiguard.evaluate(messages, opts)
45
+ } catch (error) {
46
+ return Promise.reject(error)
47
+ }
48
+ }
49
+
50
+ /**
51
+ * @param {Array<{ status: string, reason?: Error }>} results
52
+ * @throws {Error} The first block verdict, if any evaluation returned one.
37
53
  */
38
- function handleEvaluationError (ctx, err) {
39
- if (err.name === 'AIGuardAbortError') {
40
- ctx.abortController.abort(err)
41
- } else {
42
- log.error('AIGuard: unexpected error during evaluation: %s', err.message)
54
+ function throwFirstBlock (results) {
55
+ let block
56
+
57
+ for (const result of results) {
58
+ if (result.status === 'fulfilled') continue
59
+
60
+ if (result.reason?.name === 'AIGuardAbortError') {
61
+ block ??= result.reason
62
+ } else {
63
+ log.error('AIGuard: unexpected error during evaluation: %s', result.reason?.message)
64
+ }
43
65
  }
66
+
67
+ if (block) throw block
44
68
  }
45
69
 
46
- module.exports = { pushEvaluation }
70
+ module.exports = { evaluate }
@@ -11,13 +11,14 @@ let isEnabled = false
11
11
  *
12
12
  * @param {object} aiguard
13
13
  * @param {boolean} block
14
+ * @param {boolean} analyzeStreamResponses
14
15
  */
15
- function enable (aiguard, block) {
16
+ function enable (aiguard, block, analyzeStreamResponses) {
16
17
  if (isEnabled) return
17
18
 
18
19
  anthropic.enable(aiguard, block)
19
20
  openai.enable(aiguard, block)
20
- vercelAi.enable(aiguard, block)
21
+ vercelAi.enable(aiguard, block, analyzeStreamResponses)
21
22
 
22
23
  isEnabled = true
23
24
  }
@@ -8,20 +8,19 @@ const {
8
8
  getResponsesInputMessages,
9
9
  getResponsesOutputMessages,
10
10
  } = require('../messages/openai')
11
+ const { decode } = require('../messages/utils')
11
12
  const { SOURCE_AUTO } = require('../tags')
12
- const { pushEvaluation } = require('./evaluate')
13
+ const { evaluate } = require('./evaluate')
13
14
 
14
- const chatCompletionsBeforeChannel = channel('dd-trace:openai:chat.completions:before')
15
- const chatCompletionsAfterChannel = channel('dd-trace:openai:chat.completions:after')
16
- const responsesBeforeChannel = channel('dd-trace:openai:responses:before')
17
- const responsesAfterChannel = channel('dd-trace:openai:responses:after')
15
+ const chatCompletionsInterceptChannel = channel('dd-trace:openai:chat.completions:intercept')
16
+ const responsesInterceptChannel = channel('dd-trace:openai:responses:intercept')
18
17
 
19
18
  let isEnabled = false
20
19
  let aiguard
21
20
  let opts
22
21
 
23
22
  /**
24
- * Subscribes AI Guard to OpenAI lifecycle channels.
23
+ * Subscribes AI Guard to the OpenAI interception channels.
25
24
  *
26
25
  * @param {object} aiguardInstance
27
26
  * @param {boolean} block
@@ -32,10 +31,8 @@ function enable (aiguardInstance, block) {
32
31
  aiguard = aiguardInstance
33
32
  opts = { block, source: SOURCE_AUTO, integration: 'openai' }
34
33
 
35
- chatCompletionsBeforeChannel.subscribe(onChatCompletionsBefore)
36
- chatCompletionsAfterChannel.subscribe(onChatCompletionsAfter)
37
- responsesBeforeChannel.subscribe(onResponsesBefore)
38
- responsesAfterChannel.subscribe(onResponsesAfter)
34
+ chatCompletionsInterceptChannel.subscribe(onChatCompletions)
35
+ responsesInterceptChannel.subscribe(onResponses)
39
36
 
40
37
  isEnabled = true
41
38
  }
@@ -43,38 +40,70 @@ function enable (aiguardInstance, block) {
43
40
  function disable () {
44
41
  if (!isEnabled) return
45
42
 
46
- chatCompletionsBeforeChannel.unsubscribe(onChatCompletionsBefore)
47
- chatCompletionsAfterChannel.unsubscribe(onChatCompletionsAfter)
48
- responsesBeforeChannel.unsubscribe(onResponsesBefore)
49
- responsesAfterChannel.unsubscribe(onResponsesAfter)
43
+ chatCompletionsInterceptChannel.unsubscribe(onChatCompletions)
44
+ responsesInterceptChannel.unsubscribe(onResponses)
50
45
 
51
46
  aiguard = undefined
52
47
  opts = undefined
53
48
  isEnabled = false
54
49
  }
55
50
 
56
- function onChatCompletionsBefore (ctx) {
57
- pushEvaluation(ctx, aiguard, getChatCompletionsInputMessages(ctx.args?.[0]), opts)
58
- }
59
-
60
- function onChatCompletionsAfter (ctx) {
61
- const inputMessages = getChatCompletionsInputMessages(ctx.args?.[0])
51
+ function onChatCompletions (ctx) {
52
+ const inputMessages = getChatCompletionsInputMessages(ctx.arguments?.[0])
62
53
  if (!inputMessages?.length) return
63
- for (const message of getChatCompletionsOutputMessages(ctx.body)) {
64
- pushEvaluation(ctx, aiguard, [...inputMessages, message], opts)
54
+
55
+ // `parse` and `asResponse` are both wrapped, and either may run more than once per call.
56
+ let inputEvaluation
57
+ ctx.beforeResult = () => {
58
+ if (!isEnabled) return
59
+ inputEvaluation ??= evaluate(ctx, aiguard, [inputMessages], opts)
60
+ return inputEvaluation
65
61
  }
66
- }
67
62
 
68
- function onResponsesBefore (ctx) {
69
- pushEvaluation(ctx, aiguard, getResponsesInputMessages(ctx.args?.[0]), opts)
63
+ // One model call has one output however many readers observe it.
64
+ let outputEvaluation
65
+ ctx.onResult = body => {
66
+ if (!isEnabled) return body
67
+
68
+ const conversations = decode(
69
+ () => getChatCompletionsOutputMessages(body).map(message => [...inputMessages, message]),
70
+ null,
71
+ 'AIGuard: unable to decode OpenAI response body: %s'
72
+ )
73
+ if (conversations === null) return body
74
+ if (conversations.length === 0) return body
75
+
76
+ outputEvaluation ??= evaluate(ctx, aiguard, conversations, opts)
77
+ return outputEvaluation.then(() => body)
78
+ }
70
79
  }
71
80
 
72
- function onResponsesAfter (ctx) {
73
- const inputMessages = getResponsesInputMessages(ctx.args?.[0])
81
+ function onResponses (ctx) {
82
+ const inputMessages = getResponsesInputMessages(ctx.arguments?.[0])
74
83
  if (!inputMessages?.length) return
75
- const outputMessages = getResponsesOutputMessages(ctx.body)
76
- if (!outputMessages.length) return
77
- pushEvaluation(ctx, aiguard, [...inputMessages, ...outputMessages], opts)
84
+
85
+ let inputEvaluation
86
+ ctx.beforeResult = () => {
87
+ if (!isEnabled) return
88
+ inputEvaluation ??= evaluate(ctx, aiguard, [inputMessages], opts)
89
+ return inputEvaluation
90
+ }
91
+
92
+ let outputEvaluation
93
+ ctx.onResult = body => {
94
+ if (!isEnabled) return body
95
+
96
+ const outputMessages = decode(
97
+ () => getResponsesOutputMessages(body),
98
+ null,
99
+ 'AIGuard: unable to decode OpenAI response body: %s'
100
+ )
101
+ if (outputMessages === null) return body
102
+ if (!outputMessages.length) return body
103
+
104
+ outputEvaluation ??= evaluate(ctx, aiguard, [[...inputMessages, ...outputMessages]], opts)
105
+ return outputEvaluation.then(() => body)
106
+ }
78
107
  }
79
108
 
80
109
  module.exports = { enable, disable }
@@ -2,35 +2,37 @@
2
2
 
3
3
  const { channel } = require('dc-polyfill')
4
4
 
5
- const { buildOutputMessages, convertVercelPromptToMessages } = require('../messages/vercel-ai')
5
+ const {
6
+ buildOutputMessages,
7
+ convertVercelPromptToMessages,
8
+ getStreamedContent,
9
+ } = require('../messages/vercel-ai')
10
+ const { decode } = require('../messages/utils')
6
11
  const { SOURCE_AUTO } = require('../tags')
7
- const { pushEvaluation } = require('./evaluate')
12
+ const { evaluate } = require('./evaluate')
8
13
 
9
- const doGenerateBeforeChannel = channel('dd-trace:vercel-ai:doGenerate:before')
10
- const doGenerateAfterChannel = channel('dd-trace:vercel-ai:doGenerate:after')
11
- const doStreamBeforeChannel = channel('dd-trace:vercel-ai:doStream:before')
12
- const doStreamAfterChannel = channel('dd-trace:vercel-ai:doStream:after')
14
+ const modelInterceptChannel = channel('dd-trace:vercel-ai:model:intercept')
13
15
 
14
16
  let isEnabled = false
15
17
  let aiguard
16
18
  let opts
19
+ let analyzeStreamResponses
17
20
 
18
21
  /**
19
- * Subscribes AI Guard to Vercel AI lifecycle channels.
22
+ * Subscribes AI Guard to the Vercel AI model call channel.
20
23
  *
21
24
  * @param {object} aiguardInstance
22
25
  * @param {boolean} block
26
+ * @param {boolean} analyzeStreams
23
27
  */
24
- function enable (aiguardInstance, block) {
28
+ function enable (aiguardInstance, block, analyzeStreams) {
25
29
  if (isEnabled) return
26
30
 
27
31
  aiguard = aiguardInstance
28
32
  opts = { block, source: SOURCE_AUTO, integration: 'ai' }
33
+ analyzeStreamResponses = analyzeStreams
29
34
 
30
- doGenerateBeforeChannel.subscribe(onBefore)
31
- doGenerateAfterChannel.subscribe(onGenerateAfter)
32
- doStreamBeforeChannel.subscribe(onBefore)
33
- doStreamAfterChannel.subscribe(onStreamAfter)
35
+ modelInterceptChannel.subscribe(onModelIntercept)
34
36
 
35
37
  isEnabled = true
36
38
  }
@@ -38,54 +40,126 @@ function enable (aiguardInstance, block) {
38
40
  function disable () {
39
41
  if (!isEnabled) return
40
42
 
41
- doGenerateBeforeChannel.unsubscribe(onBefore)
42
- doGenerateAfterChannel.unsubscribe(onGenerateAfter)
43
- doStreamBeforeChannel.unsubscribe(onBefore)
44
- doStreamAfterChannel.unsubscribe(onStreamAfter)
43
+ modelInterceptChannel.unsubscribe(onModelIntercept)
45
44
 
46
45
  aiguard = undefined
47
46
  opts = undefined
47
+ analyzeStreamResponses = undefined
48
48
  isEnabled = false
49
49
  }
50
50
 
51
- function onBefore (ctx) {
52
- pushEvaluation(ctx, aiguard, convertVercelPromptToMessages(ctx.prompt), opts)
53
- }
51
+ function onModelIntercept (ctx) {
52
+ const inputMessages = convertVercelPromptToMessages(ctx.arguments?.[0]?.prompt)
53
+ if (!inputMessages.length) return
54
+
55
+ // Called exactly once per model call by the instrumentation, so no memoization needed.
56
+ ctx.beforeResult = () => {
57
+ if (!isEnabled) return
58
+ return evaluate(ctx, aiguard, [inputMessages], opts)
59
+ }
54
60
 
55
- function onGenerateAfter (ctx) {
56
- const inputMessages = convertVercelPromptToMessages(ctx.prompt)
57
- if (!inputMessages.length || !ctx.result?.content?.length) return
61
+ if (ctx.method === 'doStream' && !analyzeStreamResponses) return
58
62
 
59
- pushEvaluation(ctx, aiguard, buildOutputMessages(inputMessages, ctx.result.content), opts)
63
+ ctx.onResult = ctx.method === 'doStream'
64
+ ? result => {
65
+ if (!isEnabled) return result
66
+ return interceptStreamedResult(ctx, result, inputMessages)
67
+ }
68
+ : result => {
69
+ if (!isEnabled) return result
70
+
71
+ const outputMessages = decode(
72
+ () => buildOutputMessages(inputMessages, result?.content ?? []),
73
+ null,
74
+ 'AIGuard: unable to decode the model result: %s'
75
+ )
76
+ if (outputMessages === null) return result
77
+ if (!outputMessages.length) return result
78
+
79
+ return evaluate(ctx, aiguard, [outputMessages], opts).then(() => result)
80
+ }
60
81
  }
61
82
 
62
- function onStreamAfter (ctx) {
63
- const inputMessages = convertVercelPromptToMessages(ctx.prompt)
64
- if (!inputMessages.length || !ctx.chunks?.length) return
83
+ /**
84
+ * Judging a streamed response requires the whole output, so the stream is drained and replaced
85
+ * with a replay of the collected chunks. That trades streaming latency for output coverage.
86
+ *
87
+ * @param {object} ctx
88
+ * @param {object} result
89
+ * @param {Array<object>} inputMessages
90
+ * @returns {object|Promise<object>}
91
+ */
92
+ function interceptStreamedResult (ctx, result, inputMessages) {
93
+ if (!result?.stream) return result
94
+
95
+ let drained
96
+ try {
97
+ drained = drainStream(result.stream)
98
+ } catch {
99
+ // Nothing was consumed, so the caller still has the stream they handed us.
100
+ return result
101
+ }
102
+
103
+ return drained.then(({ chunks, error }) => {
104
+ // The original stream is spent from here on, so the caller must get the replay either way.
105
+ const replayed = { ...result, stream: replayChunks(chunks, error) }
65
106
 
66
- pushEvaluation(ctx, aiguard, buildOutputMessages(inputMessages, getStreamContent(ctx.chunks)), opts)
107
+ // A stream that failed part-way has no complete output to judge; the replay carries the error.
108
+ if (error) return replayed
109
+
110
+ const outputMessages = decode(
111
+ () => buildOutputMessages(inputMessages, getStreamedContent(chunks)),
112
+ null,
113
+ 'AIGuard: unable to decode the streamed model result: %s'
114
+ )
115
+ if (outputMessages === null) return replayed
116
+ if (!outputMessages.length) return replayed
117
+
118
+ return evaluate(ctx, aiguard, [outputMessages], opts).then(() => replayed)
119
+ })
67
120
  }
68
121
 
69
122
  /**
70
- * Converts Vercel stream chunks into the content shape used by doGenerate results.
123
+ * Never rejects: a stream that fails part-way is already consumed, so its chunks and its error
124
+ * are both returned for the replay to hand back to the caller.
71
125
  *
72
- * @param {Array<object>} chunks
73
- * @returns {Array<object>}
126
+ * @param {ReadableStream} stream
127
+ * @returns {Promise<{ chunks: Array<object>, error?: unknown }>}
74
128
  */
75
- function getStreamContent (chunks) {
76
- const toolCalls = []
77
- let text = ''
78
-
79
- for (const chunk of chunks) {
80
- if (chunk?.type === 'tool-call') {
81
- toolCalls.push(chunk)
82
- } else if (chunk?.type === 'text-delta') {
83
- text += chunk.delta ?? chunk.textDelta ?? ''
84
- }
129
+ function drainStream (stream) {
130
+ const chunks = []
131
+ const reader = stream.getReader()
132
+
133
+ function readAll () {
134
+ return reader.read().then(({ done, value }) => {
135
+ if (done) return { chunks }
136
+ chunks.push(value)
137
+ return readAll()
138
+ }, error => ({ chunks, error }))
85
139
  }
86
140
 
87
- if (toolCalls.length) return toolCalls
88
- return text ? [{ type: 'text', text }] : []
141
+ return readAll()
142
+ }
143
+
144
+ /**
145
+ * @param {Array<object>} chunks
146
+ * @param {unknown} [error]
147
+ * @returns {ReadableStream}
148
+ */
149
+ function replayChunks (chunks, error) {
150
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
151
+ return new ReadableStream({
152
+ start (controller) {
153
+ for (const chunk of chunks) {
154
+ controller.enqueue(chunk)
155
+ }
156
+ if (error) {
157
+ controller.error(error)
158
+ } else {
159
+ controller.close()
160
+ }
161
+ },
162
+ })
89
163
  }
90
164
 
91
165
  module.exports = { enable, disable }
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const log = require('../../log')
4
+
3
5
  const FILE_FALLBACK = '[file]'
4
6
  const IMAGE_FALLBACK = '[image]'
5
7
 
@@ -9,7 +11,13 @@ const IMAGE_FALLBACK = '[image]'
9
11
  */
10
12
  function stringifyIfNeeded (value) {
11
13
  if (value == null) return value
12
- return typeof value === 'string' ? value : JSON.stringify(value)
14
+ if (typeof value === 'string') return value
15
+
16
+ try {
17
+ return JSON.stringify(value)
18
+ } catch (error) {
19
+ log.debug('AIGuard: dropping an unserializable message field: %s', error.message)
20
+ }
13
21
  }
14
22
 
15
23
  /**
@@ -20,9 +28,29 @@ function stringifyOrEmpty (value) {
20
28
  return stringifyIfNeeded(value) ?? ''
21
29
  }
22
30
 
31
+ /**
32
+ * Runs `attempt`, returning its value. On failure, logs and returns `fallback` instead of
33
+ * throwing — this runs inside the caller's promise chain, so a bad payload must not break them.
34
+ *
35
+ * @template T
36
+ * @param {() => T} attempt
37
+ * @param {T} fallback
38
+ * @param {string} logMessage printf-style, with a single %s for the error message
39
+ * @returns {T}
40
+ */
41
+ function decode (attempt, fallback, logMessage) {
42
+ try {
43
+ return attempt()
44
+ } catch (error) {
45
+ log.error(logMessage, error.message)
46
+ return fallback
47
+ }
48
+ }
49
+
23
50
  module.exports = {
24
51
  FILE_FALLBACK,
25
52
  IMAGE_FALLBACK,
26
53
  stringifyIfNeeded,
27
54
  stringifyOrEmpty,
55
+ decode,
28
56
  }
@@ -180,7 +180,32 @@ function buildOutputMessages (inputMessages, content) {
180
180
  return []
181
181
  }
182
182
 
183
+ /**
184
+ * Converts Vercel stream chunks into the content shape a doGenerate result carries.
185
+ *
186
+ * @param {Array<object>} chunks
187
+ * @returns {Array<object>}
188
+ */
189
+ function getStreamedContent (chunks) {
190
+ const toolCalls = []
191
+ let text = ''
192
+
193
+ for (const chunk of chunks) {
194
+ if (chunk?.type === 'tool-call') {
195
+ toolCalls.push(chunk)
196
+ } else if (chunk?.type === 'text-delta') {
197
+ // AI SDK 6 renamed `textDelta` to `delta`; both shapes are supported. Coalesced with `??`
198
+ // because a missing delta must contribute nothing, not "undefined".
199
+ text += chunk.delta ?? chunk.textDelta ?? ''
200
+ }
201
+ }
202
+
203
+ if (toolCalls.length) return toolCalls
204
+ return text ? [{ type: 'text', text }] : []
205
+ }
206
+
183
207
  module.exports = {
208
+ getStreamedContent,
184
209
  convertVercelPromptToMessages,
185
210
  convertFilePartToImageUrl,
186
211
  buildToolCallOutputMessages,
@@ -2,7 +2,9 @@
2
2
 
3
3
  const log = require('../../../../../log')
4
4
 
5
- const COMMAND_PATTERN = String.raw`^(?:\s*(?:sudo|doas)\s+)?\b\S+\b\s(.*)`
5
+ // `\S+\s` accepts command tokens that end in punctuation. The optional prefix uses horizontal
6
+ // whitespace so multiline matching cannot start inside a line-terminator run.
7
+ const COMMAND_PATTERN = String.raw`^(?:[ \t]*(?:sudo|doas)[ \t]+)?\S+\s([\s\S]*)`
6
8
  const pattern = new RegExp(COMMAND_PATTERN, 'gmi')
7
9
 
8
10
  module.exports = function extractSensitiveRanges (evidence) {