olyx-guardrails 1.2.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1ee4f7c6297bf8348c6d74feff8bd055ce23b819091893043d02a6695140cd2
4
- data.tar.gz: e06e7f54f777a8e56da1fcbad45f92d327f30aea934467235a9bc4384dc2229d
3
+ metadata.gz: 196f3e181407760bea2e26500d3e3eaa5ce29b886922ad361eab20e98ea76b68
4
+ data.tar.gz: 24144315dc438469cc42ca4d876834b44c3ad3b1b42430a80d6bc693e57ceec3
5
5
  SHA512:
6
- metadata.gz: 1d85a8e9c03dc058fd188132a2754c4738063ddc79039eb5bd88c5dcf25555f3b266d6d4b29ee37503b7c25fe0beabc74d0cfe895f82ad16c82e93ff2351955b
7
- data.tar.gz: 6e04d4fda8672b775475e0741b621ee850ab178656283a20c4fc1f27aa324112cb2ee5c883890dec97b53d423b7f65e6daa88006269574481c36e69128542afd
6
+ metadata.gz: e3ecc41c16b5f43dc99beb1af720a1f5f0a4f68fbb5453504a21e01c14ff7f1c248e724bd7987e5e4db50aa15944cf03291556a41c951282b98351eb863e1512
7
+ data.tar.gz: 8814874a0d9f40c93a6deb3b2063da29378f6a149742319f19b9b6aa7a50142e52937baab9cc71e6ae58706d42a65820467391d8a583eae021f7d1cad7665b1f
data/CHANGELOG.md CHANGED
@@ -5,24 +5,28 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.2.1] - 2026-11-11
9
+
10
+ ### Changed
11
+
12
+ - Simplify evaluation, message handling, LLM analysis, and numeric date validation
13
+ while preserving the public result contract.
14
+ - Require patched Rails test bundles: 8.1.4.1 or newer on Rails 8.1 and
15
+ 8.0.5.2 or newer on Rails 8.0, with Rack >= 3.2.8 and Crass >= 1.0.8.
16
+ - Document request-body limits, host allowlists, and trusted proxy headers for
17
+ applications consuming the gem.
18
+
8
19
  ## [1.2.0] - 2026-10-02
9
20
 
10
21
  ### Changed
11
22
 
12
- - Prompt-injection normalization now scans long inputs in overlapping bounded
13
- windows instead of ignoring content after the first 20,000 characters.
14
- - Narrowed role-play matching so ordinary agent prompts do not block by
15
- default.
16
- - Provider exceptions return a stable public error instead of exposing the
17
- provider's exception message.
18
- - Confidentiality labels are no longer classified as credentials or used to
19
- redact an entire input. Applications can enforce those labels with policy
20
- rules when required.
21
- - Risk scores now describe findings independently from the policy's blocking
22
- configuration.
23
- - Removed redundant scanner passes and consolidated internal configuration and
24
- matching layers.
25
- - Simplified the public quality gate to tests, documentation, and RuboCop.
23
+ - Scans long inputs in overlapping windows and narrows injection matching to
24
+ reduce missed detections and false positives.
25
+ - Sanitizes provider failures and stops treating confidentiality labels as
26
+ credentials; applications can enforce those labels with policy rules.
27
+ - Reports risk independently from blocking policy.
28
+ - Consolidates scanning and configuration internals, with tests,
29
+ documentation, and RuboCop as the public quality gate.
26
30
 
27
31
  ## [1.1.3] - 2026-08-07
28
32
 
data/README.md CHANGED
@@ -6,61 +6,43 @@
6
6
  [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Olyx-labs/olyx-guardrails/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Olyx-labs/olyx-guardrails)
7
7
  [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
8
8
 
9
- Olyx Guardrails is an in-process safety boundary for Ruby and Rails
10
- applications that send or receive AI-generated content. It provides:
9
+ Input and output checks for AI applications in Ruby and Rails.
11
10
 
12
- - PII and secret detection with explicit redaction;
13
- - prompt-injection and jailbreak detection, including adjacent-turn checks;
14
- - immutable, organization-specific restricted-content policies;
15
- - completed-input and completed-output decisions;
16
- - opt-in Rails adapters for common ingestion paths;
17
- - provider-agnostic semantic analysis through a callable LLM hook; and
18
- - sanitized notifications and Active Support instrumentation.
11
+ Detect prompt injection, PII, secrets, and application-specific restricted
12
+ content. Return explicit allow/block decisions and redact recognized content
13
+ at the boundaries your application chooses.
19
14
 
20
- The deterministic checks run locally. The gem does not proxy model traffic,
21
- discover model calls, or send application content to a third party.
15
+ Olyx Guardrails is a standalone gem. It works without Olyx CLI, Olyx Cloud,
16
+ or an Olyx account. Deterministic checks run locally. An optional
17
+ `llm_provider` receives raw input and may send it elsewhere through your
18
+ application's adapter.
22
19
 
23
- ## Requirements
20
+ ## Install
24
21
 
25
- | Component | Supported versions |
26
- |---|---|
27
- | Ruby | 3.4 or newer |
28
- | Rails | 8.0 and 8.1 |
22
+ Requires **Ruby 3.4 or newer**. Rails is optional; the integration supports
23
+ **Rails 8.0 and 8.1**. The framework-free API does not load Rails.
29
24
 
30
- Rails is optional. The standalone Ruby API does not load Rails.
31
-
32
- The 1.1 release line supports Rails 8.0 and 8.1. Only the listed Rails
33
- series are tested and supported. When a series reaches upstream end-of-life,
34
- a subsequent gem release may remove it instead of maintaining framework
35
- security fixes independently. Every support change is recorded in the
36
- changelog.
37
-
38
- ## Installation
39
-
40
- Add the gem to the application:
25
+ Add to your `Gemfile`:
41
26
 
42
27
  ```ruby
43
- gem "olyx-guardrails", "~> 1.1"
28
+ gem "olyx-guardrails", "~> 1.2"
44
29
  ```
45
30
 
46
- Then install the bundle:
47
-
48
- ```bash
31
+ ```sh
49
32
  bundle install
50
33
  ```
51
34
 
52
- The only runtime gem dependency is Ruby's `base64` bundled gem, used for
53
- bounded encoded-input detection.
35
+ The only runtime gem dependency is Ruby's bundled `base64` gem.
54
36
 
55
- ## Quick start
37
+ ## Try it
56
38
 
57
- Create one immutable policy and use it at the application boundary:
39
+ Save this as `guardrails_example.rb`:
58
40
 
59
41
  ```ruby
60
42
  require "olyx/guardrails"
61
43
 
62
44
  policy = Olyx::Guardrails::Policy.new(
63
- name: "customer-data-boundary",
45
+ name: "application-boundary",
64
46
  max_input_length: 4_000,
65
47
  block_pii: false,
66
48
  block_injections: true,
@@ -76,198 +58,147 @@ policy = Olyx::Guardrails::Policy.new(
76
58
  ]
77
59
  )
78
60
 
61
+ input = "Contact ada@example.com about the release."
79
62
  decision = Olyx::Guardrails.check(input, policy: policy)
80
- return forbidden unless decision[:allowed]
63
+ abort "Input rejected" unless decision[:allowed]
81
64
 
82
- safe_input = Olyx::Guardrails.redact(input, policy: policy)[:text]
83
- completion = LlmClient.complete(safe_input)
65
+ puts "Allowed: #{decision[:allowed]}"
66
+ puts Olyx::Guardrails.redact(input, policy: policy)[:text]
84
67
 
85
- output_decision = Olyx::Guardrails.check_output(completion, policy: policy)
86
- return invalid_output unless output_decision[:allowed]
68
+ restricted = Olyx::Guardrails.check("Discuss Project Falcon", policy: policy)
69
+ puts "Restricted input allowed: #{restricted[:allowed]}"
87
70
  ```
88
71
 
89
- `check` makes a decision and never transforms input. `redact` transforms
90
- recognized content and never makes an allow/block decision. Applications that
91
- need both operations call both explicitly.
72
+ Run it:
92
73
 
93
- The default policy:
74
+ ```sh
75
+ bundle exec ruby guardrails_example.rb
76
+ ```
94
77
 
95
- - limits input to 10,000 characters;
96
- - blocks detected injection attempts;
97
- - reports, but does not block, detected PII and secrets; and
98
- - has no custom restricted-content rules.
78
+ Expected output:
99
79
 
100
- Production applications should construct an explicit policy instead of relying
101
- on defaults. See the
102
- [Policies guide](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/POLICIES.md)
103
- for the complete rule language and YAML examples.
80
+ ```text
81
+ Allowed: true
82
+ Contact [EMAIL] about the release.
83
+ Restricted input allowed: false
84
+ ```
104
85
 
105
- ## Rails quick start
86
+ In this policy, PII is reported without blocking, then explicitly redacted.
87
+ Injection attempts, secrets, and the configured project name cause rejection.
88
+ Policies are immutable: construct them once and reuse them.
89
+
90
+ **Checking and redacting are separate operations.** `check` returns a decision
91
+ without changing input. `redact` transforms recognized content without making
92
+ an allow/block decision. Redaction does not override a rejection.
93
+
94
+ Use `check_messages` for structured conversations and adjacent-turn detection.
95
+ Use `check_output` and `redact_output` at completed-output boundaries. Your
96
+ application owns the model call and must check the completed response before
97
+ returning it. See the [complete Ruby example](https://github.com/Olyx-labs/olyx-guardrails/blob/master/examples/ruby_only.rb) and
98
+ [API reference](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/API.md) for the full flow and result contracts.
99
+
100
+ ## Add Rails enforcement
106
101
 
107
102
  Generate an initializer and environment-keyed policy file:
108
103
 
109
- ```bash
104
+ ```sh
110
105
  bin/rails generate olyx_guardrails:install
111
106
  ```
112
107
 
113
- Add the controller concern only where content crosses an AI boundary:
108
+ Review `config/initializers/olyx_guardrails.rb` and
109
+ `config/olyx_guardrails.yml`, then include an adapter at an application boundary:
114
110
 
115
111
  ```ruby
116
112
  class AiRequestsController < ApplicationController
117
113
  include Olyx::Guardrails::Rails::Controller
118
114
 
119
115
  rescue_from Olyx::Guardrails::Blocked do |error|
120
- render json: {
121
- error: "input_rejected",
122
- decision: error.decision
123
- }, status: :unprocessable_entity
116
+ render json: { error: "input_rejected", decision: error.decision },
117
+ status: :unprocessable_entity
124
118
  end
125
119
 
126
120
  def create
127
121
  prompt = params.require(:prompt)
128
- guardrails_check!(prompt, metadata: { user_id: current_user.id })
129
-
122
+ guardrails_check!(prompt)
130
123
  safe_prompt = guardrails_redact(prompt)[:text]
124
+
131
125
  completion = LlmClient.complete(safe_prompt)
132
126
  Olyx::Guardrails::Rails::Enforcer.check_output!(completion)
127
+ safe_output = Olyx::Guardrails::Rails.redact_output(completion)[:text]
133
128
 
134
- render json: { completion: completion }, status: :created
129
+ render json: { completion: safe_output }
135
130
  end
136
131
  end
137
132
  ```
138
133
 
139
- Rails enforcement is opt-in. The gem does not scan parameters, callbacks,
140
- uploads, jobs, GraphQL operations, or Action Cable messages globally.
141
-
142
- The
143
- [Rails integration guide](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/RAILS.md)
144
- covers every adapter, boot-time configuration, safe YAML loading,
145
- instrumentation payloads, and notification delivery.
134
+ `LlmClient` represents your application's existing model client. This controller
135
+ example belongs in a Rails application; it is not a standalone script.
146
136
 
147
- ## Choose the right entry point
137
+ Enforcement is opt-in. The gem does not automatically scan every request, job,
138
+ upload, or model call. The [Rails guide](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/RAILS.md) covers controllers,
139
+ Active Job, Active Model, GraphQL, Action Cable, extracted upload text,
140
+ instrumentation, and notifications.
148
141
 
149
- | Boundary | Entry point | Behavior |
150
- |---|---|---|
151
- | Plain text input | `Guardrails.check` | Returns an allow/block decision |
152
- | Structured messages | `Guardrails.check_messages` | Adds adjacent-turn detection |
153
- | Completed model output | `Guardrails.check_output` | Explicit output decision |
154
- | Plain text transformation | `Guardrails.redact` | Returns redacted text |
155
- | Completed output transformation | `Guardrails.redact_output` | Explicit output transformation |
156
- | Rails exception flow | `Rails::Enforcer.check!` | Raises `Guardrails::Blocked` when rejected |
157
- | Low-level secret enforcement | `SecretScanner.scan!` | Raises `SecretScanner::Blocked` on a finding |
142
+ ## Defaults and operational behavior
158
143
 
159
- All completed-output methods operate on complete values. They do not inspect a
160
- token stream before content reaches the caller.
161
-
162
- ## Optional LLM provider
163
-
164
- `llm_provider` accepts any callable object. It receives the raw text and a
165
- bounded context hash containing deterministic findings:
144
+ The default policy:
166
145
 
167
- ```ruby
168
- provider = lambda do |text, context|
169
- LocalClassifier.call(text: text, signals: context)
170
- end
146
+ - rejects input longer than 10,000 Ruby characters;
147
+ - blocks detected prompt injection;
148
+ - reports PII and secrets without blocking them;
149
+ - defines no custom restricted-content rules; and
150
+ - preserves the deterministic decision if an optional LLM provider fails.
171
151
 
172
- policy = Olyx::Guardrails::Policy.new(llm_failure_mode: :block)
152
+ Define an explicit production policy. If semantic analysis is required, choose
153
+ `llm_failure_mode: :block` or `:raise`. The application supplies the classifier
154
+ callable and owns its authentication, transport, timeouts, and retries.
155
+ Semantic findings can add violations but cannot clear deterministic findings.
156
+ See [policies](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/POLICIES.md) and the
157
+ [provider contract](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/API.md#llm-provider-contract).
173
158
 
174
- result = Olyx::Guardrails.check(
175
- input,
176
- policy: policy,
177
- llm_provider: provider
178
- )
179
- ```
159
+ Rails can publish content-free Active Support events. Optional notifier handlers
160
+ receive sanitized events and run synchronously unless your application queues
161
+ delivery. See [operations](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/OPERATIONS.md) for data handling and failure modes.
180
162
 
181
- The provider returns a hash or schema object containing any of:
163
+ ## Limitations
182
164
 
183
- ```ruby
184
- {
185
- injection_attempt: false,
186
- pii_detected: false,
187
- secret_leaked: false,
188
- risk_score: 0.2,
189
- reason: "No semantic violation found"
190
- }
191
- ```
165
+ Pattern-based checks can miss novel attacks, paraphrasing, translations,
166
+ unsupported formats, and nested encodings. PII coverage is strongest for the
167
+ documented North American and common international formats. Secret detection
168
+ is format-based. Restricted-content rules do not infer semantic equivalents
169
+ unless your application supplies additional analysis.
192
170
 
193
- The gem owns normalization, validation, failure handling, and safe result
194
- merging. The application owns model selection, prompting, authentication,
195
- timeouts, retries, and transport. LLM analysis findings can add a violation but cannot clear a deterministic finding.
171
+ Completed-output methods evaluate complete values; they do not enforce a policy
172
+ on tokens already streamed to a caller. Model routing, tool permissions,
173
+ authorization, file extraction, distributed quotas, and audit storage remain
174
+ application responsibilities.
196
175
 
197
- See the
198
- [provider contract](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/API.md#llm-provider-contract)
199
- and
200
- [local HTTP example](https://github.com/Olyx-labs/olyx-guardrails/blob/master/examples/local_llm_provider.rb).
201
- The example works with an application-owned classifier sidecar backed by any
202
- inference runtime. Read the
203
- [model-suitability criteria](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/OPERATIONS.md#model-suitability)
204
- before using semantic analysis as a blocking production control.
176
+ Treat guardrails as one layer of application protection. Read the
177
+ [detection limits](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/OPERATIONS.md#detection-limits) before enabling production
178
+ blocking.
205
179
 
206
- ## Documentation
180
+ ## Documentation and examples
207
181
 
208
182
  - [Documentation index](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/README.md)
209
183
  - [Policies and restricted content](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/POLICIES.md)
210
184
  - [Rails integration](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/RAILS.md)
211
- - [Operations and production behavior](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/OPERATIONS.md)
212
- - [API reference](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/API.md)
213
- - [Release runbook](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/RELEASING.md)
214
- - [Security policy](https://github.com/Olyx-labs/olyx-guardrails/security/policy)
215
- - [Contributing](https://github.com/Olyx-labs/olyx-guardrails/blob/master/CONTRIBUTING.md)
216
- - [Changelog](CHANGELOG.md)
217
-
218
- Examples:
219
-
220
- - [Framework-free Ruby](https://github.com/Olyx-labs/olyx-guardrails/blob/master/examples/ruby_only.rb)
221
- - [Custom policy](https://github.com/Olyx-labs/olyx-guardrails/blob/master/examples/custom_policy.rb)
222
- - [Rails opt-in boundaries](https://github.com/Olyx-labs/olyx-guardrails/blob/master/examples/rails_opt_in.rb)
223
- - [Local LLM provider](https://github.com/Olyx-labs/olyx-guardrails/blob/master/examples/local_llm_provider.rb)
185
+ - [Operations and notifications](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/OPERATIONS.md)
186
+ - [Ruby API reference](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/API.md)
187
+ - [Runnable Ruby example](https://github.com/Olyx-labs/olyx-guardrails/blob/master/examples/ruby_only.rb)
188
+ - [Custom policies](https://github.com/Olyx-labs/olyx-guardrails/blob/master/examples/custom_policy.rb)
189
+ - [Application-owned local classifier](https://github.com/Olyx-labs/olyx-guardrails/blob/master/examples/local_llm_provider.rb)
224
190
  - [Notifier handlers](https://github.com/Olyx-labs/olyx-guardrails/blob/master/examples/notifier.rb)
225
191
 
226
- The framework-free, custom-policy, and notifier examples run directly. The
227
- Rails example is application-context code, and the local provider example
228
- expects the documented classifier sidecar.
229
-
230
- ## Security model and limitations
231
-
232
- Olyx Guardrails is a defense-in-depth control, not a complete semantic security
233
- boundary or data-loss-prevention system.
234
-
235
- - Pattern-based detection can miss paraphrasing, translations, novel attacks,
236
- unsupported homoglyphs, and nested encoding beyond the bounded normalization
237
- pass.
238
- - PII support is strongest for documented North American and common
239
- international formats.
240
- - Secret detection is format-based and cannot classify every high-entropy
241
- string or future credential format.
242
- - Restricted-content rules are deterministic unless an application supplies an
243
- LLM provider.
244
- - File parsing, authorization, streaming enforcement, distributed quotas,
245
- centralized policy rollout, and audit retention remain application or
246
- platform responsibilities.
247
-
248
- The
249
- [operations guide](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/OPERATIONS.md)
250
- describes failure modes, data handling, risk scores, concurrency, and
251
- deployment boundaries. Report security issues through the
252
- [security policy](https://github.com/Olyx-labs/olyx-guardrails/security/policy),
253
- not the public issue tracker.
254
-
255
- ## Development
256
-
257
- Run the same gates used by CI:
258
-
259
- ```bash
260
- bin/setup
261
- bin/ci
262
- ```
192
+ ## Contributing and security
193
+
194
+ See [CONTRIBUTING.md](https://github.com/Olyx-labs/olyx-guardrails/blob/master/CONTRIBUTING.md) for development setup and checks,
195
+ [CHANGELOG.md](CHANGELOG.md) for release changes, and the
196
+ [release runbook](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/RELEASING.md) for maintainer procedures.
197
+
198
+ Rails adapter changes must also pass `bundle exec appraisal rake test`.
263
199
 
264
- The development bundle includes Rails so contributors exercise the integration
265
- from the default test suite; Rails remains an optional runtime dependency for
266
- gem consumers. The Rails compatibility matrix is managed with Appraisal. See
267
- [contribution guide](https://github.com/Olyx-labs/olyx-guardrails/blob/master/CONTRIBUTING.md)
268
- before submitting a change.
200
+ Report vulnerabilities through the [security policy](https://github.com/Olyx-labs/olyx-guardrails/blob/master/SECURITY.md).
269
201
 
270
202
  ## License
271
203
 
272
- Olyx Guardrails is available under the
273
204
  [Apache License 2.0](LICENSE).
@@ -10,42 +10,25 @@ module Olyx
10
10
 
11
11
  module_function
12
12
 
13
+ # Keep the fixed public contract together rather than splitting its fields across helpers.
14
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
13
15
  def call(checks, analysis, policy)
14
16
  ordered = CHECK_ORDER.filter_map { |type| checks[type] }
15
- base_result(checks, ordered, policy)
16
- .merge(risk(checks, ordered, analysis))
17
- .merge(analysis_result(analysis))
18
- end
19
-
20
- def base_result(checks, ordered, policy)
21
- detection(checks, ordered).merge(policy(checks[:policy], policy))
22
- end
23
-
24
- def detection(checks, ordered)
25
- {
17
+ result = {
26
18
  allowed: ordered.all? { |check| check[:allowed] },
27
19
  pii_detected: checks[:pii][:detected],
28
20
  injection_attempt: checks[:injection][:injection_attempt],
29
- secret_leaked: checks[:secret][:leaked]
21
+ secret_leaked: checks[:secret][:leaked],
22
+ policy_name: policy.name,
23
+ policy_violated: checks[:policy][:violated],
24
+ policy_findings: checks[:policy][:findings],
25
+ risk_score: RiskScorer.call(checks, analysis),
26
+ checks: ordered
30
27
  }
28
+ result[:llm_analysis] = analysis if analysis
29
+ result
31
30
  end
32
-
33
- def policy(check, configuration)
34
- {
35
- policy_name: configuration.name,
36
- policy_violated: check[:violated],
37
- policy_findings: check[:findings]
38
- }
39
- end
40
-
41
- def risk(checks, ordered, analysis)
42
- { risk_score: RiskScorer.call(checks, analysis), checks: ordered }
43
- end
44
-
45
- def analysis_result(analysis)
46
- analysis ? { llm_analysis: analysis } : {}
47
- end
48
- private_class_method :base_result, :detection, :policy, :risk, :analysis_result
31
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
49
32
  end
50
33
  end
51
34
  end
@@ -5,6 +5,7 @@ require_relative 'check_analyzer'
5
5
  require_relative 'check_set'
6
6
  require_relative 'policy'
7
7
  require_relative 'validation'
8
+ require_relative 'message_source'
8
9
 
9
10
  module Olyx
10
11
  module Guardrails
@@ -14,15 +15,18 @@ module Olyx
14
15
  new(input, **).call
15
16
  end
16
17
 
17
- def initialize(input, policy:, llm_provider:)
18
- @source = input.to_s
18
+ def initialize(input, policy:, llm_provider:, structured: false)
19
+ Validation.array_of!(input, Hash, name: 'messages') if structured
20
+ @messages = input if structured
21
+ @source = input.to_s unless structured
19
22
  @policy = policy
20
23
  @llm_provider = llm_provider
21
24
  validate_options!
25
+ @source = MessageSource.call(input) if structured
22
26
  end
23
27
 
24
28
  def call
25
- checks = CheckSet.call(@source, policy: @policy)
29
+ checks = CheckSet.call(@source, policy: @policy, messages: @messages)
26
30
  merged, analysis = CheckAnalyzer.call(
27
31
  checks, provider: @llm_provider, source: @source, policy: @policy
28
32
  )
@@ -1,28 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'llm/analysis_pipeline'
4
-
5
3
  module Olyx
6
4
  module Guardrails
7
- # Normalizes and bounds responses from an untrusted optional LLM provider.
8
- class LlmAnalysis
5
+ # Validates the bounded provider contract; raw provider exceptions never escape.
6
+ module LlmAnalysis
9
7
  PROVIDER_ERROR = 'llm_provider failed'
8
+ INVALID_SHAPE_ERROR = 'llm_provider must return a Hash or a schema model with deep_to_h/to_h'
9
+ BOOLEAN_KEYS = %i[injection_attempt pii_detected secret_leaked].freeze
10
+ KEYS = [*BOOLEAN_KEYS, :risk_score, :reason].freeze
10
11
 
11
- def self.call(provider, text, context)
12
- new(provider, text, context).call
13
- end
12
+ module_function
14
13
 
15
- def initialize(provider, text, context)
16
- @provider = provider
17
- @text = text
18
- @context = context
19
- end
14
+ def call(provider, text, context)
15
+ result = normalize(provider.call(text, context))
16
+ return { error: INVALID_SHAPE_ERROR } unless result
20
17
 
21
- def call
22
- Llm::AnalysisPipeline.call(@provider.call(@text, @context))
18
+ selected = select_fields(result)
19
+ validation_error(selected) || bound_reason(selected)
23
20
  rescue StandardError
24
21
  { error: PROVIDER_ERROR }
25
22
  end
23
+
24
+ def normalize(value)
25
+ return value if value.is_a?(Hash)
26
+
27
+ converted = convert(value)
28
+ converted if converted.is_a?(Hash)
29
+ end
30
+
31
+ def convert(value)
32
+ return value.deep_to_h if value.respond_to?(:deep_to_h)
33
+
34
+ value.to_h if value.respond_to?(:to_h)
35
+ end
36
+
37
+ def select_fields(result)
38
+ KEYS.each_with_object({}) do |key, output|
39
+ source_key = result.key?(key) ? key : key.to_s
40
+ output[key] = result[source_key] if result.key?(source_key)
41
+ end
42
+ end
43
+
44
+ def validation_error(result)
45
+ key = BOOLEAN_KEYS.find { |field| result.key?(field) && ![true, false].include?(result[field]) }
46
+ { error: "llm_provider #{key} must be true or false" } if key
47
+ end
48
+
49
+ def bound_reason(result)
50
+ result[:reason] = result[:reason].to_s[0...500] if result.key?(:reason)
51
+ result
52
+ end
53
+ private_class_method :normalize, :convert, :select_fields, :validation_error, :bound_reason
26
54
  end
27
55
  end
28
56
  end
@@ -7,7 +7,7 @@ module Olyx
7
7
  module_function
8
8
 
9
9
  def text(message)
10
- content = message['content'] || message[:content]
10
+ content = value(message, 'content')
11
11
  return content if content.is_a?(String)
12
12
  return content.filter_map { |block| block_text(block) }.join(' ') if content.is_a?(Array)
13
13
 
@@ -15,13 +15,26 @@ module Olyx
15
15
  end
16
16
 
17
17
  def role(message)
18
- (message['role'] || message[:role]).to_s.downcase
18
+ value(message, 'role').to_s.downcase
19
+ end
20
+
21
+ # Redaction preserves a present String key, including explicit nil or false.
22
+ def key(message, name)
23
+ return name if message.key?(name)
24
+
25
+ symbol = name.to_sym
26
+ symbol if message.key?(symbol)
27
+ end
28
+
29
+ # Reading preserves the existing truthy String-then-Symbol fallback.
30
+ def value(message, name)
31
+ message[name] || message[name.to_sym]
19
32
  end
20
33
 
21
34
  def block_text(block)
22
- block['text'] || block[:text] if block.is_a?(Hash)
35
+ value(block, 'text') if block.is_a?(Hash)
23
36
  end
24
- private_class_method :block_text
37
+ private_class_method :block_text, :value
25
38
  end
26
39
  end
27
40
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'hash_key'
3
+ require_relative '../message_content'
4
4
  require_relative 'text_scrubber'
5
5
 
6
6
  module Olyx
@@ -13,7 +13,7 @@ module Olyx
13
13
  def call(block)
14
14
  return [block, false] unless block.is_a?(Hash)
15
15
 
16
- key = HashKey.call(block, 'text')
16
+ key = MessageContent.key(block, 'text')
17
17
  text = block[key] if key
18
18
  return [block, false] unless text.is_a?(String)
19
19
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative '../validation'
4
4
  require_relative 'content_scrubber'
5
- require_relative 'hash_key'
5
+ require_relative '../message_content'
6
6
 
7
7
  module Olyx
8
8
  module Guardrails
@@ -18,7 +18,7 @@ module Olyx
18
18
  end
19
19
 
20
20
  def scrub_message(message)
21
- key = HashKey.call(message, 'content')
21
+ key = MessageContent.key(message, 'content')
22
22
  return [message, false] unless key
23
23
 
24
24
  redacted, changed = ContentScrubber.call(message[key])
@@ -1,12 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'ambiguous_numeric_date'
4
- require_relative 'year_first_date'
3
+ require 'date'
5
4
 
6
5
  module Olyx
7
6
  module Guardrails
8
7
  module Pii
9
- # Validates year-first or ambiguous day/month numeric dates.
8
+ # Validates year-first and ambiguous day/month dates without guessing a locale.
10
9
  module NumericDateValidator
11
10
  PARTS = %r{\A(\d{1,4})[/-](\d{1,2})[/-](\d{1,4})\z}
12
11
 
@@ -15,11 +14,17 @@ module Olyx
15
14
  def call(value)
16
15
  parts = value.match(PARTS)&.captures
17
16
  return false unless parts
17
+ return Date.valid_date?(*parts.map(&:to_i)) if parts.first.length == 4
18
18
 
19
- return YearFirstDate.call(parts) if parts.first.length == 4
19
+ ambiguous_date?(parts)
20
+ end
20
21
 
21
- AmbiguousNumericDate.call(parts)
22
+ def ambiguous_date?(parts)
23
+ first, second, year = parts.map(&:to_i)
24
+ year += 2_000 if parts.last.length == 2
25
+ Date.valid_date?(year, second, first) || Date.valid_date?(year, first, second)
22
26
  end
27
+ private_class_method :ambiguous_date?
23
28
  end
24
29
  end
25
30
  end
@@ -3,6 +3,6 @@
3
3
  module Olyx
4
4
  module Guardrails
5
5
  # The current gem version, following semantic versioning.
6
- VERSION = '1.2.0'
6
+ VERSION = '1.2.1'
7
7
  end
8
8
  end
@@ -9,7 +9,6 @@ require_relative 'guardrails/injection_detector'
9
9
  require_relative 'guardrails/secret_scanner'
10
10
  require_relative 'guardrails/policy'
11
11
  require_relative 'guardrails/check_runner'
12
- require_relative 'guardrails/message_check_runner'
13
12
  require_relative 'guardrails/redactor'
14
13
  require_relative 'guardrails/notifier'
15
14
  require_relative 'guardrails/rails' if defined?(Rails::Railtie)
@@ -74,7 +73,7 @@ module Olyx
74
73
  # +policy+ and +llm_provider+ behave as they do for check. Invalid message
75
74
  # structure raises ArgumentError.
76
75
  def self.check_messages(messages, policy: Policy.default, llm_provider: nil)
77
- MessageCheckRunner.call(messages, policy: policy, llm_provider: llm_provider)
76
+ CheckRunner.call(messages, policy: policy, llm_provider: llm_provider, structured: true)
78
77
  end
79
78
 
80
79
  # :call-seq:
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: olyx-guardrails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Moses Njoroge
@@ -36,7 +36,7 @@ description: 'Standalone Ruby library for completed AI input and output safety.
36
36
  notifications — runs entirely in-process with one lightweight bundled-gem dependency.
37
37
  Ships with a provider-agnostic llm_provider: hook for LLM-backed semantic evaluation.'
38
38
  email:
39
- - mosesnjoroge@olyxai.io
39
+ - hello@olyxai.io
40
40
  executables: []
41
41
  extensions: []
42
42
  extra_rdoc_files:
@@ -58,15 +58,10 @@ files:
58
58
  - lib/olyx/guardrails/errors.rb
59
59
  - lib/olyx/guardrails/injection_detector.rb
60
60
  - lib/olyx/guardrails/injection_patterns.rb
61
- - lib/olyx/guardrails/llm/analysis_normalizer.rb
62
- - lib/olyx/guardrails/llm/analysis_pipeline.rb
63
- - lib/olyx/guardrails/llm/boolean_validator.rb
64
- - lib/olyx/guardrails/llm/result_sanitizer.rb
65
61
  - lib/olyx/guardrails/llm_analysis.rb
66
62
  - lib/olyx/guardrails/llm_context_builder.rb
67
63
  - lib/olyx/guardrails/llm_failure_handler.rb
68
64
  - lib/olyx/guardrails/llm_finding_merger.rb
69
- - lib/olyx/guardrails/message_check_runner.rb
70
65
  - lib/olyx/guardrails/message_content.rb
71
66
  - lib/olyx/guardrails/message_source.rb
72
67
  - lib/olyx/guardrails/multi_turn_injection_detector.rb
@@ -88,11 +83,9 @@ files:
88
83
  - lib/olyx/guardrails/notification_sanitizer.rb
89
84
  - lib/olyx/guardrails/notifier.rb
90
85
  - lib/olyx/guardrails/notifier_configuration.rb
91
- - lib/olyx/guardrails/pii/ambiguous_numeric_date.rb
92
86
  - lib/olyx/guardrails/pii/block_scrubber.rb
93
87
  - lib/olyx/guardrails/pii/content_scrubber.rb
94
88
  - lib/olyx/guardrails/pii/date_validator.rb
95
- - lib/olyx/guardrails/pii/hash_key.rb
96
89
  - lib/olyx/guardrails/pii/iban_remainder.rb
97
90
  - lib/olyx/guardrails/pii/iban_validator.rb
98
91
  - lib/olyx/guardrails/pii/ipv4_validator.rb
@@ -106,7 +99,6 @@ files:
106
99
  - lib/olyx/guardrails/pii/sin_validator.rb
107
100
  - lib/olyx/guardrails/pii/ssn_validator.rb
108
101
  - lib/olyx/guardrails/pii/text_scrubber.rb
109
- - lib/olyx/guardrails/pii/year_first_date.rb
110
102
  - lib/olyx/guardrails/pii_scrubber.rb
111
103
  - lib/olyx/guardrails/pii_validators.rb
112
104
  - lib/olyx/guardrails/policy.rb
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Olyx
4
- module Guardrails
5
- module Llm
6
- # Converts supported schema-model results into Hash values.
7
- module AnalysisNormalizer
8
- module_function
9
-
10
- def call(value)
11
- return value if value.is_a?(Hash)
12
-
13
- converted = convert(value)
14
- converted if converted.is_a?(Hash)
15
- end
16
-
17
- def convert(value)
18
- return value.deep_to_h if value.respond_to?(:deep_to_h)
19
-
20
- value.to_h if value.respond_to?(:to_h)
21
- end
22
- private_class_method :convert
23
- end
24
- end
25
- end
26
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'analysis_normalizer'
4
- require_relative 'boolean_validator'
5
- require_relative 'result_sanitizer'
6
-
7
- module Olyx
8
- module Guardrails
9
- module Llm
10
- # Applies normalization, selection, Boolean validation, and reason bounds.
11
- module AnalysisPipeline
12
- INVALID_SHAPE_ERROR = 'llm_provider must return a Hash or a schema model with deep_to_h/to_h'
13
-
14
- module_function
15
-
16
- def call(value)
17
- result = AnalysisNormalizer.call(value)
18
- return { error: INVALID_SHAPE_ERROR } unless result
19
-
20
- sanitized = ResultSanitizer.call(result)
21
- validation_error(sanitized) || bound_reason(sanitized)
22
- end
23
-
24
- def validation_error(result)
25
- invalid_key = BooleanValidator.invalid_key(result)
26
- { error: "llm_provider #{invalid_key} must be true or false" } if invalid_key
27
- end
28
-
29
- def bound_reason(result)
30
- result[:reason] = result[:reason].to_s[0...500] if result.key?(:reason)
31
- result
32
- end
33
- private_class_method :validation_error, :bound_reason
34
- end
35
- end
36
- end
37
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Olyx
4
- module Guardrails
5
- module Llm
6
- # Finds the first provider flag whose value is not Boolean.
7
- module BooleanValidator
8
- KEYS = %i[injection_attempt pii_detected secret_leaked].freeze
9
-
10
- module_function
11
-
12
- def invalid_key(analysis)
13
- KEYS.find { |key| analysis.key?(key) && ![true, false].include?(analysis[key]) }
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Olyx
4
- module Guardrails
5
- module Llm
6
- # Selects the bounded provider contract from an untrusted result Hash.
7
- module ResultSanitizer
8
- KEYS = %i[injection_attempt pii_detected secret_leaked risk_score reason].freeze
9
-
10
- module_function
11
-
12
- def call(result)
13
- KEYS.each_with_object({}) do |key, output|
14
- value, present = value_for(result, key)
15
- output[key] = value if present
16
- end
17
- end
18
-
19
- def value_for(result, key)
20
- return [result[key], true] if result.key?(key)
21
-
22
- string_key = key.to_s
23
- [result[string_key], result.key?(string_key)]
24
- end
25
- private_class_method :value_for
26
- end
27
- end
28
- end
29
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'check_analyzer'
4
- require_relative 'check_result_builder'
5
- require_relative 'check_set'
6
- require_relative 'message_source'
7
- require_relative 'policy'
8
- require_relative 'validation'
9
-
10
- module Olyx
11
- module Guardrails
12
- # Orchestrates checks for structured chat messages.
13
- class MessageCheckRunner
14
- def self.call(messages, policy: Policy.default, llm_provider: nil)
15
- Validation.array_of!(messages, Hash, name: 'messages')
16
- raise ArgumentError, 'policy must be an Olyx::Guardrails::Policy' unless policy.is_a?(Policy)
17
-
18
- Validation.callable_or_nil!(llm_provider, name: 'llm_provider')
19
- source = MessageSource.call(messages)
20
- checks = CheckSet.call(source, policy: policy, messages: messages)
21
- merged, analysis = CheckAnalyzer.call(checks, provider: llm_provider, source: source, policy: policy)
22
- CheckResultBuilder.call(merged, analysis, policy)
23
- end
24
- end
25
- end
26
- end
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'date'
4
-
5
- module Olyx
6
- module Guardrails
7
- module Pii
8
- # Accepts valid day-first or month-first numeric dates.
9
- module AmbiguousNumericDate
10
- module_function
11
-
12
- def call(parts)
13
- first, second, raw_year = parts.map(&:to_i)
14
- valid?(first, second, year(raw_year, parts.last))
15
- end
16
-
17
- def valid?(first, second, year)
18
- candidates(first, second).any? { |month, day| Date.valid_date?(year, month, day) }
19
- end
20
-
21
- def candidates(first, second)
22
- [[second, first], [first, second]]
23
- end
24
-
25
- def year(value, source)
26
- source.length == 2 ? value + 2_000 : value
27
- end
28
- private_class_method :candidates, :valid?, :year
29
- end
30
- end
31
- end
32
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Olyx
4
- module Guardrails
5
- module Pii
6
- # Finds String or Symbol variants of a Hash key.
7
- module HashKey
8
- module_function
9
-
10
- def call(hash, name)
11
- return name if hash.key?(name)
12
-
13
- symbol = name.to_sym
14
- symbol if hash.key?(symbol)
15
- end
16
- end
17
- end
18
- end
19
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'date'
4
-
5
- module Olyx
6
- module Guardrails
7
- module Pii
8
- # Validates numeric dates whose first component is a four-digit year.
9
- module YearFirstDate
10
- module_function
11
-
12
- def call(parts)
13
- Date.valid_date?(*parts.map(&:to_i))
14
- end
15
- end
16
- end
17
- end
18
- end