olyx-guardrails 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +117 -0
  3. data/CODE_OF_CONDUCT.md +28 -0
  4. data/CONTRIBUTING.md +63 -0
  5. data/LICENSE +134 -0
  6. data/README.md +611 -0
  7. data/SECURITY.md +35 -0
  8. data/docs/API.md +460 -0
  9. data/examples/claude_analyzer.rb +53 -0
  10. data/examples/custom_policy.rb +41 -0
  11. data/examples/notifier.rb +37 -0
  12. data/examples/openai_analyzer.rb +45 -0
  13. data/examples/rails_opt_in.rb +99 -0
  14. data/examples/ruby_only.rb +53 -0
  15. data/lib/generators/olyx_guardrails/install_generator.rb +22 -0
  16. data/lib/generators/olyx_guardrails/templates/initializer.rb.tt +16 -0
  17. data/lib/generators/olyx_guardrails/templates/policy.yml.tt +41 -0
  18. data/lib/olyx/guardrails/ai/analysis_normalizer.rb +26 -0
  19. data/lib/olyx/guardrails/ai/analysis_pipeline.rb +26 -0
  20. data/lib/olyx/guardrails/ai/boolean_validator.rb +18 -0
  21. data/lib/olyx/guardrails/ai/flag_finding_merger.rb +16 -0
  22. data/lib/olyx/guardrails/ai/result_sanitizer.rb +29 -0
  23. data/lib/olyx/guardrails/ai/secret_finding_merger.rb +19 -0
  24. data/lib/olyx/guardrails/ai/standard_finding_merger.rb +24 -0
  25. data/lib/olyx/guardrails/ai_analysis.rb +29 -0
  26. data/lib/olyx/guardrails/ai_context_builder.rb +32 -0
  27. data/lib/olyx/guardrails/ai_failure_handler.rb +17 -0
  28. data/lib/olyx/guardrails/ai_finding_merger.rb +30 -0
  29. data/lib/olyx/guardrails/check_analyzer.rb +26 -0
  30. data/lib/olyx/guardrails/check_base_result.rb +32 -0
  31. data/lib/olyx/guardrails/check_pipeline.rb +17 -0
  32. data/lib/olyx/guardrails/check_result_builder.rb +24 -0
  33. data/lib/olyx/guardrails/check_runner.rb +42 -0
  34. data/lib/olyx/guardrails/check_set.rb +33 -0
  35. data/lib/olyx/guardrails/checks/injection_check.rb +20 -0
  36. data/lib/olyx/guardrails/checks/injection_result.rb +22 -0
  37. data/lib/olyx/guardrails/checks/length_check.rb +18 -0
  38. data/lib/olyx/guardrails/checks/message_injection_check.rb +19 -0
  39. data/lib/olyx/guardrails/checks/pii_check.rb +19 -0
  40. data/lib/olyx/guardrails/checks/policy_check.rb +26 -0
  41. data/lib/olyx/guardrails/checks/secret_check.rb +30 -0
  42. data/lib/olyx/guardrails/checks/skipped_checks.rb +26 -0
  43. data/lib/olyx/guardrails/enum_value.rb +20 -0
  44. data/lib/olyx/guardrails/errors.rb +22 -0
  45. data/lib/olyx/guardrails/injection_detector.rb +27 -0
  46. data/lib/olyx/guardrails/injection_patterns.rb +44 -0
  47. data/lib/olyx/guardrails/integrations/openai/analyzer_setup.rb +21 -0
  48. data/lib/olyx/guardrails/integrations/openai/configuration_resolver.rb +28 -0
  49. data/lib/olyx/guardrails/integrations/openai/dependency_contracts.rb +28 -0
  50. data/lib/olyx/guardrails/integrations/openai/input_builder.rb +39 -0
  51. data/lib/olyx/guardrails/integrations/openai/member_reader.rb +21 -0
  52. data/lib/olyx/guardrails/integrations/openai/model_identifier.rb +28 -0
  53. data/lib/olyx/guardrails/integrations/openai/refusal_guard.rb +28 -0
  54. data/lib/olyx/guardrails/integrations/openai/request_builder.rb +36 -0
  55. data/lib/olyx/guardrails/integrations/openai/request_configuration.rb +25 -0
  56. data/lib/olyx/guardrails/integrations/openai/request_values.rb +33 -0
  57. data/lib/olyx/guardrails/integrations/openai/response_contents.rb +22 -0
  58. data/lib/olyx/guardrails/integrations/openai/response_option_keys.rb +32 -0
  59. data/lib/olyx/guardrails/integrations/openai/response_options.rb +23 -0
  60. data/lib/olyx/guardrails/integrations/openai/schema_registry.rb +46 -0
  61. data/lib/olyx/guardrails/integrations/openai/sdk.rb +26 -0
  62. data/lib/olyx/guardrails/integrations/openai/signal_summary.rb +40 -0
  63. data/lib/olyx/guardrails/integrations/openai_analyzer.rb +107 -0
  64. data/lib/olyx/guardrails/integrations/openai_analyzer_configuration.rb +38 -0
  65. data/lib/olyx/guardrails/integrations/openai_response_parser.rb +34 -0
  66. data/lib/olyx/guardrails/message_check_runner.rb +24 -0
  67. data/lib/olyx/guardrails/message_check_set.rb +20 -0
  68. data/lib/olyx/guardrails/message_content.rb +27 -0
  69. data/lib/olyx/guardrails/message_source.rb +16 -0
  70. data/lib/olyx/guardrails/multi_turn_injection_detector.rb +18 -0
  71. data/lib/olyx/guardrails/multi_turn_pair_scanner.rb +35 -0
  72. data/lib/olyx/guardrails/multi_turn_variant_pairs.rb +25 -0
  73. data/lib/olyx/guardrails/notification/base_event.rb +35 -0
  74. data/lib/olyx/guardrails/notification/deep_freezer.rb +25 -0
  75. data/lib/olyx/guardrails/notification/delivery_dispatcher.rb +38 -0
  76. data/lib/olyx/guardrails/notification/delivery_summary.rb +17 -0
  77. data/lib/olyx/guardrails/notification/handler.rb +33 -0
  78. data/lib/olyx/guardrails/notification/handler_collection.rb +25 -0
  79. data/lib/olyx/guardrails/notification/handler_collection_validator.rb +23 -0
  80. data/lib/olyx/guardrails/notification/key.rb +19 -0
  81. data/lib/olyx/guardrails/notification/metadata.rb +34 -0
  82. data/lib/olyx/guardrails/notification/notifier_setup.rb +19 -0
  83. data/lib/olyx/guardrails/notification/preview.rb +27 -0
  84. data/lib/olyx/guardrails/notification/scrubber.rb +22 -0
  85. data/lib/olyx/guardrails/notification/violation_labels.rb +19 -0
  86. data/lib/olyx/guardrails/notification_event_builder.rb +41 -0
  87. data/lib/olyx/guardrails/notification_sanitizer.rb +33 -0
  88. data/lib/olyx/guardrails/notifier.rb +53 -0
  89. data/lib/olyx/guardrails/notifier_configuration.rb +30 -0
  90. data/lib/olyx/guardrails/pii/ambiguous_numeric_date.rb +32 -0
  91. data/lib/olyx/guardrails/pii/block_scrubber.rb +31 -0
  92. data/lib/olyx/guardrails/pii/content_scrubber.rb +33 -0
  93. data/lib/olyx/guardrails/pii/date_validator.rb +25 -0
  94. data/lib/olyx/guardrails/pii/hash_key.rb +19 -0
  95. data/lib/olyx/guardrails/pii/iban_remainder.rb +25 -0
  96. data/lib/olyx/guardrails/pii/iban_validator.rb +34 -0
  97. data/lib/olyx/guardrails/pii/ipv4_validator.rb +17 -0
  98. data/lib/olyx/guardrails/pii/ipv6_validator.rb +20 -0
  99. data/lib/olyx/guardrails/pii/luhn_checksum.rb +29 -0
  100. data/lib/olyx/guardrails/pii/luhn_validator.rb +21 -0
  101. data/lib/olyx/guardrails/pii/message_scrubber.rb +31 -0
  102. data/lib/olyx/guardrails/pii/named_date_validator.rb +28 -0
  103. data/lib/olyx/guardrails/pii/numeric_date_validator.rb +26 -0
  104. data/lib/olyx/guardrails/pii/pattern_catalog.rb +41 -0
  105. data/lib/olyx/guardrails/pii/sin_validator.rb +24 -0
  106. data/lib/olyx/guardrails/pii/ssn_validator.rb +21 -0
  107. data/lib/olyx/guardrails/pii/text_scrubber.rb +28 -0
  108. data/lib/olyx/guardrails/pii/year_first_date.rb +18 -0
  109. data/lib/olyx/guardrails/pii_scrubber.rb +23 -0
  110. data/lib/olyx/guardrails/pii_validators.rb +26 -0
  111. data/lib/olyx/guardrails/policy/ai_failure_mode.rb +15 -0
  112. data/lib/olyx/guardrails/policy/configuration.rb +62 -0
  113. data/lib/olyx/guardrails/policy/configuration_hash.rb +20 -0
  114. data/lib/olyx/guardrails/policy/finding_order.rb +20 -0
  115. data/lib/olyx/guardrails/policy/finding_presenter.rb +41 -0
  116. data/lib/olyx/guardrails/policy/match_collector.rb +20 -0
  117. data/lib/olyx/guardrails/policy/name.rb +23 -0
  118. data/lib/olyx/guardrails/policy/normalized_pattern_matcher.rb +34 -0
  119. data/lib/olyx/guardrails/policy/pattern_matcher.rb +19 -0
  120. data/lib/olyx/guardrails/policy/redaction_result.rb +20 -0
  121. data/lib/olyx/guardrails/policy/redaction_spans.rb +32 -0
  122. data/lib/olyx/guardrails/policy/rule_collection.rb +34 -0
  123. data/lib/olyx/guardrails/policy/rule_matcher.rb +29 -0
  124. data/lib/olyx/guardrails/policy/rule_normalizer.rb +21 -0
  125. data/lib/olyx/guardrails/policy/scan_result.rb +21 -0
  126. data/lib/olyx/guardrails/policy/secret_pattern_collection.rb +22 -0
  127. data/lib/olyx/guardrails/policy/unmatched_segment_builder.rb +28 -0
  128. data/lib/olyx/guardrails/policy/unmatched_transformer.rb +20 -0
  129. data/lib/olyx/guardrails/policy.rb +46 -0
  130. data/lib/olyx/guardrails/policy_aware_redactor.rb +42 -0
  131. data/lib/olyx/guardrails/policy_redactor.rb +28 -0
  132. data/lib/olyx/guardrails/policy_rule/configuration.rb +46 -0
  133. data/lib/olyx/guardrails/policy_rule/description_value.rb +24 -0
  134. data/lib/olyx/guardrails/policy_rule/match_mode.rb +15 -0
  135. data/lib/olyx/guardrails/policy_rule/name_value.rb +22 -0
  136. data/lib/olyx/guardrails/policy_rule/pattern_compiler.rb +28 -0
  137. data/lib/olyx/guardrails/policy_rule/regexp_compiler.rb +31 -0
  138. data/lib/olyx/guardrails/policy_rule/replacement_value.rb +26 -0
  139. data/lib/olyx/guardrails/policy_rule/term_compiler.rb +33 -0
  140. data/lib/olyx/guardrails/policy_rule/text_values.rb +23 -0
  141. data/lib/olyx/guardrails/policy_rule/values.rb +33 -0
  142. data/lib/olyx/guardrails/policy_rule.rb +30 -0
  143. data/lib/olyx/guardrails/policy_scanner.rb +44 -0
  144. data/lib/olyx/guardrails/rails/action_cable.rb +21 -0
  145. data/lib/olyx/guardrails/rails/active_job_handler.rb +27 -0
  146. data/lib/olyx/guardrails/rails/active_model_validator.rb +23 -0
  147. data/lib/olyx/guardrails/rails/check_telemetry.rb +19 -0
  148. data/lib/olyx/guardrails/rails/configuration.rb +68 -0
  149. data/lib/olyx/guardrails/rails/configuration_finalizer.rb +30 -0
  150. data/lib/olyx/guardrails/rails/configuration_registry.rb +41 -0
  151. data/lib/olyx/guardrails/rails/configuration_values.rb +30 -0
  152. data/lib/olyx/guardrails/rails/constant_resolver.rb +18 -0
  153. data/lib/olyx/guardrails/rails/controller.rb +34 -0
  154. data/lib/olyx/guardrails/rails/controller_metadata.rb +22 -0
  155. data/lib/olyx/guardrails/rails/decision_service.rb +26 -0
  156. data/lib/olyx/guardrails/rails/enforcer.rb +33 -0
  157. data/lib/olyx/guardrails/rails/evaluation_lifecycle.rb +23 -0
  158. data/lib/olyx/guardrails/rails/evaluation_service.rb +12 -0
  159. data/lib/olyx/guardrails/rails/filter_parameters.rb +26 -0
  160. data/lib/olyx/guardrails/rails/graphql.rb +21 -0
  161. data/lib/olyx/guardrails/rails/ingress.rb +26 -0
  162. data/lib/olyx/guardrails/rails/input_runtime.rb +25 -0
  163. data/lib/olyx/guardrails/rails/instrumentation.rb +39 -0
  164. data/lib/olyx/guardrails/rails/instrumentation_payloads.rb +25 -0
  165. data/lib/olyx/guardrails/rails/instrumentation_publisher.rb +24 -0
  166. data/lib/olyx/guardrails/rails/integration_configuration.rb +27 -0
  167. data/lib/olyx/guardrails/rails/job.rb +37 -0
  168. data/lib/olyx/guardrails/rails/job_argument.rb +41 -0
  169. data/lib/olyx/guardrails/rails/job_reference.rb +36 -0
  170. data/lib/olyx/guardrails/rails/message_evaluation_service.rb +13 -0
  171. data/lib/olyx/guardrails/rails/message_runtime.rb +20 -0
  172. data/lib/olyx/guardrails/rails/notification_dispatcher.rb +22 -0
  173. data/lib/olyx/guardrails/rails/output_runtime.rb +26 -0
  174. data/lib/olyx/guardrails/rails/path_value.rb +30 -0
  175. data/lib/olyx/guardrails/rails/policy_configuration.rb +25 -0
  176. data/lib/olyx/guardrails/rails/policy_document.rb +40 -0
  177. data/lib/olyx/guardrails/rails/policy_file.rb +29 -0
  178. data/lib/olyx/guardrails/rails/policy_file_error.rb +25 -0
  179. data/lib/olyx/guardrails/rails/policy_rule_names.rb +19 -0
  180. data/lib/olyx/guardrails/rails/policy_yaml.rb +22 -0
  181. data/lib/olyx/guardrails/rails/queue_name.rb +21 -0
  182. data/lib/olyx/guardrails/rails/redaction_payload.rb +36 -0
  183. data/lib/olyx/guardrails/rails/redaction_service.rb +21 -0
  184. data/lib/olyx/guardrails/rails/result_summary.rb +36 -0
  185. data/lib/olyx/guardrails/rails/runtime.rb +30 -0
  186. data/lib/olyx/guardrails/rails/timer.rb +27 -0
  187. data/lib/olyx/guardrails/rails/upload.rb +34 -0
  188. data/lib/olyx/guardrails/rails.rb +35 -0
  189. data/lib/olyx/guardrails/railtie.rb +19 -0
  190. data/lib/olyx/guardrails/redaction/content_result.rb +32 -0
  191. data/lib/olyx/guardrails/redaction/input_validator.rb +17 -0
  192. data/lib/olyx/guardrails/redaction/public_result.rb +23 -0
  193. data/lib/olyx/guardrails/redaction/unrestricted_text.rb +17 -0
  194. data/lib/olyx/guardrails/redactor.rb +29 -0
  195. data/lib/olyx/guardrails/risk/ai_score.rb +19 -0
  196. data/lib/olyx/guardrails/risk/check_weights.rb +18 -0
  197. data/lib/olyx/guardrails/risk/deterministic_score.rb +20 -0
  198. data/lib/olyx/guardrails/risk/weights.rb +18 -0
  199. data/lib/olyx/guardrails/risk_scorer.rb +28 -0
  200. data/lib/olyx/guardrails/secret_finding_collector.rb +31 -0
  201. data/lib/olyx/guardrails/secret_scanner.rb +42 -0
  202. data/lib/olyx/guardrails/secrets/blocked.rb +17 -0
  203. data/lib/olyx/guardrails/secrets/catalog_source.rb +28 -0
  204. data/lib/olyx/guardrails/secrets/cloud_credential_patterns.rb +16 -0
  205. data/lib/olyx/guardrails/secrets/confidentiality_source.rb +21 -0
  206. data/lib/olyx/guardrails/secrets/custom_pattern_compiler.rb +23 -0
  207. data/lib/olyx/guardrails/secrets/custom_pattern_source.rb +30 -0
  208. data/lib/olyx/guardrails/secrets/finding_order.rb +17 -0
  209. data/lib/olyx/guardrails/secrets/finding_presenter.rb +34 -0
  210. data/lib/olyx/guardrails/secrets/generic_credential_patterns.rb +15 -0
  211. data/lib/olyx/guardrails/secrets/internal_endpoint_source.rb +28 -0
  212. data/lib/olyx/guardrails/secrets/network_patterns.rb +13 -0
  213. data/lib/olyx/guardrails/secrets/normalized_source_set.rb +27 -0
  214. data/lib/olyx/guardrails/secrets/pattern_catalog.rb +37 -0
  215. data/lib/olyx/guardrails/secrets/private_network_validator.rb +19 -0
  216. data/lib/olyx/guardrails/secrets/redaction_spans.rb +32 -0
  217. data/lib/olyx/guardrails/secrets/redactor.rb +27 -0
  218. data/lib/olyx/guardrails/secrets/regexp_finding_source.rb +21 -0
  219. data/lib/olyx/guardrails/secrets/source_set.rb +19 -0
  220. data/lib/olyx/guardrails/single_message_injection_detector.rb +35 -0
  221. data/lib/olyx/guardrails/supplemental_violation_labels.rb +22 -0
  222. data/lib/olyx/guardrails/text/base64_decoder.rb +45 -0
  223. data/lib/olyx/guardrails/text/detection_variants.rb +28 -0
  224. data/lib/olyx/guardrails/text/html_decoder.rb +18 -0
  225. data/lib/olyx/guardrails/text/mapped_builder.rb +33 -0
  226. data/lib/olyx/guardrails/text/mapped_normalization.rb +27 -0
  227. data/lib/olyx/guardrails/text/normalizer.rb +35 -0
  228. data/lib/olyx/guardrails/text/unicode_escape_decoder.rb +20 -0
  229. data/lib/olyx/guardrails/text/url_decoder.rb +20 -0
  230. data/lib/olyx/guardrails/validation.rb +33 -0
  231. data/lib/olyx/guardrails/version.rb +8 -0
  232. data/lib/olyx/guardrails/violation_labels.rb +24 -0
  233. data/lib/olyx/guardrails.rb +63 -0
  234. metadata +283 -0
data/README.md ADDED
@@ -0,0 +1,611 @@
1
+ # Olyx Guardrails
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/olyx-guardrails.svg)](https://rubygems.org/gems/olyx-guardrails)
4
+ [![Test](https://github.com/Olyx-labs/olyx-guardrails/actions/workflows/test.yml/badge.svg)](https://github.com/Olyx-labs/olyx-guardrails/actions/workflows/test.yml)
5
+ [![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)
6
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
7
+
8
+ `olyx-guardrails` is a dependency-light Ruby library for synchronous AI
9
+ boundary safety:
10
+
11
+ - PII and credential redaction
12
+ - Prompt-injection and jailbreak detection
13
+ - Secret and internal-endpoint detection
14
+ - Input-length enforcement
15
+ - Reusable named policies for organization-specific restricted content
16
+ - Structured-message checks and completed-output checks
17
+ - Optional semantic analysis through a caller-supplied AI hook
18
+ - Optional OpenAI Responses API connector with native schema models
19
+ - Vendor-neutral notifications through configurable application handlers
20
+
21
+ The core checks run in-process and have no runtime gem dependencies.
22
+
23
+ ## Installation
24
+
25
+ ```ruby
26
+ gem "olyx-guardrails", "~> 1.0"
27
+ ```
28
+
29
+ ```bash
30
+ bundle install
31
+ ```
32
+
33
+ Ruby 3.4 or newer is required. The repository pins the current Ruby 3.4 patch
34
+ release in `.ruby-version` for rbenv users.
35
+
36
+ ## Choose an integration path
37
+
38
+ - [`examples/ruby_only.rb`](examples/ruby_only.rb) is a runnable, framework-free
39
+ example with custom policy enforcement, redaction, completed-output checks,
40
+ and vendor-neutral notifications.
41
+ - [`examples/rails_opt_in.rb`](examples/rails_opt_in.rb) shows explicit Rails
42
+ boundaries for controllers, GraphQL, Action Cable, Active Job, Active Model,
43
+ uploads, callbacks, and service objects.
44
+
45
+ The Ruby-only path requires no Rails runtime. The Rails path remains opt-in and
46
+ uses the same core `Policy` and decision contracts.
47
+
48
+ ## Rails integration
49
+
50
+ The core gem remains framework-neutral. Rails applications receive an optional
51
+ Railtie, boot-validated configuration, explicit adapters for controllers,
52
+ GraphQL, Action Cable, Active Job, Active Model, uploads, and service objects,
53
+ sanitized Active Support events, filtered AI parameter names, and an Active Job
54
+ notifier adapter. Enforcement remains opt-in: the gem never guesses which
55
+ application values are AI-bound.
56
+
57
+ Install the Rails configuration:
58
+
59
+ ```bash
60
+ bin/rails generate olyx_guardrails:install
61
+ ```
62
+
63
+ This creates `config/initializers/olyx_guardrails.rb` and an environment-keyed
64
+ `config/olyx_guardrails.yml`. YAML is loaded with `YAML.safe_load`; ERB,
65
+ aliases, arbitrary classes, and symbols are not evaluated. A missing,
66
+ unreadable, unsafe, or invalid configured policy stops application boot instead
67
+ of falling back to a permissive policy.
68
+
69
+ Use the explicit controller concern at the AI boundary:
70
+
71
+ ```ruby
72
+ class AiRequestsController < ApplicationController
73
+ include Olyx::Guardrails::Rails::Controller
74
+
75
+ rescue_from Olyx::Guardrails::Blocked do |error|
76
+ render json: { error: "input_rejected", decision: error.decision },
77
+ status: :unprocessable_entity
78
+ end
79
+
80
+ def create
81
+ guardrails_check!(
82
+ params.require(:prompt),
83
+ metadata: { user_id: current_user.id }
84
+ )
85
+ safe_prompt = guardrails_redact(params[:prompt])[:text]
86
+
87
+ render json: LlmClient.complete(safe_prompt), status: :created
88
+ end
89
+ end
90
+ ```
91
+
92
+ The exception contains a frozen decision summary—policy name, risk score,
93
+ violation labels, and restricted rule identifiers—but never the input or AI
94
+ reason. The application owns the HTTP response because `403`, `422`, and other
95
+ policies have different semantics.
96
+
97
+ Protect the other common Rails ingress paths explicitly:
98
+
99
+ ```ruby
100
+ class CompletionResolver
101
+ include Olyx::Guardrails::Rails::GraphQL
102
+
103
+ def resolve(prompt:)
104
+ guardrails_check_graphql!(prompt)
105
+ result = LlmClient.complete(prompt)
106
+ guardrails_check_graphql_output!(result)
107
+ result
108
+ end
109
+ end
110
+
111
+ class PromptChannel < ApplicationCable::Channel
112
+ include Olyx::Guardrails::Rails::ActionCable
113
+
114
+ def receive(data)
115
+ guardrails_check_cable!(data.fetch("prompt"))
116
+ end
117
+ end
118
+
119
+ class CompletionJob < ApplicationJob
120
+ include Olyx::Guardrails::Rails::Job
121
+ guardrails_input_arguments 0, :system_prompt
122
+ end
123
+
124
+ class PromptDraft
125
+ include ActiveModel::Model
126
+ attr_accessor :prompt
127
+ validates :prompt, olyx_guardrails: true
128
+ end
129
+ ```
130
+
131
+ For callbacks and service objects, call
132
+ `Olyx::Guardrails::Rails::Enforcer.check!`, `check_messages!`, or
133
+ `check_output!`. For uploads, the application retains ownership of parsing and
134
+ passes a bounded text extractor:
135
+
136
+ ```ruby
137
+ Olyx::Guardrails::Rails::Upload.check!(
138
+ params.require(:document),
139
+ extractor: ->(upload) { DocumentText.extract(upload.tempfile) }
140
+ )
141
+ ```
142
+
143
+ These adapters close framework routing gaps; they do not parse arbitrary file
144
+ formats, discover model calls, or intercept every callback automatically.
145
+
146
+ Rails checks publish content-free Active Support events:
147
+
148
+ ```ruby
149
+ ActiveSupport::Notifications.subscribe("violation.olyx_guardrails") do |event|
150
+ Rails.logger.warn(event.payload.to_json)
151
+ end
152
+ ```
153
+
154
+ Available events are `check.olyx_guardrails`, `violation.olyx_guardrails`,
155
+ `redact.olyx_guardrails`, and `notification.olyx_guardrails`. They include
156
+ bounded decision or delivery fields and evaluation duration, never raw input,
157
+ redacted output, metadata, or analyzer prose. Subscriber failures are isolated
158
+ from enforcement.
159
+
160
+ Use any Active Job backend for asynchronous notification delivery:
161
+
162
+ ```ruby
163
+ Olyx::Guardrails::Rails.configure do |config|
164
+ config.policy_path = Rails.root.join("config/olyx_guardrails.yml")
165
+ config.notifier_handlers = {
166
+ incidents: Olyx::Guardrails::Rails::ActiveJobHandler.new(
167
+ job: "GuardrailIncidentJob",
168
+ queue: :low
169
+ )
170
+ }
171
+ end
172
+ ```
173
+
174
+ The String job name is resolved when the event is delivered, which remains
175
+ compatible with Rails code reloading. The job receives the same sanitized,
176
+ immutable vendor-neutral event described under Notifications.
177
+
178
+ ## Configure a reusable policy
179
+
180
+ All enforcement settings and organization-specific restrictions live in one
181
+ immutable policy object:
182
+
183
+ ```ruby
184
+ require "olyx/guardrails"
185
+
186
+ POLICY = Olyx::Guardrails::Policy.new(
187
+ name: "customer-data-boundary",
188
+ max_input_length: 4_000,
189
+ block_pii: true,
190
+ block_injections: true,
191
+ block_secrets: true,
192
+ ai_failure_mode: :block,
193
+ secret_patterns: ["company-token-[a-z0-9]{24}"],
194
+ rules: [
195
+ {
196
+ name: :confidential_projects,
197
+ description: "Project names that must not enter an AI request",
198
+ patterns: ["project[ -]falcon", /PF-\d{4}/],
199
+ block: true,
200
+ replacement: "[CONFIDENTIAL_PROJECT]"
201
+ },
202
+ {
203
+ name: :competitor_references,
204
+ terms: ["competitor corp"],
205
+ match: :whole_word,
206
+ block: false
207
+ }
208
+ ]
209
+ )
210
+ ```
211
+
212
+ Use `terms` for literal, case-insensitive restricted text; regex metacharacters
213
+ inside a term are escaped automatically. `match:` controls term interpretation:
214
+ `:substring` (default), `:whole_word`, or `:regexp`. String `patterns` are always
215
+ regular-expression source and compiled case-insensitively, while `Regexp`
216
+ patterns retain their flags. Each rule must define at least one term or pattern.
217
+ A blocking rule makes `check` reject the input; a non-blocking rule records the
218
+ violation for monitoring. `redact` transforms matches from either kind. Rule
219
+ configuration is compiled once when the policy is created, rejects
220
+ empty-matching or invalid expressions, and applies a timeout during matching.
221
+
222
+ `ai_failure_mode:` controls an optional analyzer failure: `:allow` keeps the
223
+ deterministic decision (the default), `:block` adds a failed AI check and rejects
224
+ the request, and `:raise` raises `Olyx::Guardrails::AiAnalyzerError`. Production
225
+ applications that require semantic analysis should choose `:block` or `:raise`
226
+ explicitly.
227
+
228
+ String-keyed configuration loaded from YAML or JSON is supported:
229
+
230
+ ```ruby
231
+ policy = Olyx::Guardrails::Policy.from_h(configuration)
232
+ ```
233
+
234
+ Policy findings are separate from secret findings. They expose the rule name,
235
+ description, block decision, `[REDACTED]` marker, SHA-256 fingerprint, and offsets;
236
+ the matched restricted text is never returned in plaintext.
237
+ Use `secret_patterns` for organization-specific credential formats that should
238
+ remain classified as secrets; use `rules` for business restrictions.
239
+ See [`examples/custom_policy.rb`](examples/custom_policy.rb) for a complete
240
+ decision-and-redaction flow.
241
+
242
+ ## Decision and transformation are separate
243
+
244
+ Use `check` to make an allow/block decision:
245
+
246
+ ```ruby
247
+ require "olyx/guardrails"
248
+
249
+ result = Olyx::Guardrails.check(
250
+ input,
251
+ policy: POLICY
252
+ )
253
+
254
+ return forbidden unless result[:allowed]
255
+ ```
256
+
257
+ Use `redact` when transformed text is required:
258
+
259
+ ```ruby
260
+ redaction = Olyx::Guardrails.redact(input, policy: POLICY)
261
+
262
+ safe_input = redaction[:text]
263
+ redaction[:redacted] # true when text changed
264
+ redaction[:pii_detected] # true when regex-detected PII was removed
265
+ redaction[:secret_leaked] # true when a secret was removed
266
+ ```
267
+
268
+ `check` never claims to transform input, and `redact` never makes an
269
+ allow/block decision.
270
+
271
+ Use structured messages when adjacent turns matter, and explicit output names
272
+ at the completed model boundary:
273
+
274
+ ```ruby
275
+ message_result = Olyx::Guardrails.check_messages(
276
+ [{role: "user", content: prompt}, {role: "assistant", content: draft}],
277
+ policy: POLICY
278
+ )
279
+
280
+ output_result = Olyx::Guardrails.check_output(completion, policy: POLICY)
281
+ safe_output = Olyx::Guardrails.redact_output(completion, policy: POLICY)[:text]
282
+ ```
283
+
284
+ `check_output` and `redact_output` operate on completed values. They are
285
+ deliberately distinct entry-point names so applications can instrument input
286
+ and output boundaries without implying token-stream enforcement.
287
+
288
+ ## `Olyx::Guardrails.check`
289
+
290
+ ```ruby
291
+ Olyx::Guardrails.check(
292
+ input,
293
+ policy: Olyx::Guardrails::Policy.default,
294
+ ai_analyzer: nil
295
+ )
296
+ ```
297
+
298
+ | Option | Type | Default | Description |
299
+ |---|---|---|---|
300
+ | `input` | any | — | Converted via `to_s` |
301
+ | `policy` | `Olyx::Guardrails::Policy` | `Policy.default` | Limits, built-in blocking behavior, and named restricted-content rules |
302
+ | `ai_analyzer` | callable or nil | `nil` | Optional semantic analyzer |
303
+
304
+ `Policy.default` preserves the built-in defaults: 10,000-character input limit,
305
+ block injections, flag PII and secrets, and no custom restricted-content
306
+ rules. Invalid policy configuration raises `ArgumentError` when the policy is
307
+ constructed; mistakes do not silently degrade into an allow decision.
308
+
309
+ ```ruby
310
+ {
311
+ allowed: Boolean,
312
+ pii_detected: Boolean,
313
+ injection_attempt: Boolean,
314
+ secret_leaked: Boolean,
315
+ policy_name: String,
316
+ policy_violated: Boolean,
317
+ policy_findings: Array<Hash>,
318
+ risk_score: Float,
319
+ checks: [
320
+ { type: "pii", allowed: true, detected: Boolean },
321
+ { type: "injection", allowed: Boolean, injection_attempt: Boolean, patterns: Array },
322
+ { type: "secret", allowed: Boolean, leaked: Boolean, count: Integer },
323
+ { type: "policy", allowed: Boolean, violated: Boolean, count: Integer, findings: Array },
324
+ { type: "length", allowed: Boolean, length: Integer, max_length: Integer }
325
+ ],
326
+ ai_analysis: Hash # only when ai_analyzer is supplied and runs
327
+ }
328
+ ```
329
+
330
+ The length check runs first. When it fails, content and AI checks are skipped and
331
+ content checks carry `skipped: true`.
332
+
333
+ ## `Olyx::Guardrails.redact`
334
+
335
+ ```ruby
336
+ Olyx::Guardrails.redact(
337
+ input,
338
+ policy: Olyx::Guardrails::Policy.default
339
+ )
340
+ ```
341
+
342
+ The returned `:text` has every regex-detected PII and secret match removed.
343
+ Repeated credentials in the same category are all redacted. Findings expose a
344
+ masked value, a short SHA-256 fingerprint, and source offsets—not plaintext
345
+ credentials.
346
+
347
+ ```ruby
348
+ {
349
+ text: String,
350
+ redacted: Boolean,
351
+ pii_detected: Boolean,
352
+ secret_leaked: Boolean,
353
+ policy_name: String,
354
+ policy_violated: Boolean,
355
+ policy_findings: Array<Hash>,
356
+ findings: [
357
+ {
358
+ category: String,
359
+ matched: String, # masked
360
+ fingerprint: String, # "sha256:..."
361
+ start: Integer,
362
+ end: Integer
363
+ }
364
+ ]
365
+ }
366
+ ```
367
+
368
+ An oversized input raises `ArgumentError` rather than running an unbounded
369
+ transformation.
370
+
371
+ ## AI analyzer hook
372
+
373
+ The optional hook receives `(text, context)` and returns a Hash or a schema
374
+ model implementing `deep_to_h`/`to_h`:
375
+
376
+ ```ruby
377
+ hook = lambda do |text, context|
378
+ {
379
+ injection_attempt: false,
380
+ pii_detected: false,
381
+ secret_leaked: false,
382
+ risk_score: 0.2,
383
+ reason: "No semantic policy violation found"
384
+ }
385
+ end
386
+
387
+ result = Olyx::Guardrails.check(input, ai_analyzer: hook)
388
+ ```
389
+
390
+ Boolean findings must be actual `true` or `false` values. Invalid responses and
391
+ hook exceptions are recorded under `result[:ai_analysis][:error]`; the policy's
392
+ `ai_failure_mode` then allows the deterministic result, blocks, or raises. AI
393
+ findings can add a violation but cannot clear one.
394
+
395
+ The hook receives raw input. If it calls a third-party model, the caller is
396
+ responsible for data-residency, privacy, and vendor-trust requirements.
397
+ The context includes locally matched `policy_violated` and `policy_rules`
398
+ signals, but the analyzer result contract does not invent custom policy
399
+ matches. Organization rules in this release remain deterministic; applications
400
+ that require semantic topic policy should make that separate decision explicit
401
+ rather than relabeling it as injection, PII, or a secret.
402
+
403
+ ### OpenAI structured-output connector
404
+
405
+ Add the official SDK only in applications that use this connector:
406
+
407
+ ```ruby
408
+ gem "openai", "~> 0.62"
409
+ ```
410
+
411
+ ```ruby
412
+ require "openai"
413
+ require "olyx/guardrails"
414
+ require "olyx/guardrails/integrations/openai_analyzer"
415
+
416
+ openai = OpenAI::Client.new(api_key: ENV.fetch("OPENAI_API_KEY"))
417
+
418
+ analyzer = Olyx::Guardrails::Integrations::OpenAIAnalyzer.new(
419
+ client: openai,
420
+ model: ENV.fetch("OPENAI_MODEL"),
421
+ request_options: { timeout: 15, max_retries: 1 }
422
+ )
423
+
424
+ result = Olyx::Guardrails.check(input, ai_analyzer: analyzer)
425
+ ```
426
+
427
+ The connector sends the built-in
428
+ `OpenAIAnalyzer::AnalysisSchema < OpenAI::BaseModel` through the Responses API
429
+ and consumes the SDK's parsed schema-model object. Requests default to
430
+ `store: false`; refusals, API failures, and missing parsed output are bounded
431
+ under `result[:ai_analysis][:error]` without clearing local findings.
432
+
433
+ A custom OpenAI schema model can be supplied with `schema:`. It should expose
434
+ the analyzer fields shown above; unknown fields are discarded by
435
+ `Guardrails.check`.
436
+
437
+ #### Model compatibility and minimum
438
+
439
+ The connector has no model allowlist or baked-in default. It forwards String or
440
+ Symbol model identifiers unchanged, so aliases, dated snapshots, and fine-tuned
441
+ model IDs work when the selected model supports both:
442
+
443
+ 1. text output through `v1/responses`; and
444
+ 2. Structured Outputs.
445
+
446
+ That capability check—not a particular model name—is the technical minimum.
447
+ The API returns an error under `result[:ai_analysis][:error]` when the selected
448
+ model cannot satisfy it. Verify both features on the
449
+ [OpenAI model catalog](https://developers.openai.com/api/docs/models) because
450
+ model availability and capabilities change independently of this gem.
451
+
452
+ Do not use models whose catalog entry says **Structured outputs: Not
453
+ supported**. This excludes GPT-3.5 Turbo, GPT-4, GPT-4 Turbo, Realtime models,
454
+ and specialized image, video, audio, transcription, speech, embedding, and
455
+ moderation models. JSON mode is not used as a fallback because valid JSON alone
456
+ does not guarantee this connector's schema.
457
+
458
+ For the broadest model portability, leave `response_options: {}`. Parameters
459
+ such as `temperature`, reasoning effort, verbosity, and tool configuration are
460
+ model-specific and can make an otherwise compatible model reject the request.
461
+ Custom and fine-tuned models remain compatible only when their effective model
462
+ supports Structured Outputs; custom schemas must also stay within that model's
463
+ supported JSON Schema subset.
464
+
465
+ Mini and nano text models that support Structured Outputs are technically
466
+ compatible; model size is not rejected in code. However, schema conformance
467
+ only guarantees the output shape, not correct security classification.
468
+ Therefore, do not establish a production minimum from cost, latency, or a
469
+ successful schema response alone. Require representative adversarial
470
+ evaluations for false negatives, false positives, refusals, latency, and cost.
471
+ Keep the local deterministic findings authoritative. OpenAI's
472
+ [Structured Outputs guide](https://developers.openai.com/api/docs/guides/structured-outputs)
473
+ also notes that structured responses can still contain mistakes.
474
+
475
+ The gem and its optional OpenAI connector require Ruby 3.4 or newer.
476
+
477
+ ## Component APIs
478
+
479
+ ### PII
480
+
481
+ ```ruby
482
+ PiiScrubber.scrub(text)
483
+ PiiScrubber.scrub_messages(messages)
484
+ PiiScrubber.scrub_messages_with_detection(messages)
485
+ ```
486
+
487
+ String message content and array-style text content blocks are supported
488
+ without changing symbol/string key style.
489
+
490
+ ### Injection detection
491
+
492
+ ```ruby
493
+ InjectionDetector.scan(messages)
494
+ InjectionDetector.injection?(text)
495
+ ```
496
+
497
+ Multi-turn patterns require an adjacent `user` → `assistant` role transition.
498
+
499
+ ### Secret handling
500
+
501
+ The operation is explicit:
502
+
503
+ ```ruby
504
+ SecretScanner.scan(text, custom_patterns: []) # detect only
505
+ SecretScanner.redact(text, custom_patterns: []) # transform
506
+ SecretScanner.scan!(text, custom_patterns: []) # raise Blocked on detection
507
+ ```
508
+
509
+ Invalid custom regexes raise `ArgumentError`. Custom regexes are trusted
510
+ configuration: review them for pathological backtracking before deployment.
511
+ This low-level option extends credential detection and reports matches as
512
+ secrets. Use `PolicyRule` for business restrictions so findings retain a name,
513
+ description, block decision, and distinct `policy` classification.
514
+
515
+ ## Risk score
516
+
517
+ The heuristic score is injection (`0.50`) + secret (`0.25`) + restricted-policy
518
+ match (`0.25`) + PII (`0.10`) + any blocked check (`0.15`), clamped to
519
+ `0.0..1.0`. It supports graduated responses; it is not a probability or
520
+ calibrated model confidence.
521
+
522
+ ## Notifications
523
+
524
+ ```ruby
525
+ notifier = Olyx::Guardrails::Notifier.new(
526
+ policy: POLICY,
527
+ handlers: {
528
+ audit_log: ->(event) { Rails.logger.warn(event.to_json) },
529
+ incident_queue: ->(event) { IncidentNotificationJob.perform_later(event) }
530
+ }
531
+ )
532
+
533
+ notifier.notify(
534
+ result,
535
+ input: input,
536
+ metadata: { user_id: current_user.id, endpoint: request.path }
537
+ )
538
+ ```
539
+
540
+ Pass the same policy used by `Guardrails.check`. Each named handler receives the
541
+ same frozen, vendor-neutral event. A handler can write to a logger, enqueue a
542
+ job, send email, publish to a message bus, or invoke any incident/webhook SDK.
543
+ Handlers run synchronously and failures are isolated; enqueue work when delivery
544
+ must be asynchronous.
545
+
546
+ Input previews, AI reasons, metadata keys, metadata values, and handler errors
547
+ are bounded and redacted with the policy's restricted-content rules, custom
548
+ credential patterns, and the built-in PII and secret detectors.
549
+
550
+ ## Limitations
551
+
552
+ - Pattern-based injection detection normalizes NFKC text, common Greek/Cyrillic
553
+ homoglyphs, zero-width characters, and one bounded layer of URL, HTML-entity,
554
+ Unicode-escape, or Base64 encoding. It can still miss paraphrasing,
555
+ translation, nested/stacked encoding beyond one layer, unsupported
556
+ homoglyphs, and new attack techniques.
557
+ - Secret coverage includes common cloud/SaaS tokens, JWTs, PEM private keys,
558
+ credential-bearing database URLs, and private endpoints, but remains
559
+ format-based and cannot reliably classify arbitrary high-entropy strings or
560
+ every provider-specific credential. Coverage is intentionally not
561
+ exhaustive — additional vendor token formats are a good first
562
+ contribution; see [CONTRIBUTING.md](CONTRIBUTING.md).
563
+ - PII patterns remain biased toward common US/Western formats (plus
564
+ Luhn-valid Canadian SIN). Broader international coverage is a good fit for
565
+ a community PR rather than something this library tries to front-load.
566
+ - Restricted-content policies are pattern-based. They do not infer semantic
567
+ equivalents, translations, or obfuscated terms unless explicitly configured.
568
+ - Completed-output APIs do not inspect streaming tokens before they reach the
569
+ caller. Upload checks require caller-supplied text extraction.
570
+ - In-process decisions, notifications, and metrics are local to one Ruby
571
+ process. The gem does not provide cross-service policy distribution, global
572
+ quotas, centralized audit retention, managed retries, or fleet dashboards.
573
+ - Redaction addresses recognized data patterns; it is not a replacement for
574
+ authorization or a complete data-loss-prevention system.
575
+
576
+ ## Gem and Olyx Cloud boundary
577
+
578
+ The gem is intended to be useful on its own: deterministic checks, custom
579
+ policies, completed input/output enforcement, Rails boundary adapters, safe
580
+ local telemetry, and caller-owned notification handlers all run in the
581
+ application process.
582
+
583
+ Olyx Cloud is the operational control plane for teams that outgrow local
584
+ enforcement: a full LLM proxy route, centralized and versioned policy rollout,
585
+ cross-service audit and retention, distributed quotas, managed provider routing
586
+ and retries, streaming enforcement, fleet analytics, and managed asynchronous
587
+ delivery. Those distributed capabilities are intentionally not simulated by
588
+ the gem, so adopting the open-source library does not create a later migration
589
+ away from its public APIs.
590
+
591
+ ## Development and security
592
+
593
+ ```bash
594
+ bundle exec rake test
595
+ COVERAGE=true bundle exec rake test
596
+ bundle exec ruby script/flog_gate.rb
597
+ gem build olyx-guardrails.gemspec
598
+ ```
599
+
600
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development expectations and
601
+ [SECURITY.md](SECURITY.md) for private vulnerability reporting. Everyone
602
+ interacting in this project's codebase, issue tracker, and other spaces is
603
+ expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md).
604
+
605
+ ## Changelog
606
+
607
+ See [CHANGELOG.md](CHANGELOG.md) for release history.
608
+
609
+ ## License
610
+
611
+ Apache-2.0. See [LICENSE](LICENSE).
data/SECURITY.md ADDED
@@ -0,0 +1,35 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ Security fixes are provided for the latest released minor version. Users should
6
+ upgrade to the latest patch release before reporting an issue.
7
+
8
+ | Version | Supported |
9
+ |---|---|
10
+ | 1.0.x | Yes |
11
+ | < 1.0 | No |
12
+
13
+ ## Reporting a vulnerability
14
+
15
+ Do not open a public issue for a suspected vulnerability.
16
+
17
+ Email `security@olyxai.io` with:
18
+
19
+ - the affected version and component;
20
+ - reproduction steps or a minimal proof of concept;
21
+ - expected and observed behavior;
22
+ - the potential confidentiality, integrity, or availability impact; and
23
+ - any suggested remediation or disclosure constraints.
24
+
25
+ You should receive an acknowledgement within three business days and an initial
26
+ assessment within seven business days. We will coordinate remediation and
27
+ disclosure with the reporter. Please avoid accessing data that is not yours,
28
+ degrading services, or publicly disclosing the issue before a fix is available.
29
+
30
+ ## Scope notes
31
+
32
+ Pattern-based guardrails are defense-in-depth controls, not complete semantic
33
+ security boundaries. Documented detection limitations alone are not
34
+ vulnerabilities, but bypasses of a documented invariant—especially redaction,
35
+ blocking, or third-party data-egress guarantees—are in scope.