znyx-sdk 1.2.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 816193b73770d4949028adf8359e4e402aa8cdd2fc9a2f39235ea9bc47acc946
4
+ data.tar.gz: 4cff940a92b4b7905422405a09a70e6f08864d08f318adb8e20c58e2ce5e3a8f
5
+ SHA512:
6
+ metadata.gz: 71e56c056f15328e7f7cb1a83177906755f6fbb8e5279700b593673f09a49b04ab7754a611fc5f660e47bde51975491f35155377270f51da190942f3286ea3ad
7
+ data.tar.gz: fb7938b04ed59c4b2b3d51c6245b25f1a0ca58b0b5329fff2096a96b732d2f31c76f48dee14cfa3f72958791114e7af6b5bbb709b30b519e3c40474429324d53
@@ -0,0 +1,113 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "json"
4
+ require "securerandom"
5
+
6
+ module ZnyxSdk
7
+ # Official Ruby SDK for the ZNYX Runtime guardrails API.
8
+ #
9
+ # @example
10
+ # client = ZnyxSdk::Client.new("http://localhost:8080")
11
+ #
12
+ # result = client.evaluate_input(
13
+ # tenant_id: "my-org",
14
+ # app_id: "my-app",
15
+ # text: user_message
16
+ # )
17
+ # raise result.user_message if result.blocked?
18
+ #
19
+ class Client
20
+ # @param base_url [String] URL of the running ZNYX Runtime (e.g. "http://localhost:8080")
21
+ # @param api_key [String, nil] Optional API key sent as Authorization: Bearer
22
+ # @param timeout [Integer] HTTP read/open timeout in seconds (default: 10)
23
+ def initialize(base_url, api_key: nil, timeout: 10)
24
+ @uri = URI.parse(base_url.chomp("/"))
25
+ @api_key = api_key
26
+ @timeout = timeout
27
+
28
+ # Anonymous, opt-out install telemetry (ZNYX_TELEMETRY=false to disable).
29
+ begin
30
+ Telemetry.maybe_send_install_ping
31
+ rescue StandardError
32
+ # telemetry must never break client construction
33
+ end
34
+ end
35
+
36
+ # Evaluates a user prompt before sending it to the LLM.
37
+ #
38
+ # @param text [String] The text to evaluate
39
+ # @param tenant_id [String]
40
+ # @param app_id [String]
41
+ # @param kwargs [Hash] Optional: agent_id, env, trace_id, session_id, metadata
42
+ # @return [EvaluationResponse]
43
+ def evaluate_input(text:, tenant_id: "default", app_id: "default", **kwargs)
44
+ post("/v1/evaluate/input", build_payload(text: text, tenant_id: tenant_id, app_id: app_id, **kwargs))
45
+ end
46
+
47
+ # Evaluates an LLM response before returning it to the user.
48
+ def evaluate_output(text:, tenant_id: "default", app_id: "default", **kwargs)
49
+ post("/v1/evaluate/output", build_payload(text: text, tenant_id: tenant_id, app_id: app_id, **kwargs))
50
+ end
51
+
52
+ # Evaluates a tool call before executing it.
53
+ #
54
+ # @param tool_name [String]
55
+ # @param tool_args [Hash]
56
+ def evaluate_tool(tool_name:, tool_args:, tenant_id: "default", app_id: "default", **kwargs)
57
+ payload = {
58
+ request_id: new_request_id,
59
+ tenant_id: tenant_id,
60
+ app_id: app_id,
61
+ agent_id: kwargs.delete(:agent_id) || "default",
62
+ env: kwargs.delete(:env) || "prod",
63
+ tool_name: tool_name,
64
+ tool_args: tool_args,
65
+ }.merge(kwargs.compact)
66
+ post("/v1/evaluate/tool", payload)
67
+ end
68
+
69
+ private
70
+
71
+ def build_payload(text:, tenant_id:, app_id:, **kwargs)
72
+ {
73
+ request_id: new_request_id,
74
+ tenant_id: tenant_id,
75
+ app_id: app_id,
76
+ agent_id: kwargs.delete(:agent_id) || "default",
77
+ env: kwargs.delete(:env) || "prod",
78
+ text: text,
79
+ }.merge(kwargs.compact)
80
+ end
81
+
82
+ def post(path, payload)
83
+ http = Net::HTTP.new(@uri.host, @uri.port)
84
+ http.use_ssl = @uri.scheme == "https"
85
+ http.read_timeout = @timeout
86
+ http.open_timeout = @timeout
87
+
88
+ request = Net::HTTP::Post.new(path)
89
+ request["Content-Type"] = "application/json"
90
+ request["Authorization"] = "Bearer #{@api_key}" if @api_key
91
+ request.body = JSON.generate(payload)
92
+
93
+ response = http.request(request)
94
+
95
+ if response.code.to_i >= 400
96
+ raise ZnyxError.new(
97
+ "ZNYX Runtime returned #{response.code}: #{response.body}",
98
+ status_code: response.code.to_i
99
+ )
100
+ end
101
+
102
+ EvaluationResponse.new(JSON.parse(response.body))
103
+ rescue ZnyxError
104
+ raise
105
+ rescue => e
106
+ raise ZnyxError, "HTTP request failed: #{e.message}"
107
+ end
108
+
109
+ def new_request_id
110
+ "req_#{SecureRandom.hex(4)}"
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,65 @@
1
+ module ZnyxSdk
2
+ DECISIONS = %w[ALLOW BLOCK REDACT WARN TRANSFORM].freeze
3
+
4
+ # Immutable value object returned by all evaluate methods.
5
+ class EvaluationResponse
6
+ attr_reader :request_id, :decision, :risk_score, :policy_version,
7
+ :rule_hits, :sanitized_text, :user_message,
8
+ :developer_message, :latency_ms, :trace_id, :session_id,
9
+ :detector_results
10
+
11
+ def initialize(data)
12
+ @request_id = data["request_id"]
13
+ @decision = data["decision"]
14
+ @risk_score = data["risk_score"].to_i
15
+ @policy_version = data["policy_version"]
16
+ @rule_hits = Array(data["rule_hits"]).map { |h| RuleHit.new(h) }
17
+ @sanitized_text = data["sanitized_text"]
18
+ @user_message = data["user_message"]
19
+ @developer_message = data["developer_message"]
20
+ @latency_ms = data["latency_ms"].to_i
21
+ @trace_id = data["trace_id"]
22
+ @session_id = data["session_id"]
23
+ @detector_results = Array(data["detector_results"]).map { |d| DetectorResult.new(d) }
24
+ end
25
+
26
+ def blocked? = @decision == "BLOCK"
27
+ def redacted? = @decision == "REDACT"
28
+
29
+ # Returns sanitized_text if present, otherwise the original string.
30
+ def safe_text(original)
31
+ sanitized_text.nil? || sanitized_text.empty? ? original : sanitized_text
32
+ end
33
+ end
34
+
35
+ class RuleHit
36
+ attr_reader :rule_id, :severity, :message
37
+
38
+ def initialize(data)
39
+ @rule_id = data["rule_id"]
40
+ @severity = data["severity"]
41
+ @message = data["message"]
42
+ end
43
+ end
44
+
45
+ class DetectorResult
46
+ attr_reader :detector_name, :decision, :risk_score, :latency_ms, :rule_hits
47
+
48
+ def initialize(data)
49
+ @detector_name = data["detector_name"]
50
+ @decision = data["decision"]
51
+ @risk_score = data["risk_score"].to_i
52
+ @latency_ms = data["latency_ms"].to_i
53
+ @rule_hits = Array(data["rule_hits"]).map { |h| RuleHit.new(h) }
54
+ end
55
+ end
56
+
57
+ class ZnyxError < StandardError
58
+ attr_reader :status_code
59
+
60
+ def initialize(message, status_code: nil)
61
+ super(message)
62
+ @status_code = status_code
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+ require "securerandom"
7
+ require "rbconfig"
8
+ require "time"
9
+ require "fileutils"
10
+
11
+ module ZnyxSdk
12
+ # Anonymous install telemetry for the ZNYX Ruby SDK.
13
+ #
14
+ # Sends a single fire-and-forget ping when a client is first constructed, then
15
+ # at most one "heartbeat" ping per 24h. Non-sensitive metadata only:
16
+ # install_id (random UUID, persisted to ~/.znyx/sdk-state.json), SDK version,
17
+ # source ("ruby-sdk"), OS / arch / Ruby version, run_count.
18
+ #
19
+ # No PII, no request content, no tenant data. Opt out with
20
+ # ZNYX_TELEMETRY=false (on by default). Disclosed once to stderr on the first
21
+ # run. Every operation is best-effort — telemetry must never slow down, block,
22
+ # or break the calling application. See TELEMETRY.md.
23
+ module Telemetry
24
+ # Telemetry endpoint. Defaults to the ZNYX production receiver so anonymous
25
+ # install telemetry is on out of the box (opt-out, fully transparent).
26
+ # Override with ZNYX_TELEMETRY_URL (or ZNYX_HEARTBEAT_URL), or opt out with
27
+ # ZNYX_TELEMETRY=false.
28
+ ENDPOINT = (
29
+ (ENV["ZNYX_TELEMETRY_URL"] || "").empty? ? nil : ENV["ZNYX_TELEMETRY_URL"]
30
+ ) || (
31
+ (ENV["ZNYX_HEARTBEAT_URL"] || "").empty? ? nil : ENV["ZNYX_HEARTBEAT_URL"]
32
+ ) || "https://cp.znyx.ai/v1/install-telemetry"
33
+
34
+ STATE_FILE = File.join(Dir.home, ".znyx", "sdk-state.json")
35
+ HEARTBEAT_INTERVAL = 86_400 # seconds (24h)
36
+ SOURCE = "ruby-sdk"
37
+
38
+ DISCLOSURE =
39
+ "[znyx-sdk] Anonymous usage telemetry is on (install id, SDK version, OS - " \
40
+ "no PII, no request content). Opt out with ZNYX_TELEMETRY=false."
41
+
42
+ @attempted = false
43
+
44
+ class << self
45
+ # Send an anonymous install ping, throttled to once per 24h. Fire-and-forget.
46
+ def maybe_send_install_ping
47
+ return if @attempted
48
+
49
+ @attempted = true
50
+
51
+ return if ENDPOINT.to_s.empty? || !enabled?
52
+
53
+ state = load_state
54
+ last_ping = state["last_ping_at"]
55
+
56
+ if last_ping.nil?
57
+ event_type = "first_run"
58
+ else
59
+ begin
60
+ elapsed = Time.now - Time.iso8601(last_ping)
61
+ rescue StandardError
62
+ elapsed = HEARTBEAT_INTERVAL # unparseable -> treat as due
63
+ end
64
+ return if elapsed < HEARTBEAT_INTERVAL
65
+
66
+ event_type = "heartbeat"
67
+ end
68
+
69
+ install_id = state["install_id"] || SecureRandom.uuid
70
+ run_count = (state["run_count"] || 0).to_i + 1
71
+ now_iso = Time.now.utc.iso8601
72
+
73
+ state.merge!(
74
+ "install_id" => install_id,
75
+ "first_seen_at" => state["first_seen_at"] || now_iso,
76
+ "last_ping_at" => now_iso,
77
+ "run_count" => run_count
78
+ )
79
+ save_state(state)
80
+
81
+ if event_type == "first_run"
82
+ begin
83
+ warn DISCLOSURE
84
+ rescue StandardError
85
+ # ignore
86
+ end
87
+ end
88
+
89
+ payload = {
90
+ install_id: install_id,
91
+ version: version,
92
+ event_type: event_type,
93
+ source: SOURCE,
94
+ os: RbConfig::CONFIG["host_os"],
95
+ os_version: host_os_version,
96
+ arch: RbConfig::CONFIG["host_cpu"],
97
+ ruby_version: RUBY_VERSION,
98
+ run_count: run_count,
99
+ timestamp: now_iso
100
+ }
101
+
102
+ Thread.new { post(payload) }
103
+ rescue StandardError
104
+ # Telemetry must never break client construction.
105
+ end
106
+
107
+ private
108
+
109
+ def enabled?
110
+ val = (ENV["ZNYX_TELEMETRY"] || ENV["GUARDRAILS_TELEMETRY"] || "true").downcase
111
+ !%w[false 0 no].include?(val)
112
+ end
113
+
114
+ def version
115
+ defined?(ZnyxSdk::VERSION) ? ZnyxSdk::VERSION : "unknown"
116
+ end
117
+
118
+ def host_os_version
119
+ `uname -r`.strip
120
+ rescue StandardError
121
+ "unknown"
122
+ end
123
+
124
+ def load_state
125
+ return {} unless File.exist?(STATE_FILE)
126
+
127
+ JSON.parse(File.read(STATE_FILE))
128
+ rescue StandardError
129
+ {}
130
+ end
131
+
132
+ def save_state(state)
133
+ FileUtils.mkdir_p(File.dirname(STATE_FILE))
134
+ File.write(STATE_FILE, JSON.pretty_generate(state))
135
+ rescue StandardError
136
+ # best-effort; never raise
137
+ end
138
+
139
+ def post(payload)
140
+ uri = URI.parse(ENDPOINT)
141
+ http = Net::HTTP.new(uri.host, uri.port)
142
+ http.use_ssl = uri.scheme == "https"
143
+ http.open_timeout = 3
144
+ http.read_timeout = 3
145
+
146
+ request = Net::HTTP::Post.new(uri.request_uri)
147
+ request["Content-Type"] = "application/json"
148
+ request.body = JSON.generate(payload)
149
+
150
+ http.request(request)
151
+ rescue StandardError
152
+ # never fail the app because of telemetry
153
+ end
154
+ end
155
+ end
156
+ end
data/lib/znyx_sdk.rb ADDED
@@ -0,0 +1,7 @@
1
+ require_relative "znyx_sdk/models"
2
+ require_relative "znyx_sdk/client"
3
+ require_relative "znyx_sdk/telemetry"
4
+
5
+ module ZnyxSdk
6
+ VERSION = "1.0.1"
7
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: znyx-sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Zitrino
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-07-22 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - community@zitrino.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/znyx_sdk.rb
21
+ - lib/znyx_sdk/client.rb
22
+ - lib/znyx_sdk/models.rb
23
+ - lib/znyx_sdk/telemetry.rb
24
+ homepage: https://znyx.ai
25
+ licenses:
26
+ - Apache-2.0
27
+ metadata:
28
+ source_code_uri: https://github.com/zitrino-oss/znyx-sdk
29
+ bug_tracker_uri: https://github.com/zitrino-oss/znyx-sdk/issues
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '3.0'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubygems_version: 3.4.19
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: Official Ruby SDK for the ZNYX Runtime guardrails API
49
+ test_files: []