datadog 2.24.0 → 2.25.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 +26 -2
- data/lib/datadog/ai_guard/api_client.rb +82 -0
- data/lib/datadog/ai_guard/component.rb +42 -0
- data/lib/datadog/ai_guard/configuration/ext.rb +17 -0
- data/lib/datadog/ai_guard/configuration/settings.rb +98 -0
- data/lib/datadog/ai_guard/configuration.rb +11 -0
- data/lib/datadog/ai_guard/evaluation/message.rb +25 -0
- data/lib/datadog/ai_guard/evaluation/no_op_result.rb +34 -0
- data/lib/datadog/ai_guard/evaluation/request.rb +81 -0
- data/lib/datadog/ai_guard/evaluation/result.rb +43 -0
- data/lib/datadog/ai_guard/evaluation/tool_call.rb +18 -0
- data/lib/datadog/ai_guard/evaluation.rb +72 -0
- data/lib/datadog/ai_guard/ext.rb +16 -0
- data/lib/datadog/ai_guard.rb +153 -0
- data/lib/datadog/appsec/remote.rb +4 -3
- data/lib/datadog/appsec/security_engine/engine.rb +3 -3
- data/lib/datadog/appsec/security_engine/runner.rb +2 -2
- data/lib/datadog/core/configuration/components.rb +6 -0
- data/lib/datadog/core/configuration/supported_configurations.rb +6 -0
- data/lib/datadog/core/error.rb +6 -6
- data/lib/datadog/core/pin.rb +4 -0
- data/lib/datadog/core/rate_limiter.rb +1 -1
- data/lib/datadog/core/semaphore.rb +1 -4
- data/lib/datadog/core/telemetry/event/app_started.rb +2 -1
- data/lib/datadog/core/transport/response.rb +3 -1
- data/lib/datadog/core/utils/safe_dup.rb +2 -2
- data/lib/datadog/core/utils/sequence.rb +2 -0
- data/lib/datadog/di/boot.rb +4 -2
- data/lib/datadog/di/contrib/active_record.rb +4 -5
- data/lib/datadog/di/instrumenter.rb +9 -3
- data/lib/datadog/di/logger.rb +2 -2
- data/lib/datadog/di/probe_file_loader/railtie.rb +1 -1
- data/lib/datadog/di/probe_notifier_worker.rb +5 -5
- data/lib/datadog/error_tracking/filters.rb +2 -2
- data/lib/datadog/kit/appsec/events/v2.rb +2 -3
- data/lib/datadog/profiling/collectors/code_provenance.rb +1 -1
- data/lib/datadog/profiling/collectors/info.rb +3 -3
- data/lib/datadog/profiling/ext/dir_monkey_patches.rb +18 -0
- data/lib/datadog/tracing/contrib/waterdrop.rb +4 -0
- data/lib/datadog/tracing/distributed/baggage.rb +3 -2
- data/lib/datadog/tracing/sampling/priority_sampler.rb +3 -1
- data/lib/datadog/tracing/span.rb +1 -1
- data/lib/datadog/tracing/span_operation.rb +15 -9
- data/lib/datadog/version.rb +1 -1
- data/lib/datadog.rb +1 -0
- metadata +21 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b31bd418a350c8999b821462c0699a19af1b02242a7a22f4fee10859cd520d4c
|
|
4
|
+
data.tar.gz: 4a77d7cb714bfb82312d642b583d23f7b9407435c6577b864f1ca98751826ec6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f51a7d85ab5e057e5a485f7dffe7d329b4aacde56d784239a15dbb8a48508d082ba69dbecb992336d132965b642ccdf39cd4f4e2378d749174cab64496b1fe2f
|
|
7
|
+
data.tar.gz: 83504124b5fad578d31002d5ab4d19450c14c8a857b7b553925bbff2cb2fdb53ee2dbeb2e7c774128c92203e69b3c484896591435ec4df01740e2028b70ff7fe
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [2.25.0] - 2026-01-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
AI Guard: Add SDK for evaluating the safety of user messages and assistant commands for LLM session ([#5144][])
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
Core: Bump minimum version of `datadog-ruby_core_source` dependency ([#5215][])
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
AppSec: Fix processing of numeric data for WAF and RASP checks ([#5222][])
|
|
18
|
+
|
|
5
19
|
## [2.24.0] - 2026-01-08
|
|
6
20
|
|
|
7
21
|
### Added
|
|
@@ -9,6 +23,11 @@
|
|
|
9
23
|
* Core: Add support for installing the gem on Ruby 4.0.x stable ([#5157][])
|
|
10
24
|
* Tracing: Add origin detection using extra headers and the `DD_EXTERNAL_ENV` variable. ([#5028][])
|
|
11
25
|
* Dynamic Instrumentation: Add one-click enablement support ([#5150][])
|
|
26
|
+
* SSI: Add support for Bundler deployment mode ([#5053][])
|
|
27
|
+
* SSI: Report UI-oriented injection results ([#5053][])
|
|
28
|
+
* SSI: Guard against Bundler global force_ruby_platform ([#5053][])
|
|
29
|
+
* SSI: Guard against Bundler 4.0 and Bundler 2.7 in 4.0 mode ([#5053][])
|
|
30
|
+
* SSI: Guard against Ruby 3.5+ ([#5053][])
|
|
12
31
|
|
|
13
32
|
### Changed
|
|
14
33
|
|
|
@@ -3418,7 +3437,8 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
|
|
|
3418
3437
|
Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
|
|
3419
3438
|
|
|
3420
3439
|
|
|
3421
|
-
[Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.
|
|
3440
|
+
[Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v2.25.0...master
|
|
3441
|
+
[2.25.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.24.0...v2.25.0
|
|
3422
3442
|
[2.24.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.23.0...v2.24.0
|
|
3423
3443
|
[2.23.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.22.0...v2.23.0
|
|
3424
3444
|
[2.22.0]: https://github.com/DataDog/dd-trace-rb/compare/v2.21.0...v2.22.0
|
|
@@ -5059,12 +5079,14 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
|
|
|
5059
5079
|
[#5044]: https://github.com/DataDog/dd-trace-rb/issues/5044
|
|
5060
5080
|
[#5045]: https://github.com/DataDog/dd-trace-rb/issues/5045
|
|
5061
5081
|
[#5049]: https://github.com/DataDog/dd-trace-rb/issues/5049
|
|
5082
|
+
[#5053]: https://github.com/DataDog/dd-trace-rb/issues/5053
|
|
5062
5083
|
[#5054]: https://github.com/DataDog/dd-trace-rb/issues/5054
|
|
5063
5084
|
[#5058]: https://github.com/DataDog/dd-trace-rb/issues/5058
|
|
5064
5085
|
[#5073]: https://github.com/DataDog/dd-trace-rb/issues/5073
|
|
5065
5086
|
[#5086]: https://github.com/DataDog/dd-trace-rb/issues/5086
|
|
5066
5087
|
[#5091]: https://github.com/DataDog/dd-trace-rb/issues/5091
|
|
5067
5088
|
[#5122]: https://github.com/DataDog/dd-trace-rb/issues/5122
|
|
5089
|
+
[#5144]: https://github.com/DataDog/dd-trace-rb/issues/5144
|
|
5068
5090
|
[#5145]: https://github.com/DataDog/dd-trace-rb/issues/5145
|
|
5069
5091
|
[#5146]: https://github.com/DataDog/dd-trace-rb/issues/5146
|
|
5070
5092
|
[#5148]: https://github.com/DataDog/dd-trace-rb/issues/5148
|
|
@@ -5078,6 +5100,8 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
|
|
|
5078
5100
|
[#5176]: https://github.com/DataDog/dd-trace-rb/issues/5176
|
|
5079
5101
|
[#5194]: https://github.com/DataDog/dd-trace-rb/issues/5194
|
|
5080
5102
|
[#5197]: https://github.com/DataDog/dd-trace-rb/issues/5197
|
|
5103
|
+
[#5215]: https://github.com/DataDog/dd-trace-rb/issues/5215
|
|
5104
|
+
[#5222]: https://github.com/DataDog/dd-trace-rb/issues/5222
|
|
5081
5105
|
[@AdrianLC]: https://github.com/AdrianLC
|
|
5082
5106
|
[@Azure7111]: https://github.com/Azure7111
|
|
5083
5107
|
[@BabyGroot]: https://github.com/BabyGroot
|
|
@@ -5232,4 +5256,4 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
|
|
|
5232
5256
|
[@y-yagi]: https://github.com/y-yagi
|
|
5233
5257
|
[@yujideveloper]: https://github.com/yujideveloper
|
|
5234
5258
|
[@yukimurasawa]: https://github.com/yukimurasawa
|
|
5235
|
-
[@zachmccormick]: https://github.com/zachmccormick
|
|
5259
|
+
[@zachmccormick]: https://github.com/zachmccormick
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Datadog
|
|
8
|
+
module AIGuard
|
|
9
|
+
# API Client for AI Guard API.
|
|
10
|
+
# Uses net/http to perform request. Raises on client and server errors.
|
|
11
|
+
class APIClient
|
|
12
|
+
DEFAULT_SITE = "app.datadoghq.com"
|
|
13
|
+
DEFAULT_PATH = "/api/v2/ai-guard"
|
|
14
|
+
|
|
15
|
+
def initialize(endpoint:, api_key:, application_key:, timeout:)
|
|
16
|
+
@timeout = timeout
|
|
17
|
+
|
|
18
|
+
@endpoint_uri = if endpoint
|
|
19
|
+
URI(endpoint) #: URI::HTTP
|
|
20
|
+
else
|
|
21
|
+
URI::HTTPS.build(
|
|
22
|
+
host: Datadog.configuration.site || DEFAULT_SITE,
|
|
23
|
+
path: DEFAULT_PATH
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@headers = {
|
|
28
|
+
"DD-API-KEY": api_key.to_s,
|
|
29
|
+
"DD-APPLICATION-KEY": application_key.to_s,
|
|
30
|
+
"DD-AI-GUARD-VERSION": Datadog::VERSION::STRING,
|
|
31
|
+
"DD-AI-GUARD-SOURCE": "SDK",
|
|
32
|
+
"DD-AI-GUARD-LANGUAGE": "ruby",
|
|
33
|
+
"content-type": "application/json"
|
|
34
|
+
}.freeze
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def post(path, body:)
|
|
38
|
+
Net::HTTP.start(@endpoint_uri.host.to_s, @endpoint_uri.port, use_ssl: use_ssl?, read_timeout: @timeout) do |http|
|
|
39
|
+
request = Net::HTTP::Post.new(@endpoint_uri.request_uri + path, @headers)
|
|
40
|
+
request.body = body.to_json
|
|
41
|
+
|
|
42
|
+
response = http.request(request)
|
|
43
|
+
raise_on_http_error!(response)
|
|
44
|
+
|
|
45
|
+
parse_response_body(response.body)
|
|
46
|
+
end
|
|
47
|
+
rescue Net::ReadTimeout
|
|
48
|
+
raise AIGuardClientError, "Request to AI Guard timed out"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def raise_on_http_error!(response)
|
|
54
|
+
case response
|
|
55
|
+
when Net::HTTPSuccess
|
|
56
|
+
# do nothing
|
|
57
|
+
when Net::HTTPRedirection
|
|
58
|
+
raise AIGuardClientError, "Redirects for AI Guard API are not supported"
|
|
59
|
+
else
|
|
60
|
+
error_message = begin
|
|
61
|
+
parsed_body = JSON.parse(response.body)
|
|
62
|
+
Array(parsed_body.fetch('errors')).join(', ')
|
|
63
|
+
rescue JSON::ParserError, KeyError
|
|
64
|
+
response.body
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
raise AIGuardClientError, error_message
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parse_response_body(body)
|
|
72
|
+
JSON.parse(body)
|
|
73
|
+
rescue JSON::ParserError
|
|
74
|
+
raise AIGuardClientError, "Could not parse response body"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def use_ssl?
|
|
78
|
+
@endpoint_uri.scheme == 'https'
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'api_client'
|
|
4
|
+
require_relative 'evaluation'
|
|
5
|
+
require_relative 'evaluation/request'
|
|
6
|
+
require_relative 'evaluation/result'
|
|
7
|
+
require_relative 'evaluation/no_op_result'
|
|
8
|
+
require_relative 'evaluation/message'
|
|
9
|
+
require_relative 'evaluation/tool_call'
|
|
10
|
+
require_relative 'ext'
|
|
11
|
+
|
|
12
|
+
module Datadog
|
|
13
|
+
module AIGuard
|
|
14
|
+
# Component for API Guard product
|
|
15
|
+
class Component
|
|
16
|
+
attr_reader :api_client, :logger
|
|
17
|
+
|
|
18
|
+
def self.build(settings, logger:, telemetry:)
|
|
19
|
+
return unless settings.respond_to?(:ai_guard) && settings.ai_guard.enabled
|
|
20
|
+
|
|
21
|
+
api_client = APIClient.new(
|
|
22
|
+
endpoint: settings.ai_guard.endpoint,
|
|
23
|
+
api_key: settings.api_key,
|
|
24
|
+
application_key: settings.ai_guard.app_key,
|
|
25
|
+
timeout: settings.ai_guard.timeout_ms / 1_000
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
new(api_client, logger: logger, telemetry: telemetry)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(api_client, logger:, telemetry:)
|
|
32
|
+
@api_client = api_client
|
|
33
|
+
@logger = logger
|
|
34
|
+
@telemetry = telemetry
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def shutdown!
|
|
38
|
+
# no-op
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module AIGuard
|
|
5
|
+
module Configuration
|
|
6
|
+
# This module contains constants for AI Guard component
|
|
7
|
+
module Ext
|
|
8
|
+
ENV_AI_GUARD_ENABLED = "DD_AI_GUARD_ENABLED"
|
|
9
|
+
ENV_AI_GUARD_ENDPOINT = "DD_AI_GUARD_ENDPOINT"
|
|
10
|
+
ENV_AI_GUARD_TIMEOUT = "DD_AI_GUARD_TIMEOUT"
|
|
11
|
+
ENV_AI_GUARD_MAX_CONTENT_SIZE = "DD_AI_GUARD_MAX_CONTENT_SIZE"
|
|
12
|
+
ENV_AI_GUARD_MAX_MESSAGES_LENGTH = "DD_AI_GUARD_MAX_MESSAGES_LENGTH"
|
|
13
|
+
ENV_APP_KEY = "DD_APP_KEY"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
require_relative "ext"
|
|
5
|
+
|
|
6
|
+
module Datadog
|
|
7
|
+
module AIGuard
|
|
8
|
+
module Configuration
|
|
9
|
+
# AI Guard specific settings
|
|
10
|
+
module Settings
|
|
11
|
+
def self.extended(base)
|
|
12
|
+
base = base.singleton_class unless base.is_a?(Class)
|
|
13
|
+
add_settings!(base)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.add_settings!(base)
|
|
17
|
+
base.class_eval do
|
|
18
|
+
# AI Guard specific configurations.
|
|
19
|
+
# @public_api
|
|
20
|
+
settings :ai_guard do
|
|
21
|
+
# Enable AI Guard.
|
|
22
|
+
#
|
|
23
|
+
# You can use this option to skip calls to AI Guard API without having to remove library as a whole.
|
|
24
|
+
#
|
|
25
|
+
# @default `DD_AI_GUARD_ENABLED`, otherwise `false`
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
option :enabled do |o|
|
|
28
|
+
o.type :bool
|
|
29
|
+
o.env Ext::ENV_AI_GUARD_ENABLED
|
|
30
|
+
o.default false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# AI Guard API endpoint path.
|
|
34
|
+
#
|
|
35
|
+
# @default `DD_AI_GUARD_ENDPOINT`, otherwise `nil`
|
|
36
|
+
# @return [String, nil]
|
|
37
|
+
option :endpoint do |o|
|
|
38
|
+
o.type :string, nilable: true
|
|
39
|
+
o.env Ext::ENV_AI_GUARD_ENDPOINT
|
|
40
|
+
|
|
41
|
+
o.setter do |value|
|
|
42
|
+
next unless value
|
|
43
|
+
|
|
44
|
+
uri = URI(value.to_s)
|
|
45
|
+
raise ArgumentError, "Please provide an absolute URI that includes a protocol" unless uri.absolute?
|
|
46
|
+
|
|
47
|
+
uri.to_s.delete_suffix("/")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Datadog Application key.
|
|
52
|
+
#
|
|
53
|
+
# @default `DD_APP_KEY` environment variable, otherwise `nil`
|
|
54
|
+
# @return [String, nil]
|
|
55
|
+
option :app_key do |o|
|
|
56
|
+
o.type :string, nilable: true
|
|
57
|
+
o.env Ext::ENV_APP_KEY
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Request timeout in milliseconds.
|
|
61
|
+
#
|
|
62
|
+
# @default `DD_AI_GUARD_TIMEOUT`, otherwise 10 000 ms
|
|
63
|
+
# @return [Integer]
|
|
64
|
+
option :timeout_ms do |o|
|
|
65
|
+
o.type :int
|
|
66
|
+
o.env Ext::ENV_AI_GUARD_TIMEOUT
|
|
67
|
+
o.default 10_000
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Maximum content size in bytes.
|
|
71
|
+
# Content that exceeds the maximum allowed size is truncated before
|
|
72
|
+
# being stored in the current span context.
|
|
73
|
+
#
|
|
74
|
+
# @default `DD_AI_GUARD_MAX_CONTENT_SIZE`, otherwise 524 228 bytes
|
|
75
|
+
# @return [Integer]
|
|
76
|
+
option :max_content_size_bytes do |o|
|
|
77
|
+
o.type :int
|
|
78
|
+
o.env Ext::ENV_AI_GUARD_MAX_CONTENT_SIZE
|
|
79
|
+
o.default 512 * 1024
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Maximum number of messages.
|
|
83
|
+
# Older messages are omitted once the message limit is reached.
|
|
84
|
+
#
|
|
85
|
+
# @default `DD_AI_GUARD_MAX_MESSAGES_LENGTH`, otherwise 16 messages
|
|
86
|
+
# @return [Integer]
|
|
87
|
+
option :max_messages_length do |o|
|
|
88
|
+
o.type :int
|
|
89
|
+
o.env Ext::ENV_AI_GUARD_MAX_MESSAGES_LENGTH
|
|
90
|
+
o.default 16
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module AIGuard
|
|
5
|
+
module Evaluation
|
|
6
|
+
# Message class for AI Guard
|
|
7
|
+
class Message
|
|
8
|
+
attr_reader :role, :content, :tool_call, :tool_call_id
|
|
9
|
+
|
|
10
|
+
def initialize(role:, content: nil, tool_call: nil, tool_call_id: nil)
|
|
11
|
+
raise ArgumentError, "Role must be set to a non-empty value" if role.to_s.empty?
|
|
12
|
+
|
|
13
|
+
@role = role.to_sym
|
|
14
|
+
@content = content
|
|
15
|
+
@tool_call = tool_call
|
|
16
|
+
@tool_call_id = tool_call_id
|
|
17
|
+
|
|
18
|
+
if @tool_call && !@tool_call.is_a?(ToolCall)
|
|
19
|
+
raise ArgumentError, "Expected an instance of #{ToolCall.name} for :tool_call argument"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module AIGuard
|
|
5
|
+
module Evaluation
|
|
6
|
+
# Class for emulating AI Guard evaluation result when AI Guard is disabled.
|
|
7
|
+
class NoOpResult
|
|
8
|
+
attr_reader :action, :reason, :tags
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@action = Result::ALLOW_ACTION
|
|
12
|
+
@reason = "AI Guard is disabled"
|
|
13
|
+
@tags = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def allow?
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def deny?
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def abort?
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def blocking_enabled?
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module AIGuard
|
|
5
|
+
module Evaluation
|
|
6
|
+
# Request builds the request body from an array of messages and processes the response
|
|
7
|
+
class Request
|
|
8
|
+
REQUEST_PATH = "/evaluate"
|
|
9
|
+
|
|
10
|
+
attr_reader :serialized_messages
|
|
11
|
+
|
|
12
|
+
def initialize(messages)
|
|
13
|
+
@serialized_messages = serialize_messages(messages)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def perform
|
|
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
|
|
33
|
+
{
|
|
34
|
+
data: {
|
|
35
|
+
attributes: {
|
|
36
|
+
messages: @serialized_messages,
|
|
37
|
+
meta: {
|
|
38
|
+
service: Datadog.configuration.service,
|
|
39
|
+
env: Datadog.configuration.env
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def serialize_messages(messages)
|
|
47
|
+
serialized_messages = []
|
|
48
|
+
|
|
49
|
+
messages.each do |message|
|
|
50
|
+
serialized_messages << serialize_message(message)
|
|
51
|
+
|
|
52
|
+
break if serialized_messages.count == Datadog.configuration.ai_guard.max_messages_length
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
serialized_messages
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def serialize_message(message)
|
|
59
|
+
if message.tool_call
|
|
60
|
+
{
|
|
61
|
+
role: message.role,
|
|
62
|
+
tool_calls: [
|
|
63
|
+
{
|
|
64
|
+
id: message.tool_call.id,
|
|
65
|
+
function: {
|
|
66
|
+
name: message.tool_call.tool_name,
|
|
67
|
+
arguments: message.tool_call.arguments
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
elsif message.tool_call_id
|
|
73
|
+
{role: message.role, tool_call_id: message.tool_call_id, content: message.content}
|
|
74
|
+
else
|
|
75
|
+
{role: message.role, content: message.content}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module AIGuard
|
|
5
|
+
module Evaluation
|
|
6
|
+
# Wrapper class for evaluation API response
|
|
7
|
+
class Result
|
|
8
|
+
ALLOW_ACTION = "ALLOW"
|
|
9
|
+
DENY_ACTION = "DENY"
|
|
10
|
+
ABORT_ACTION = "ABORT"
|
|
11
|
+
|
|
12
|
+
attr_reader :action, :reason, :tags
|
|
13
|
+
|
|
14
|
+
def initialize(raw_response)
|
|
15
|
+
attributes = raw_response.fetch("data").fetch("attributes")
|
|
16
|
+
|
|
17
|
+
@action = attributes.fetch("action")
|
|
18
|
+
@reason = attributes.fetch("reason")
|
|
19
|
+
@tags = attributes.fetch("tags")
|
|
20
|
+
@is_blocking_enabled = attributes.fetch("is_blocking_enabled")
|
|
21
|
+
rescue KeyError => e
|
|
22
|
+
raise AIGuardClientError, "Missing key: \"#{e.key}\""
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def allow?
|
|
26
|
+
action == ALLOW_ACTION
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def deny?
|
|
30
|
+
action == DENY_ACTION
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def abort?
|
|
34
|
+
action == ABORT_ACTION
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def blocking_enabled?
|
|
38
|
+
!!@is_blocking_enabled
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module AIGuard
|
|
5
|
+
module Evaluation
|
|
6
|
+
# Tool call class for AI Guard
|
|
7
|
+
class ToolCall
|
|
8
|
+
attr_reader :tool_name, :id, :arguments
|
|
9
|
+
|
|
10
|
+
def initialize(tool_name, id:, arguments:)
|
|
11
|
+
@tool_name = tool_name
|
|
12
|
+
@id = id
|
|
13
|
+
@arguments = arguments
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module AIGuard
|
|
5
|
+
# module that contains a function for performing AI Guard Evaluation request
|
|
6
|
+
# and creating `ai_guard` span with required tags
|
|
7
|
+
module Evaluation
|
|
8
|
+
class << self
|
|
9
|
+
def perform(messages, allow_raise: false)
|
|
10
|
+
raise ArgumentError, "Messages must not be empty" if messages&.empty?
|
|
11
|
+
|
|
12
|
+
Tracing.trace(Ext::SPAN_NAME) do |span, trace|
|
|
13
|
+
if (last_message = messages.last)
|
|
14
|
+
if last_message.tool_call
|
|
15
|
+
span.set_tag(Ext::TARGET_TAG, "tool")
|
|
16
|
+
span.set_tag(Ext::TOOL_NAME_TAG, last_message.tool_call.tool_name)
|
|
17
|
+
elsif last_message.tool_call_id
|
|
18
|
+
span.set_tag(Ext::TARGET_TAG, "tool")
|
|
19
|
+
|
|
20
|
+
if (tool_call_message = messages.find { |m| m.tool_call&.id == last_message.tool_call_id })
|
|
21
|
+
span.set_tag(Ext::TOOL_NAME_TAG, tool_call_message.tool_call.tool_name) # steep:ignore
|
|
22
|
+
end
|
|
23
|
+
else
|
|
24
|
+
span.set_tag(Ext::TARGET_TAG, "prompt")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
request = Request.new(messages)
|
|
29
|
+
result = request.perform
|
|
30
|
+
|
|
31
|
+
span.set_tag(Ext::ACTION_TAG, result.action)
|
|
32
|
+
span.set_tag(Ext::REASON_TAG, result.reason)
|
|
33
|
+
|
|
34
|
+
span.set_metastruct_tag(
|
|
35
|
+
Ext::METASTRUCT_TAG,
|
|
36
|
+
{
|
|
37
|
+
messages: truncate_content(request.serialized_messages),
|
|
38
|
+
attack_categories: result.tags
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if allow_raise && (result.deny? || result.abort?) && result.blocking_enabled?
|
|
43
|
+
span.set_tag(Ext::BLOCKED_TAG, true)
|
|
44
|
+
raise AIGuardAbortError.new(action: result.action, reason: result.reason, tags: result.tags)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
result
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def perform_no_op
|
|
52
|
+
AIGuard.logger&.warn("AI Guard is disabled, messages were not evaluated")
|
|
53
|
+
|
|
54
|
+
NoOpResult.new
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def truncate_content(serialized_messages)
|
|
60
|
+
serialized_messages.map do |message| # steep:ignore
|
|
61
|
+
next message unless message[:content]
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
**message,
|
|
65
|
+
content: message[:content].byteslice(0, Datadog.configuration.ai_guard.max_content_size_bytes)
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module AIGuard
|
|
5
|
+
# AI Guard specific constants
|
|
6
|
+
module Ext
|
|
7
|
+
SPAN_NAME = "ai_guard"
|
|
8
|
+
TARGET_TAG = "ai_guard.target"
|
|
9
|
+
TOOL_NAME_TAG = "ai_guard.tool_name"
|
|
10
|
+
ACTION_TAG = "ai_guard.action"
|
|
11
|
+
REASON_TAG = "ai_guard.reason"
|
|
12
|
+
BLOCKED_TAG = "ai_guard.blocked"
|
|
13
|
+
METASTRUCT_TAG = "ai_guard"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|