datadog 2.42.0 → 2.44.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -2
- data/README.md +3 -0
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +46 -5
- data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +6 -7
- data/ext/datadog_profiling_native_extension/collectors_stack.c +6 -7
- data/ext/datadog_profiling_native_extension/collectors_stack.h +1 -1
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +80 -36
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -2
- data/ext/datadog_profiling_native_extension/extconf.rb +4 -2
- data/ext/datadog_profiling_native_extension/heap_recorder.c +13 -0
- data/ext/datadog_profiling_native_extension/heap_recorder.h +4 -0
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +34 -2
- data/ext/datadog_profiling_native_extension/private_vm_api_access.h +1 -0
- data/ext/datadog_profiling_native_extension/profiling.c +1 -0
- data/ext/datadog_profiling_native_extension/stack_recorder.c +17 -4
- data/ext/datadog_profiling_native_extension/time_helpers.h +5 -4
- data/ext/libdatadog_api/di.c +8 -3
- data/ext/libdatadog_api/extconf.rb +1 -1
- data/ext/libdatadog_api/feature_flags.c +8 -0
- data/ext/libdatadog_api/init.c +3 -1
- data/ext/libdatadog_api/otel_thread_context.c +51 -15
- data/ext/libdatadog_api/trace_exporter.c +642 -22
- data/ext/libdatadog_extconf_helpers.rb +14 -9
- data/lib/datadog/ai_guard/component.rb +17 -8
- data/lib/datadog/ai_guard/configuration/ext.rb +2 -0
- data/lib/datadog/ai_guard/configuration.rb +13 -0
- data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +48 -55
- data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +1 -1
- data/lib/datadog/ai_guard/contrib/ruby_llm/message_adapter.rb +76 -0
- data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +1 -0
- data/lib/datadog/ai_guard/contrib/ruby_llm/redaction_change_set.rb +135 -0
- data/lib/datadog/ai_guard/evaluation/client.rb +50 -0
- data/lib/datadog/ai_guard/evaluation/content_part.rb +12 -0
- data/lib/datadog/ai_guard/evaluation/message.rb +25 -7
- data/lib/datadog/ai_guard/evaluation/message_builder.rb +31 -0
- data/lib/datadog/ai_guard/evaluation/no_op_result.rb +6 -6
- data/lib/datadog/ai_guard/evaluation/outcome.rb +24 -0
- data/lib/datadog/ai_guard/evaluation/request.rb +4 -60
- data/lib/datadog/ai_guard/evaluation/response.rb +33 -0
- data/lib/datadog/ai_guard/evaluation/result.rb +11 -18
- data/lib/datadog/ai_guard/evaluation/tool_call.rb +15 -2
- data/lib/datadog/ai_guard/evaluation.rb +25 -11
- data/lib/datadog/ai_guard/ext.rb +1 -0
- data/lib/datadog/ai_guard/{api_client.rb → http_client.rb} +6 -4
- data/lib/datadog/ai_guard/metrics/telemetry.rb +43 -0
- data/lib/datadog/ai_guard/redaction/replacements.rb +97 -0
- data/lib/datadog/ai_guard/redaction/result.rb +29 -0
- data/lib/datadog/ai_guard/redaction.rb +80 -0
- data/lib/datadog/ai_guard.rb +58 -57
- data/lib/datadog/core/configuration/components.rb +9 -1
- data/lib/datadog/core/configuration/settings.rb +5 -2
- data/lib/datadog/core/configuration/supported_configurations.rb +8 -0
- data/lib/datadog/core/remote/client/capabilities.rb +10 -4
- data/lib/datadog/core/remote/component.rb +9 -2
- data/lib/datadog/core/remote/worker.rb +8 -0
- data/lib/datadog/di/instrumenter.rb +1 -1
- data/lib/datadog/di/probe_manager.rb +10 -5
- data/lib/datadog/di/proc_responder.rb +1 -1
- data/lib/datadog/open_feature/agentless/configuration_source.rb +246 -0
- data/lib/datadog/open_feature/agentless/parser.rb +43 -0
- data/lib/datadog/open_feature/agentless/response.rb +19 -0
- data/lib/datadog/open_feature/agentless/transport.rb +103 -0
- data/lib/datadog/open_feature/component.rb +57 -1
- data/lib/datadog/open_feature/configuration/agentless_endpoint.rb +90 -0
- data/lib/datadog/open_feature/configuration/source/resolution.rb +22 -0
- data/lib/datadog/open_feature/configuration/source.rb +43 -0
- data/lib/datadog/open_feature/configuration.rb +118 -0
- data/lib/datadog/open_feature/native_evaluator.rb +1 -14
- data/lib/datadog/open_feature/remote.rb +5 -5
- data/lib/datadog/open_feature/transport.rb +5 -0
- data/lib/datadog/opentelemetry/signal_configuration.rb +13 -9
- data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +2 -2
- data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
- data/lib/datadog/profiling/collectors/info.rb +2 -6
- data/lib/datadog/symbol_database/extractor.rb +84 -102
- data/lib/datadog/tracing/component.rb +2 -0
- data/lib/datadog/tracing/configuration/ext.rb +1 -0
- data/lib/datadog/tracing/configuration/settings.rb +6 -0
- data/lib/datadog/tracing/context.rb +24 -2
- data/lib/datadog/tracing/context_provider.rb +9 -5
- data/lib/datadog/tracing/contrib/grape/endpoint.rb +32 -5
- data/lib/datadog/tracing/contrib/sequel/jdbc_connection_string.rb +140 -0
- data/lib/datadog/tracing/contrib/sequel/utils.rb +49 -62
- data/lib/datadog/tracing/metadata/ext.rb +3 -0
- data/lib/datadog/tracing/otel_thread_context.rb +78 -9
- data/lib/datadog/tracing/tracer.rb +15 -2
- data/lib/datadog/tracing/transport/native.rb +18 -26
- data/lib/datadog/tracing/transport/span_events_negotiation.rb +41 -0
- data/lib/datadog/tracing/transport/trace_formatter.rb +13 -3
- data/lib/datadog/tracing/transport/traces.rb +3 -23
- data/lib/datadog/version.rb +1 -1
- metadata +25 -7
- data/lib/datadog/ai_guard/evaluation/content_builder.rb +0 -31
|
@@ -5,14 +5,18 @@ module Datadog
|
|
|
5
5
|
module Evaluation
|
|
6
6
|
# Class for emulating AI Guard evaluation result when AI Guard is disabled.
|
|
7
7
|
class NoOpResult
|
|
8
|
-
attr_reader :action, :reason, :tags, :sds_findings, :
|
|
8
|
+
attr_reader :action, :reason, :tags, :sds_findings, :redaction_replacements,
|
|
9
|
+
:tag_probabilities, :messages
|
|
9
10
|
|
|
10
|
-
def initialize
|
|
11
|
+
def initialize(messages)
|
|
11
12
|
@action = Result::ALLOW_ACTION
|
|
12
13
|
@reason = "AI Guard is disabled"
|
|
14
|
+
|
|
13
15
|
@tags = []
|
|
16
|
+
@messages = messages
|
|
14
17
|
@sds_findings = []
|
|
15
18
|
@tag_probabilities = {}
|
|
19
|
+
@redaction_replacements = []
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
def allow?
|
|
@@ -26,10 +30,6 @@ module Datadog
|
|
|
26
30
|
def abort?
|
|
27
31
|
false
|
|
28
32
|
end
|
|
29
|
-
|
|
30
|
-
def blocking_enabled?
|
|
31
|
-
false
|
|
32
|
-
end
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module AIGuard
|
|
5
|
+
module Evaluation
|
|
6
|
+
# Internal outcome of an evaluation request
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
class Outcome
|
|
10
|
+
attr_reader :result, :redaction
|
|
11
|
+
|
|
12
|
+
def initialize(result:, redaction:, blocking_enabled:)
|
|
13
|
+
@result = result
|
|
14
|
+
@redaction = redaction
|
|
15
|
+
@blocking_enabled = blocking_enabled
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def block?
|
|
19
|
+
@blocking_enabled && (result.deny? || result.abort?)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -3,37 +3,19 @@
|
|
|
3
3
|
module Datadog
|
|
4
4
|
module AIGuard
|
|
5
5
|
module Evaluation
|
|
6
|
-
# Request builds the request body
|
|
6
|
+
# Request builds the request body for the AI Guard /evaluate endpoint
|
|
7
7
|
class Request
|
|
8
8
|
REQUEST_PATH = "/evaluate"
|
|
9
9
|
|
|
10
|
-
attr_reader :serialized_messages
|
|
11
|
-
|
|
12
10
|
def initialize(messages)
|
|
13
|
-
@
|
|
11
|
+
@messages = messages
|
|
14
12
|
end
|
|
15
13
|
|
|
16
|
-
def
|
|
17
|
-
api_client = AIGuard.api_client
|
|
18
|
-
|
|
19
|
-
# This should never happen, as we are only calling this method when AI Guard is enabled,
|
|
20
|
-
# and this means the API Client was not initialized properly.
|
|
21
|
-
#
|
|
22
|
-
# Please report this at https://github.com/datadog/dd-trace-rb/blob/master/CONTRIBUTING.md#found-a-bug
|
|
23
|
-
raise "AI Guard API Client not initialized" unless api_client
|
|
24
|
-
|
|
25
|
-
raw_response = api_client.post(REQUEST_PATH, body: build_request_body)
|
|
26
|
-
|
|
27
|
-
Result.new(raw_response)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
private
|
|
31
|
-
|
|
32
|
-
def build_request_body
|
|
14
|
+
def body
|
|
33
15
|
{
|
|
34
16
|
data: {
|
|
35
17
|
attributes: {
|
|
36
|
-
messages: @
|
|
18
|
+
messages: @messages.map(&:to_h),
|
|
37
19
|
meta: {
|
|
38
20
|
service: Datadog.configuration.service,
|
|
39
21
|
env: Datadog.configuration.env,
|
|
@@ -42,44 +24,6 @@ module Datadog
|
|
|
42
24
|
},
|
|
43
25
|
}
|
|
44
26
|
end
|
|
45
|
-
|
|
46
|
-
def serialize_messages(messages)
|
|
47
|
-
messages.map { |message| serialize_message(message) }
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def serialize_message(message)
|
|
51
|
-
if message.tool_call
|
|
52
|
-
{
|
|
53
|
-
role: message.role,
|
|
54
|
-
tool_calls: [
|
|
55
|
-
{
|
|
56
|
-
id: message.tool_call.id,
|
|
57
|
-
function: {
|
|
58
|
-
name: message.tool_call.tool_name,
|
|
59
|
-
arguments: message.tool_call.arguments,
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
}
|
|
64
|
-
elsif message.content.is_a?(::Array)
|
|
65
|
-
{role: message.role, content: serialize_content_parts(message.content)}
|
|
66
|
-
elsif message.tool_call_id
|
|
67
|
-
{role: message.role, tool_call_id: message.tool_call_id, content: message.content}
|
|
68
|
-
else
|
|
69
|
-
{role: message.role, content: message.content}
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def serialize_content_parts(parts)
|
|
74
|
-
parts.map do |part|
|
|
75
|
-
case part
|
|
76
|
-
when ContentPart::Text
|
|
77
|
-
{type: "text", text: part.text}
|
|
78
|
-
when ContentPart::ImageURL
|
|
79
|
-
{type: "image_url", image_url: {url: part.url}}
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
27
|
end
|
|
84
28
|
end
|
|
85
29
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module AIGuard
|
|
5
|
+
module Evaluation
|
|
6
|
+
# Wrapper class for evaluation API response
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
class Response
|
|
10
|
+
attr_reader :action, :reason, :tags, :sds_findings, :tag_probabilities,
|
|
11
|
+
:redaction_replacements
|
|
12
|
+
|
|
13
|
+
def initialize(raw_response)
|
|
14
|
+
attributes = raw_response.fetch("data").fetch("attributes")
|
|
15
|
+
|
|
16
|
+
@action = attributes.fetch("action")
|
|
17
|
+
@reason = attributes.fetch("reason")
|
|
18
|
+
@tags = attributes.fetch("tags")
|
|
19
|
+
@tag_probabilities = attributes.fetch("tag_probs")
|
|
20
|
+
@is_blocking_enabled = attributes.fetch("is_blocking_enabled")
|
|
21
|
+
@sds_findings = attributes.fetch("sds_findings", [])
|
|
22
|
+
@redaction_replacements = attributes.fetch("redaction_replacements", [])
|
|
23
|
+
rescue KeyError => e
|
|
24
|
+
raise AIGuardClientError, "Missing key: \"#{e.key}\""
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def blocking_enabled?
|
|
28
|
+
!!@is_blocking_enabled
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -3,25 +3,22 @@
|
|
|
3
3
|
module Datadog
|
|
4
4
|
module AIGuard
|
|
5
5
|
module Evaluation
|
|
6
|
-
# Wrapper class for evaluation API response
|
|
7
6
|
class Result
|
|
8
7
|
ALLOW_ACTION = "ALLOW"
|
|
9
8
|
DENY_ACTION = "DENY"
|
|
10
9
|
ABORT_ACTION = "ABORT"
|
|
11
10
|
|
|
12
|
-
attr_reader :action, :reason, :tags, :sds_findings,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@action =
|
|
18
|
-
@reason =
|
|
19
|
-
@tags =
|
|
20
|
-
@
|
|
21
|
-
@
|
|
22
|
-
@
|
|
23
|
-
rescue KeyError => e
|
|
24
|
-
raise AIGuardClientError, "Missing key: \"#{e.key}\""
|
|
11
|
+
attr_reader :messages, :action, :reason, :tags, :sds_findings,
|
|
12
|
+
:tag_probabilities, :redaction_replacements
|
|
13
|
+
|
|
14
|
+
def initialize(messages, action:, reason:, tags:, sds_findings:, tag_probabilities:, redaction_replacements:)
|
|
15
|
+
@messages = messages
|
|
16
|
+
@action = action
|
|
17
|
+
@reason = reason
|
|
18
|
+
@tags = tags
|
|
19
|
+
@sds_findings = sds_findings
|
|
20
|
+
@tag_probabilities = tag_probabilities
|
|
21
|
+
@redaction_replacements = redaction_replacements
|
|
25
22
|
end
|
|
26
23
|
|
|
27
24
|
def allow?
|
|
@@ -35,10 +32,6 @@ module Datadog
|
|
|
35
32
|
def abort?
|
|
36
33
|
action == ABORT_ACTION
|
|
37
34
|
end
|
|
38
|
-
|
|
39
|
-
def blocking_enabled?
|
|
40
|
-
!!@is_blocking_enabled
|
|
41
|
-
end
|
|
42
35
|
end
|
|
43
36
|
end
|
|
44
37
|
end
|
|
@@ -3,14 +3,27 @@
|
|
|
3
3
|
module Datadog
|
|
4
4
|
module AIGuard
|
|
5
5
|
module Evaluation
|
|
6
|
-
#
|
|
6
|
+
# Represents a tool invocation requested by an assistant message
|
|
7
7
|
class ToolCall
|
|
8
8
|
attr_reader :tool_name, :id, :arguments
|
|
9
9
|
|
|
10
10
|
def initialize(tool_name, id:, arguments:)
|
|
11
11
|
@tool_name = tool_name
|
|
12
12
|
@id = id
|
|
13
|
-
@arguments =
|
|
13
|
+
@arguments =
|
|
14
|
+
case arguments
|
|
15
|
+
when String then arguments
|
|
16
|
+
when Hash then JSON.generate(arguments)
|
|
17
|
+
else raise ArgumentError, "Tool call arguments must be a String or Hash"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def copy(arguments:)
|
|
22
|
+
ToolCall.new(tool_name, id: id, arguments: arguments)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_h
|
|
26
|
+
{id: id, function: {name: tool_name, arguments: arguments}}
|
|
14
27
|
end
|
|
15
28
|
end
|
|
16
29
|
end
|
|
@@ -25,37 +25,51 @@ module Datadog
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
if (last_message = messages.last)
|
|
28
|
-
if last_message.
|
|
28
|
+
if (tool_call = last_message.tool_calls.first)
|
|
29
29
|
span.set_tag(Ext::TARGET_TAG, "tool")
|
|
30
|
-
span.set_tag(Ext::TOOL_NAME_TAG,
|
|
30
|
+
span.set_tag(Ext::TOOL_NAME_TAG, tool_call.tool_name)
|
|
31
31
|
elsif last_message.tool_call_id
|
|
32
32
|
span.set_tag(Ext::TARGET_TAG, "tool")
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
messages.reverse_each do |message|
|
|
35
|
+
# @type var tool_call: ToolCall?
|
|
36
|
+
tool_call = message.tool_calls.find { |call| call.id == last_message.tool_call_id }
|
|
37
|
+
break span.set_tag(Ext::TOOL_NAME_TAG, tool_call.tool_name) if tool_call
|
|
36
38
|
end
|
|
37
39
|
else
|
|
38
40
|
span.set_tag(Ext::TARGET_TAG, "prompt")
|
|
39
41
|
end
|
|
40
42
|
end
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
outcome =
|
|
45
|
+
begin
|
|
46
|
+
Client.evaluate(messages)
|
|
47
|
+
rescue
|
|
48
|
+
Metrics::Telemetry.report_error
|
|
49
|
+
raise
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
result = outcome.result
|
|
53
|
+
redaction = outcome.redaction
|
|
44
54
|
|
|
45
55
|
span.set_tag(Ext::ACTION_TAG, result.action)
|
|
46
56
|
span.set_tag(Ext::REASON_TAG, result.reason)
|
|
57
|
+
span.set_tag(Ext::REDACTED_TAG, redaction.redacted?) if redaction.performed?
|
|
47
58
|
|
|
48
59
|
span.set_metastruct_tag(
|
|
49
60
|
Ext::METASTRUCT_TAG,
|
|
50
61
|
{
|
|
51
|
-
messages: truncate_content(truncate_messages(
|
|
62
|
+
messages: truncate_content(truncate_messages(result.messages).map(&:to_h)),
|
|
52
63
|
attack_categories: result.tags,
|
|
53
64
|
sds: result.sds_findings,
|
|
54
65
|
tag_probs: result.tag_probabilities,
|
|
55
66
|
}
|
|
56
67
|
)
|
|
57
68
|
|
|
58
|
-
|
|
69
|
+
blocked = allow_raise && outcome.block?
|
|
70
|
+
Metrics::Telemetry.report_evaluation(outcome, blocked: blocked)
|
|
71
|
+
|
|
72
|
+
if blocked
|
|
59
73
|
span.set_tag(Ext::BLOCKED_TAG, true)
|
|
60
74
|
raise AIGuardAbortError.new(action: result.action, reason: result.reason, tags: result.tags)
|
|
61
75
|
end
|
|
@@ -64,10 +78,10 @@ module Datadog
|
|
|
64
78
|
end
|
|
65
79
|
end
|
|
66
80
|
|
|
67
|
-
def perform_no_op
|
|
81
|
+
def perform_no_op(messages)
|
|
68
82
|
AIGuard.logger&.warn("AI Guard is disabled, messages were not evaluated")
|
|
69
83
|
|
|
70
|
-
NoOpResult.new
|
|
84
|
+
NoOpResult.new(messages)
|
|
71
85
|
end
|
|
72
86
|
|
|
73
87
|
private
|
|
@@ -82,7 +96,7 @@ module Datadog
|
|
|
82
96
|
def truncate_content(serialized_messages)
|
|
83
97
|
max_bytes = Datadog.configuration.ai_guard.max_content_size_bytes
|
|
84
98
|
|
|
85
|
-
serialized_messages.map do |message|
|
|
99
|
+
serialized_messages.map do |message|
|
|
86
100
|
next message unless message[:content]
|
|
87
101
|
|
|
88
102
|
if message[:content].is_a?(::Array)
|
data/lib/datadog/ai_guard/ext.rb
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "uri"
|
|
4
|
-
require "net/http"
|
|
5
4
|
require "json"
|
|
5
|
+
require "net/http"
|
|
6
6
|
|
|
7
7
|
module Datadog
|
|
8
8
|
module AIGuard
|
|
9
|
-
#
|
|
10
|
-
# Uses net/http to perform
|
|
11
|
-
|
|
9
|
+
# HTTP client for the AI Guard API
|
|
10
|
+
# Uses net/http to perform requests, raises on client and server errors
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
class HTTPClient
|
|
12
14
|
DEFAULT_SITE = "app.datadoghq.com"
|
|
13
15
|
DEFAULT_PATH = "/api/v2/ai-guard"
|
|
14
16
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module AIGuard
|
|
5
|
+
# Namespace for metrics
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
module Metrics
|
|
9
|
+
# A module for telemetry metrics reporting
|
|
10
|
+
module Telemetry
|
|
11
|
+
NAMESPACE = "ai_guard"
|
|
12
|
+
CLIENT_ERROR = "client_error"
|
|
13
|
+
REDACTION_ERROR = "redaction_error"
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def report_evaluation(outcome, blocked:)
|
|
18
|
+
telemetry = AIGuard.telemetry
|
|
19
|
+
return unless telemetry
|
|
20
|
+
|
|
21
|
+
result = outcome.result
|
|
22
|
+
redaction = outcome.redaction
|
|
23
|
+
|
|
24
|
+
tags = {action: result.action, block: blocked.to_s, error: "false"}
|
|
25
|
+
tags[:redacted] = redaction.redacted?.to_s if redaction.performed?
|
|
26
|
+
|
|
27
|
+
telemetry.inc(NAMESPACE, "requests", 1, tags: tags)
|
|
28
|
+
if redaction.failures_count.positive?
|
|
29
|
+
telemetry.inc(NAMESPACE, "error", redaction.failures_count, tags: {type: REDACTION_ERROR})
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def report_error
|
|
34
|
+
telemetry = AIGuard.telemetry
|
|
35
|
+
return unless telemetry
|
|
36
|
+
|
|
37
|
+
telemetry.inc(NAMESPACE, "requests", 1, tags: {error: "true"})
|
|
38
|
+
telemetry.inc(NAMESPACE, "error", 1, tags: {type: CLIENT_ERROR})
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
module Datadog
|
|
6
|
+
module AIGuard
|
|
7
|
+
module Redaction
|
|
8
|
+
# Converts raw backend redaction replacements into a conflict-free
|
|
9
|
+
# path-to-replacement map
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
class Replacements
|
|
13
|
+
extend Forwardable
|
|
14
|
+
|
|
15
|
+
TEXT_PATH_PATTERN = /\Amessages\[([0-9]+)\]\.content\[([0-9]+)\]\.text\z/
|
|
16
|
+
CONTENT_PATH_PATTERN = /\Amessages\[([0-9]+)\]\.content\z/
|
|
17
|
+
ARGUMENTS_PATH_PATTERN = /\Amessages\[([0-9]+)\]\.tool_calls\[([0-9]+)\]\.function\.arguments\z/
|
|
18
|
+
|
|
19
|
+
def_delegators :@replacements, :each, :empty?
|
|
20
|
+
|
|
21
|
+
attr_reader :failures_count
|
|
22
|
+
|
|
23
|
+
def initialize(raw_replacements)
|
|
24
|
+
@failures_count = 0
|
|
25
|
+
@replacements = build(raw_replacements)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# NOTE: Identical duplicates collapse into a single entry,
|
|
31
|
+
# conflicting replacements remove the path and record one failure,
|
|
32
|
+
# and malformed or unsupported entries are skipped and counted individually
|
|
33
|
+
#
|
|
34
|
+
# Examples
|
|
35
|
+
#
|
|
36
|
+
# translates this
|
|
37
|
+
#
|
|
38
|
+
# [
|
|
39
|
+
# {"path" => "messages[0].content", "replacement" => "<redacted>"},
|
|
40
|
+
# {"path" => "messages[1].content[2].text", "replacement" => "<redacted>"},
|
|
41
|
+
# {"path" => "messages[2].tool_calls[0].function.arguments", "replacement" => "{}"}
|
|
42
|
+
# ]
|
|
43
|
+
#
|
|
44
|
+
# into this
|
|
45
|
+
#
|
|
46
|
+
# {
|
|
47
|
+
# [0, :content] => "<redacted>",
|
|
48
|
+
# [1, :text, 2] => "<redacted>",
|
|
49
|
+
# [2, :arguments, 0] => "{}"
|
|
50
|
+
# }
|
|
51
|
+
def build(raw_replacements)
|
|
52
|
+
unless raw_replacements.is_a?(::Array)
|
|
53
|
+
@failures_count += 1
|
|
54
|
+
return {}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
conflicting = {}
|
|
58
|
+
raw_replacements.each_with_object({}) do |entry, replacements|
|
|
59
|
+
next @failures_count += 1 unless entry.is_a?(::Hash)
|
|
60
|
+
|
|
61
|
+
raw_path = entry["path"]
|
|
62
|
+
replacement = entry["replacement"]
|
|
63
|
+
|
|
64
|
+
if !raw_path.is_a?(::String) || raw_path.empty? || !replacement.is_a?(::String)
|
|
65
|
+
next @failures_count += 1
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @type var path: Replacements::path?
|
|
69
|
+
path =
|
|
70
|
+
case raw_path
|
|
71
|
+
when CONTENT_PATH_PATTERN
|
|
72
|
+
[Regexp.last_match(1).to_i, :content]
|
|
73
|
+
when TEXT_PATH_PATTERN
|
|
74
|
+
[Regexp.last_match(1).to_i, :text, Regexp.last_match(2).to_i]
|
|
75
|
+
when ARGUMENTS_PATH_PATTERN
|
|
76
|
+
[Regexp.last_match(1).to_i, :arguments, Regexp.last_match(2).to_i]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
next @failures_count += 1 unless path
|
|
80
|
+
next if conflicting.key?(path)
|
|
81
|
+
|
|
82
|
+
if replacements.key?(path)
|
|
83
|
+
next if replacements[path] == replacement
|
|
84
|
+
|
|
85
|
+
replacements.delete(path)
|
|
86
|
+
conflicting[path] = true
|
|
87
|
+
|
|
88
|
+
next @failures_count += 1
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
replacements[path] = replacement
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module AIGuard
|
|
5
|
+
module Redaction
|
|
6
|
+
# A result of applying redaction replacements
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
class Result
|
|
10
|
+
attr_reader :messages, :applied_count, :failures_count
|
|
11
|
+
|
|
12
|
+
def initialize(messages, applied_count:, failures_count:, performed: true)
|
|
13
|
+
@messages = messages
|
|
14
|
+
@applied_count = applied_count
|
|
15
|
+
@failures_count = failures_count
|
|
16
|
+
@performed = performed
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def performed?
|
|
20
|
+
@performed
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def redacted?
|
|
24
|
+
applied_count.positive?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module AIGuard
|
|
5
|
+
# Sensitive data redaction for evaluation messages
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
module Redaction
|
|
9
|
+
class << self
|
|
10
|
+
def skip(messages)
|
|
11
|
+
Redaction::Result.new(messages, applied_count: 0, failures_count: 0, performed: false)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def perform(messages, replacements:)
|
|
15
|
+
redaction_replacements = Replacements.new(replacements)
|
|
16
|
+
failures_count = redaction_replacements.failures_count
|
|
17
|
+
|
|
18
|
+
if redaction_replacements.empty?
|
|
19
|
+
return Redaction::Result.new(messages, applied_count: 0, failures_count: failures_count)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
applied_count = 0
|
|
23
|
+
redacted_messages = messages.dup
|
|
24
|
+
|
|
25
|
+
redaction_replacements.each do |path, replacement|
|
|
26
|
+
index = path[0]
|
|
27
|
+
redacted_message = redact(redacted_messages[index], path: path, replacement: replacement)
|
|
28
|
+
|
|
29
|
+
next failures_count += 1 unless redacted_message
|
|
30
|
+
|
|
31
|
+
redacted_messages[index] = redacted_message
|
|
32
|
+
applied_count += 1
|
|
33
|
+
rescue
|
|
34
|
+
failures_count += 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
Redaction::Result.new(
|
|
38
|
+
redacted_messages, applied_count: applied_count, failures_count: failures_count
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def redact(message, path:, replacement:)
|
|
45
|
+
return unless message.is_a?(Evaluation::Message)
|
|
46
|
+
|
|
47
|
+
_, kind, index = path
|
|
48
|
+
|
|
49
|
+
case kind
|
|
50
|
+
when :content
|
|
51
|
+
return unless message.content.is_a?(::String)
|
|
52
|
+
|
|
53
|
+
message.copy(content: replacement)
|
|
54
|
+
when :text
|
|
55
|
+
# @type var index: Integer
|
|
56
|
+
content = message.content
|
|
57
|
+
return unless content.is_a?(::Array)
|
|
58
|
+
|
|
59
|
+
part = content[index]
|
|
60
|
+
return if !part.is_a?(Evaluation::ContentPart::Text) || !part.text.is_a?(::String)
|
|
61
|
+
|
|
62
|
+
redacted_content = ::Array.new(content)
|
|
63
|
+
redacted_content[index] = part.copy(text: replacement)
|
|
64
|
+
|
|
65
|
+
message.copy(content: redacted_content)
|
|
66
|
+
when :arguments
|
|
67
|
+
# @type var index: Integer
|
|
68
|
+
tool_call = message.tool_calls[index]
|
|
69
|
+
return unless tool_call && tool_call.arguments.is_a?(::String)
|
|
70
|
+
|
|
71
|
+
redacted_tool_calls = ::Array.new(message.tool_calls)
|
|
72
|
+
redacted_tool_calls[index] = tool_call.copy(arguments: replacement)
|
|
73
|
+
|
|
74
|
+
message.copy(tool_calls: redacted_tool_calls)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|