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
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copy the relevant sections into a Rails application. Nothing is scanned
4
+ # globally: each helper is called at an application-owned AI boundary.
5
+
6
+ require 'olyx/guardrails/rails'
7
+
8
+ # config/initializers/olyx_guardrails.rb
9
+ Olyx::Guardrails::Rails.configure do |config|
10
+ config.enabled = true
11
+ config.policy_path = Rails.root.join('config/olyx_guardrails.yml')
12
+ config.ai_analyzer = nil
13
+ config.notifier_handlers = {
14
+ audit: ->(event) { Rails.logger.warn(event.inspect) }
15
+ }
16
+ config.filter_parameters = %i[prompt system_prompt ai_input llm_input]
17
+ end
18
+
19
+ # Controller input, redaction, and completed-output enforcement.
20
+ class AiRequestsController < ApplicationController
21
+ include Olyx::Guardrails::Rails::Controller
22
+
23
+ rescue_from Olyx::Guardrails::Blocked do |error|
24
+ render json: { error: 'input_rejected', decision: error.decision },
25
+ status: :unprocessable_entity
26
+ end
27
+
28
+ def create
29
+ prompt = params.require(:prompt)
30
+ guardrails_check!(prompt, metadata: { account_id: current_account.id })
31
+ safe_prompt = guardrails_redact(prompt)[:text]
32
+ completion = LlmClient.complete(safe_prompt)
33
+ Olyx::Guardrails::Rails::Enforcer.check_output!(completion)
34
+ render json: { completion: completion }, status: :created
35
+ end
36
+ end
37
+
38
+ # GraphQL resolver or mutation.
39
+ class CompletionResolver
40
+ include Olyx::Guardrails::Rails::GraphQL
41
+
42
+ def resolve(prompt:)
43
+ guardrails_check_graphql!(prompt)
44
+ completion = LlmClient.complete(prompt)
45
+ guardrails_check_graphql_output!(completion)
46
+ { completion: completion }
47
+ end
48
+ end
49
+
50
+ # Action Cable channel.
51
+ class PromptChannel < ApplicationCable::Channel
52
+ include Olyx::Guardrails::Rails::ActionCable
53
+
54
+ def receive(data)
55
+ guardrails_check_cable!(data.fetch('prompt'))
56
+ transmit(status: 'accepted')
57
+ end
58
+ end
59
+
60
+ # Active Job arguments are checked immediately before perform.
61
+ class CompletionJob < ApplicationJob
62
+ include Olyx::Guardrails::Rails::Job
63
+
64
+ guardrails_input_arguments 0, :system_prompt
65
+
66
+ def perform(prompt, system_prompt:)
67
+ LlmClient.complete(prompt, system_prompt: system_prompt)
68
+ end
69
+ end
70
+
71
+ # Active Model validation for an explicitly declared AI-bound attribute.
72
+ class PromptDraft
73
+ include ActiveModel::Model
74
+
75
+ attr_accessor :prompt
76
+
77
+ validates :prompt, olyx_guardrails: true
78
+ end
79
+
80
+ # Caller-owned upload parsing. The extractor must return a String.
81
+ class UploadedPromptChecker
82
+ def self.call(upload)
83
+ Olyx::Guardrails::Rails::Upload.check!(
84
+ upload,
85
+ extractor: ->(file) { DocumentTextExtractor.call(file.tempfile) }
86
+ )
87
+ end
88
+ end
89
+
90
+ # Service objects, callbacks, and custom transports use the reusable enforcer.
91
+ class ConversationService
92
+ def self.call(messages)
93
+ Olyx::Guardrails::Rails::Enforcer.check_messages!(
94
+ messages,
95
+ metadata: { boundary: 'conversation_service' }
96
+ )
97
+ LlmClient.chat(messages)
98
+ end
99
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Run with:
4
+ # ruby -Ilib examples/ruby_only.rb "Summarize these release notes"
5
+ #
6
+ # This path has no Rails dependency and keeps policy, enforcement, model calls,
7
+ # and notification delivery under the application's control.
8
+
9
+ require 'olyx/guardrails'
10
+
11
+ policy = Olyx::Guardrails::Policy.new(
12
+ name: 'ruby-application',
13
+ max_input_length: 4_000,
14
+ block_pii: true,
15
+ block_injections: true,
16
+ block_secrets: true,
17
+ ai_failure_mode: :block,
18
+ secret_patterns: ['company-token-[a-z0-9]{24}'],
19
+ rules: [
20
+ {
21
+ name: :confidential_project,
22
+ terms: ['Project Falcon'],
23
+ match: :whole_word,
24
+ replacement: '[CONFIDENTIAL_PROJECT]'
25
+ }
26
+ ]
27
+ )
28
+
29
+ notifier = Olyx::Guardrails::Notifier.new(
30
+ policy: policy,
31
+ handlers: { stderr: ->(event) { warn("guardrail event: #{event.inspect}") } }
32
+ )
33
+
34
+ prompt = ARGV.fetch(0, 'Summarize these public release notes')
35
+ decision = Olyx::Guardrails.check(prompt, policy: policy)
36
+ notifier.notify(decision, input: prompt, metadata: { boundary: 'model_input' })
37
+ unless decision[:allowed]
38
+ rejected = decision[:checks].reject { |check| check[:allowed] }
39
+ rejected_types = rejected.map { |check| check[:type] }
40
+ abort "Input rejected: #{rejected_types.join(', ')}"
41
+ end
42
+
43
+ safe_prompt = Olyx::Guardrails.redact(prompt, policy: policy)[:text]
44
+
45
+ # Replace this lambda with the application's model client.
46
+ model = ->(input) { "Completed response for: #{input}" }
47
+ completion = model.call(safe_prompt)
48
+
49
+ output_decision = Olyx::Guardrails.check_output(completion, policy: policy)
50
+ notifier.notify(output_decision, input: completion, metadata: { boundary: 'model_output' })
51
+ abort 'Output rejected' unless output_decision[:allowed]
52
+
53
+ puts Olyx::Guardrails.redact_output(completion, policy: policy)[:text]
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/base'
4
+
5
+ module OlyxGuardrails
6
+ module Generators
7
+ # Installs explicit, environment-specific Rails policy configuration.
8
+ class InstallGenerator < ::Rails::Generators::Base
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ desc 'Install Olyx Guardrails Rails configuration and policies'
12
+
13
+ def copy_initializer
14
+ template 'initializer.rb.tt', 'config/initializers/olyx_guardrails.rb'
15
+ end
16
+
17
+ def copy_policy
18
+ template 'policy.yml.tt', 'config/olyx_guardrails.yml'
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ Olyx::Guardrails::Rails.configure do |config|
4
+ config.enabled = true
5
+ config.policy_path = Rails.root.join("config/olyx_guardrails.yml")
6
+
7
+ # Optional semantic analyzer. Keep deterministic findings authoritative.
8
+ config.ai_analyzer = nil
9
+
10
+ # Handlers receive sanitized, immutable events. Keep this empty when
11
+ # ActiveSupport::Notifications is sufficient for your application.
12
+ config.notifier_handlers = {}
13
+
14
+ # Prevent common AI input parameter names from appearing in Rails logs.
15
+ config.filter_parameters = %i[prompt system_prompt ai_input llm_input]
16
+ end
@@ -0,0 +1,41 @@
1
+ development:
2
+ name: development
3
+ max_input_length: 10000
4
+ block_pii: false
5
+ block_injections: true
6
+ block_secrets: false
7
+ ai_failure_mode: allow
8
+ secret_patterns: []
9
+ rules: []
10
+
11
+ test:
12
+ name: test
13
+ max_input_length: 10000
14
+ block_pii: true
15
+ block_injections: true
16
+ block_secrets: true
17
+ ai_failure_mode: block
18
+ secret_patterns: []
19
+ rules: []
20
+
21
+ production:
22
+ name: production
23
+ max_input_length: 10000
24
+ block_pii: true
25
+ block_injections: true
26
+ block_secrets: true
27
+ ai_failure_mode: block
28
+ secret_patterns: []
29
+ # String values are case-insensitive regular-expression source.
30
+ # - 'company-token-[a-z0-9]{24}'
31
+ rules: []
32
+ # Add organization restrictions explicitly. Example:
33
+ # - name: confidential_projects
34
+ # description: Internal project names must not enter AI requests
35
+ # terms:
36
+ # - 'Project Falcon'
37
+ # match: whole_word
38
+ # patterns:
39
+ # - '\bPF-[0-9]{4}\b'
40
+ # block: true
41
+ # replacement: '[CONFIDENTIAL_PROJECT]'
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Ai
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
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Ai
6
+ # Applies normalization, selection, Boolean validation, and reason bounds.
7
+ module AnalysisPipeline
8
+ INVALID_SHAPE_ERROR = 'ai_analyzer must return a Hash or a schema model with deep_to_h/to_h'
9
+
10
+ module_function
11
+
12
+ def call(value)
13
+ result = AnalysisNormalizer.call(value)
14
+ return { error: INVALID_SHAPE_ERROR } unless result
15
+
16
+ sanitized = ResultSanitizer.call(result)
17
+ invalid_key = BooleanValidator.invalid_key(sanitized)
18
+ return { error: "ai_analyzer #{invalid_key} must be true or false" } if invalid_key
19
+
20
+ sanitized[:reason] = sanitized[:reason].to_s[0...500] if sanitized.key?(:reason)
21
+ sanitized
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Ai
6
+ # Finds the first analyzer 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
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Ai
6
+ # Merges one semantic Boolean finding into its deterministic check.
7
+ module FlagFindingMerger
8
+ module_function
9
+
10
+ def call(check, flagged, changes)
11
+ flagged ? check.merge(**changes, ai_flagged: true) : check
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Ai
6
+ # Selects the bounded analyzer 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
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Ai
6
+ # Merges the semantic secret flag into a deterministic secret check.
7
+ module SecretFindingMerger
8
+ module_function
9
+
10
+ def call(check, analysis, policy)
11
+ return check unless analysis[:secret_leaked]
12
+
13
+ count = [check[:count].to_i, 1].max
14
+ check.merge(leaked: true, allowed: !policy.block_secrets?, count: count, ai_flagged: true)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Ai
6
+ # Merges semantic PII and injection flags using declarative mappings.
7
+ module StandardFindingMerger
8
+ SETTINGS = {
9
+ pii: %i[pii_detected detected block_pii?],
10
+ injection: %i[injection_attempt injection_attempt block_injections?]
11
+ }.freeze
12
+
13
+ module_function
14
+
15
+ def call(checks, analysis, policy)
16
+ SETTINGS.to_h do |check_name, (finding_name, result_name, blocking_query)|
17
+ changes = { result_name => true, allowed: !policy.public_send(blocking_query) }
18
+ [check_name, FlagFindingMerger.call(checks[check_name], analysis[finding_name], changes)]
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ai/analysis_normalizer'
4
+ require_relative 'ai/analysis_pipeline'
5
+ require_relative 'ai/boolean_validator'
6
+ require_relative 'ai/result_sanitizer'
7
+
8
+ module Olyx
9
+ module Guardrails
10
+ # Normalizes and bounds responses from an untrusted optional AI analyzer.
11
+ class AiAnalysis
12
+ def self.call(analyzer, text, context)
13
+ new(analyzer, text, context).call
14
+ end
15
+
16
+ def initialize(analyzer, text, context)
17
+ @analyzer = analyzer
18
+ @text = text
19
+ @context = context
20
+ end
21
+
22
+ def call
23
+ Ai::AnalysisPipeline.call(@analyzer.call(@text, @context))
24
+ rescue StandardError => error
25
+ { error: error.message.to_s[0..200] }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ # Builds the bounded deterministic context passed to semantic analyzers.
6
+ module AiContextBuilder
7
+ module_function
8
+
9
+ def call(checks)
10
+ detection(checks).merge(policy(checks[:policy]))
11
+ end
12
+
13
+ def detection(checks)
14
+ injection = checks[:injection]
15
+ {
16
+ pii_detected: checks[:pii][:detected],
17
+ injection_attempt: injection[:injection_attempt],
18
+ injection_patterns: injection[:patterns],
19
+ secret_leaked: checks[:secret][:leaked]
20
+ }
21
+ end
22
+
23
+ def policy(check)
24
+ {
25
+ policy_violated: check[:violated],
26
+ policy_rules: check[:findings].map { |finding| finding[:rule] }
27
+ }
28
+ end
29
+ private_class_method :detection, :policy
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ # Applies the policy-selected behavior for an analyzer failure.
6
+ module AiFailureHandler
7
+ module_function
8
+
9
+ def call(checks, analysis, mode)
10
+ return checks if mode == :allow
11
+ raise AiAnalyzerError, analysis[:error] if mode == :raise
12
+
13
+ checks.merge(ai: { type: 'ai', allowed: false, error: true })
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ai/secret_finding_merger'
4
+ require_relative 'ai/flag_finding_merger'
5
+ require_relative 'ai/standard_finding_merger'
6
+
7
+ module Olyx
8
+ module Guardrails
9
+ # Unions semantic findings into deterministic checks without clearing any.
10
+ class AiFindingMerger
11
+ def self.call(checks, analysis, policy)
12
+ new(checks, analysis, policy).call
13
+ end
14
+
15
+ def initialize(checks, analysis, policy)
16
+ @checks = checks
17
+ @analysis = analysis
18
+ @policy = policy
19
+ end
20
+
21
+ def call
22
+ @checks.merge(
23
+ Ai::StandardFindingMerger.call(@checks, @analysis, @policy).merge(
24
+ secret: Ai::SecretFindingMerger.call(@checks[:secret], @analysis, @policy)
25
+ )
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ai_failure_handler'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ # Runs optional semantic analysis and merges valid findings.
8
+ module CheckAnalyzer
9
+ module_function
10
+
11
+ def call(checks, analyzer:, source:, policy:)
12
+ return [checks, nil] unless analyzer && checks[:length][:allowed]
13
+
14
+ analysis = AiAnalysis.call(analyzer, source, AiContextBuilder.call(checks))
15
+ [merge(checks, analysis, policy), analysis]
16
+ end
17
+
18
+ def merge(checks, analysis, policy)
19
+ return AiFailureHandler.call(checks, analysis, policy.ai_failure_mode) if analysis[:error]
20
+
21
+ AiFindingMerger.call(checks, analysis, policy)
22
+ end
23
+ private_class_method :merge
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ # Builds the stable deterministic portion of a check decision.
6
+ module CheckBaseResult
7
+ module_function
8
+
9
+ def call(checks, ordered, policy)
10
+ detection(checks, ordered).merge(policy(checks[:policy], policy))
11
+ end
12
+
13
+ def detection(checks, ordered)
14
+ {
15
+ allowed: ordered.all? { |check| check[:allowed] },
16
+ pii_detected: checks[:pii][:detected],
17
+ injection_attempt: checks[:injection][:injection_attempt],
18
+ secret_leaked: checks[:secret][:leaked]
19
+ }
20
+ end
21
+
22
+ def policy(check, configuration)
23
+ {
24
+ policy_name: configuration.name,
25
+ policy_violated: check[:violated],
26
+ policy_findings: check[:findings]
27
+ }
28
+ end
29
+ private_class_method :detection, :policy
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ # Applies optional analysis and builds the public decision contract.
6
+ module CheckPipeline
7
+ module_function
8
+
9
+ def call(source, checks, policy:, ai_analyzer:)
10
+ merged, analysis = CheckAnalyzer.call(
11
+ checks, analyzer: ai_analyzer, source: source, policy: policy
12
+ )
13
+ CheckResultBuilder.call(merged, analysis, policy)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'risk_scorer'
4
+ require_relative 'check_base_result'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ # Presents merged checks as the stable public decision contract.
9
+ module CheckResultBuilder
10
+ CHECK_ORDER = %i[pii injection secret policy length ai].freeze
11
+
12
+ module_function
13
+
14
+ def call(checks, analysis, policy)
15
+ ordered = CHECK_ORDER.filter_map { |type| checks[type] }
16
+ result = CheckBaseResult.call(checks, ordered, policy)
17
+ result[:risk_score] = RiskScorer.call(checks, ordered, analysis)
18
+ result[:checks] = ordered
19
+ result[:ai_analysis] = analysis if analysis
20
+ result
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ai_analysis'
4
+ require_relative 'ai_context_builder'
5
+ require_relative 'ai_finding_merger'
6
+ require_relative 'check_result_builder'
7
+ require_relative 'check_analyzer'
8
+ require_relative 'check_set'
9
+ require_relative 'check_pipeline'
10
+ require_relative 'policy'
11
+ require_relative 'validation'
12
+
13
+ module Olyx
14
+ module Guardrails
15
+ # Orchestrates deterministic checks and optional semantic enrichment.
16
+ class CheckRunner
17
+ def self.call(input, **)
18
+ new(input, **).call
19
+ end
20
+
21
+ def initialize(input, policy:, ai_analyzer:)
22
+ @source = input.to_s
23
+ @policy = policy
24
+ @ai_analyzer = ai_analyzer
25
+ validate_options!
26
+ end
27
+
28
+ def call
29
+ checks = CheckSet.call(@source, policy: @policy)
30
+ CheckPipeline.call(@source, checks, policy: @policy, ai_analyzer: @ai_analyzer)
31
+ end
32
+
33
+ private
34
+
35
+ def validate_options!
36
+ raise ArgumentError, 'policy must be an Olyx::Guardrails::Policy' unless @policy.is_a?(Policy)
37
+
38
+ Validation.callable_or_nil!(@ai_analyzer, name: 'ai_analyzer')
39
+ end
40
+ end
41
+ end
42
+ end