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 +4 -4
- data/CHANGELOG.md +18 -14
- data/README.md +107 -176
- data/lib/olyx/guardrails/check_result_builder.rb +12 -29
- data/lib/olyx/guardrails/check_runner.rb +7 -3
- data/lib/olyx/guardrails/llm_analysis.rb +42 -14
- data/lib/olyx/guardrails/message_content.rb +17 -4
- data/lib/olyx/guardrails/pii/block_scrubber.rb +2 -2
- data/lib/olyx/guardrails/pii/message_scrubber.rb +2 -2
- data/lib/olyx/guardrails/pii/numeric_date_validator.rb +10 -5
- data/lib/olyx/guardrails/version.rb +1 -1
- data/lib/olyx/guardrails.rb +1 -2
- metadata +2 -10
- data/lib/olyx/guardrails/llm/analysis_normalizer.rb +0 -26
- data/lib/olyx/guardrails/llm/analysis_pipeline.rb +0 -37
- data/lib/olyx/guardrails/llm/boolean_validator.rb +0 -18
- data/lib/olyx/guardrails/llm/result_sanitizer.rb +0 -29
- data/lib/olyx/guardrails/message_check_runner.rb +0 -26
- data/lib/olyx/guardrails/pii/ambiguous_numeric_date.rb +0 -32
- data/lib/olyx/guardrails/pii/hash_key.rb +0 -19
- data/lib/olyx/guardrails/pii/year_first_date.rb +0 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 196f3e181407760bea2e26500d3e3eaa5ce29b886922ad361eab20e98ea76b68
|
|
4
|
+
data.tar.gz: 24144315dc438469cc42ca4d876834b44c3ad3b1b42430a80d6bc693e57ceec3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
[](https://securityscorecards.dev/viewer/?uri=github.com/Olyx-labs/olyx-guardrails)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
##
|
|
20
|
+
## Install
|
|
24
21
|
|
|
25
|
-
|
|
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
|
-
|
|
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.
|
|
28
|
+
gem "olyx-guardrails", "~> 1.2"
|
|
44
29
|
```
|
|
45
30
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```bash
|
|
31
|
+
```sh
|
|
49
32
|
bundle install
|
|
50
33
|
```
|
|
51
34
|
|
|
52
|
-
The only runtime gem dependency is Ruby's `base64`
|
|
53
|
-
bounded encoded-input detection.
|
|
35
|
+
The only runtime gem dependency is Ruby's bundled `base64` gem.
|
|
54
36
|
|
|
55
|
-
##
|
|
37
|
+
## Try it
|
|
56
38
|
|
|
57
|
-
|
|
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: "
|
|
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
|
-
|
|
63
|
+
abort "Input rejected" unless decision[:allowed]
|
|
81
64
|
|
|
82
|
-
|
|
83
|
-
|
|
65
|
+
puts "Allowed: #{decision[:allowed]}"
|
|
66
|
+
puts Olyx::Guardrails.redact(input, policy: policy)[:text]
|
|
84
67
|
|
|
85
|
-
|
|
86
|
-
|
|
68
|
+
restricted = Olyx::Guardrails.check("Discuss Project Falcon", policy: policy)
|
|
69
|
+
puts "Restricted input allowed: #{restricted[:allowed]}"
|
|
87
70
|
```
|
|
88
71
|
|
|
89
|
-
|
|
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
|
-
|
|
74
|
+
```sh
|
|
75
|
+
bundle exec ruby guardrails_example.rb
|
|
76
|
+
```
|
|
94
77
|
|
|
95
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
[
|
|
103
|
-
|
|
80
|
+
```text
|
|
81
|
+
Allowed: true
|
|
82
|
+
Contact [EMAIL] about the release.
|
|
83
|
+
Restricted input allowed: false
|
|
84
|
+
```
|
|
104
85
|
|
|
105
|
-
|
|
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
|
-
```
|
|
104
|
+
```sh
|
|
110
105
|
bin/rails generate olyx_guardrails:install
|
|
111
106
|
```
|
|
112
107
|
|
|
113
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
129
|
+
render json: { completion: safe_output }
|
|
135
130
|
end
|
|
136
131
|
end
|
|
137
132
|
```
|
|
138
133
|
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
163
|
+
## Limitations
|
|
182
164
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
198
|
-
[
|
|
199
|
-
|
|
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
|
|
212
|
-
- [API reference](https://github.com/Olyx-labs/olyx-guardrails/blob/master/docs/API.md)
|
|
213
|
-
- [
|
|
214
|
-
- [
|
|
215
|
-
- [
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
new(provider, text, context).call
|
|
13
|
-
end
|
|
12
|
+
module_function
|
|
14
13
|
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
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
|
|
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
|
|
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 '
|
|
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 =
|
|
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 '
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
19
|
+
ambiguous_date?(parts)
|
|
20
|
+
end
|
|
20
21
|
|
|
21
|
-
|
|
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
|
data/lib/olyx/guardrails.rb
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
-
|
|
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
|