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.
- package/README.md +15 -0
- package/ci/test-optimization-validation/framework-adapters/cucumber.js +4 -2
- package/ci/test-optimization-validation/package-check.js +4 -1
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +1 -1
- package/index.d.ts +12 -0
- package/next.d.ts +27 -0
- package/next.js +3 -0
- package/package.json +17 -13
- package/packages/datadog-esbuild/index.js +14 -1
- package/packages/datadog-esbuild/src/resolver.js +284 -0
- package/packages/datadog-esbuild/src/utils.js +163 -119
- package/packages/datadog-instrumentations/src/ai.js +39 -97
- package/packages/datadog-instrumentations/src/anthropic.js +76 -134
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +1 -1
- package/packages/datadog-instrumentations/src/azure-cosmos.js +1 -1
- package/packages/datadog-instrumentations/src/azure-durable-functions.js +23 -9
- package/packages/datadog-instrumentations/src/azure-functions.js +48 -0
- package/packages/datadog-instrumentations/src/bullmq.js +1 -1
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
- package/packages/datadog-instrumentations/src/dd-trace-api.js +2 -2
- package/packages/datadog-instrumentations/src/graphql.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/bundler-register.js +39 -14
- package/packages/datadog-instrumentations/src/helpers/instrument.js +25 -8
- package/packages/datadog-instrumentations/src/helpers/instrumentation-utils.js +72 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +9 -64
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +46 -14
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/azure-durable-functions.js +17 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +1 -5
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +6 -7
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +99 -70
- package/packages/datadog-instrumentations/src/jest.js +141 -25
- package/packages/datadog-instrumentations/src/langchain.js +1 -1
- package/packages/datadog-instrumentations/src/langgraph.js +1 -1
- package/packages/datadog-instrumentations/src/mercurius.js +1 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +41 -5
- package/packages/datadog-instrumentations/src/modelcontextprotocol-sdk.js +1 -1
- package/packages/datadog-instrumentations/src/openai-agents.js +1 -1
- package/packages/datadog-instrumentations/src/openai.js +54 -76
- package/packages/datadog-instrumentations/src/playwright.js +4 -0
- package/packages/datadog-instrumentations/src/vitest-worker.js +7 -10
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +7 -1
- package/packages/datadog-plugin-azure-cosmos/src/index.js +1 -1
- package/packages/datadog-plugin-azure-durable-functions/src/index.js +96 -1
- package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -24
- package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +2 -36
- package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-request-context.js +31 -0
- package/packages/datadog-plugin-graphql/src/resolve-error.js +33 -11
- package/packages/datadog-turbopack/index.js +212 -0
- package/packages/datadog-turbopack/src/constants.js +5 -0
- package/packages/datadog-turbopack/src/loader.js +500 -0
- package/packages/dd-trace/src/aiguard/index.js +2 -1
- package/packages/dd-trace/src/aiguard/integrations/anthropic.js +53 -25
- package/packages/dd-trace/src/aiguard/integrations/evaluate.js +49 -25
- package/packages/dd-trace/src/aiguard/integrations/index.js +3 -2
- package/packages/dd-trace/src/aiguard/integrations/openai.js +59 -30
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +116 -42
- package/packages/dd-trace/src/aiguard/messages/utils.js +29 -1
- package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +25 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +3 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +531 -70
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +2 -2
- package/packages/dd-trace/src/appsec/telemetry/api_security.js +1 -3
- package/packages/dd-trace/src/appsec/telemetry/common.js +4 -0
- package/packages/dd-trace/src/appsec/telemetry/index.js +23 -5
- package/packages/dd-trace/src/appsec/telemetry/rasp.js +1 -20
- package/packages/dd-trace/src/appsec/telemetry/user.js +1 -3
- package/packages/dd-trace/src/appsec/telemetry/waf.js +1 -20
- package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -0
- package/packages/dd-trace/src/ci-visibility/requests/video-request.js +4 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
- package/packages/dd-trace/src/config/helper.js +10 -2
- package/packages/dd-trace/src/config/index.js +20 -5
- package/packages/dd-trace/src/config/supported-configurations.json +10 -0
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -7
- package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +14 -1
- package/packages/dd-trace/src/debugger/devtools_client/request-options.js +1 -6
- package/packages/dd-trace/src/evp_proxy/direct.js +2 -28
- package/packages/dd-trace/src/evp_proxy/discovery.js +3 -4
- package/packages/dd-trace/src/evp_proxy/path.js +12 -3
- package/packages/dd-trace/src/exporters/agentless/writer.js +3 -1
- package/packages/dd-trace/src/exporters/common/docker.js +4 -1
- package/packages/dd-trace/src/exporters/common/proxy.js +55 -0
- package/packages/dd-trace/src/exporters/common/request.js +11 -3
- package/packages/dd-trace/src/opentelemetry/metrics/index.js +1 -1
- package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +5 -4
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +106 -25
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +12 -1
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +8 -2
- package/packages/dd-trace/src/plugin_manager.js +9 -3
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/util/test.js +4 -1
- package/packages/dd-trace/src/priority_sampler.js +4 -0
- package/packages/dd-trace/src/sampling_rule.js +3 -1
- package/packages/dd-trace/src/span_processor.js +18 -3
- package/packages/dd-trace/src/telemetry/send-data.js +5 -4
- 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
|
-
*
|
|
6
|
+
* Runs one evaluation per conversation and rejects if any of them says block.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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
|
|
20
|
-
if (!
|
|
18
|
+
function evaluate (interceptCtx, aiguard, conversations, opts) {
|
|
19
|
+
if (!conversations?.length) return
|
|
21
20
|
|
|
22
|
-
const
|
|
21
|
+
const parentSpan = interceptCtx?.tracingContext?.currentStore?.span
|
|
22
|
+
const evaluateOpts = parentSpan ? { ...opts, childOf: parentSpan } : opts
|
|
23
|
+
const evaluations = []
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
*
|
|
35
|
+
* Turns a synchronous throw into a rejection, so every outcome is inspected the same way.
|
|
33
36
|
*
|
|
34
|
-
* @param {object}
|
|
35
|
-
* @param {
|
|
36
|
-
* @param {
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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 = {
|
|
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 {
|
|
13
|
+
const { evaluate } = require('./evaluate')
|
|
13
14
|
|
|
14
|
-
const
|
|
15
|
-
const
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
57
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
73
|
-
const inputMessages = getResponsesInputMessages(ctx.
|
|
81
|
+
function onResponses (ctx) {
|
|
82
|
+
const inputMessages = getResponsesInputMessages(ctx.arguments?.[0])
|
|
74
83
|
if (!inputMessages?.length) return
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 {
|
|
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 {
|
|
12
|
+
const { evaluate } = require('./evaluate')
|
|
8
13
|
|
|
9
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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 {
|
|
73
|
-
* @returns {Array<object>}
|
|
126
|
+
* @param {ReadableStream} stream
|
|
127
|
+
* @returns {Promise<{ chunks: Array<object>, error?: unknown }>}
|
|
74
128
|
*/
|
|
75
|
-
function
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|