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,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'handler'
4
+ require_relative 'handler_collection_validator'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module Notification
9
+ # Validates and freezes named notification handlers.
10
+ module HandlerCollection
11
+ MAXIMUM = 20
12
+
13
+ module_function
14
+
15
+ def call(handlers)
16
+ HandlerCollectionValidator.input!(handlers, maximum: MAXIMUM)
17
+
18
+ normalized = handlers.map { |name, handler| Handler.call(name, handler) }
19
+ HandlerCollectionValidator.unique!(normalized)
20
+ normalized.freeze
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Notification
6
+ # Validates handler collection shape and normalized-name uniqueness.
7
+ module HandlerCollectionValidator
8
+ module_function
9
+
10
+ def input!(handlers, maximum:)
11
+ return if handlers.is_a?(Hash) && !handlers.empty? && handlers.length <= maximum
12
+
13
+ raise ArgumentError, "handlers must be a non-empty Hash with at most #{maximum} entries"
14
+ end
15
+
16
+ def unique!(handlers)
17
+ names = handlers.map(&:first)
18
+ raise ArgumentError, 'handler names must be unique' unless names.uniq.length == names.length
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Notification
6
+ # Normalizes a redacted metadata key.
7
+ module Key
8
+ LENGTH = 50
9
+
10
+ module_function
11
+
12
+ def call(value, scrubber)
13
+ normalized = scrubber.call(value).gsub(/[^A-Za-z0-9_.-]/, '_')[0...LENGTH]
14
+ normalized.empty? ? 'metadata' : normalized
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Notification
6
+ # Bounds, sanitizes, and collision-proofs notification metadata.
7
+ module Metadata
8
+ MAX_ENTRIES = 20
9
+ KEY_LENGTH = 50
10
+
11
+ module_function
12
+
13
+ def call(metadata, sanitizer)
14
+ metadata.first(MAX_ENTRIES).each_with_object({}) do |(key, value), output|
15
+ safe_key = unique_key(sanitizer.key(key), output)
16
+ output[safe_key] = sanitizer.field(value)
17
+ end
18
+ end
19
+
20
+ def unique_key(base, output)
21
+ index = 1
22
+ candidate = base
23
+ while output.key?(candidate)
24
+ index += 1
25
+ suffix = "_#{index}"
26
+ candidate = "#{base[0...(KEY_LENGTH - suffix.length)]}#{suffix}"
27
+ end
28
+ candidate
29
+ end
30
+ private_class_method :unique_key
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Notification
6
+ # Constructs the notifier's validated policy and delivery dispatcher.
7
+ module NotifierSetup
8
+ module_function
9
+
10
+ def call(policy, handlers)
11
+ config = NotifierConfiguration.new(policy: policy, handlers: handlers)
12
+ configured_policy = config.policy
13
+ sanitizer = NotificationSanitizer.new(configured_policy)
14
+ [configured_policy, DeliveryDispatcher.new(config.handlers, sanitizer)]
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Notification
6
+ # Produces a bounded redacted preview with a truncation marker.
7
+ module Preview
8
+ LENGTH = 300
9
+
10
+ module_function
11
+
12
+ def call(value, scrubber)
13
+ source = value.to_s
14
+ scrubbed = scrubber.call(source)
15
+ preview = scrubbed[0...LENGTH]
16
+ preview += '…' if truncated?(source, scrubbed)
17
+ preview
18
+ end
19
+
20
+ def truncated?(source, scrubbed)
21
+ scrubbed.length > LENGTH || source.length > Scrubber::WINDOW
22
+ end
23
+ private_class_method :truncated?
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../policy_aware_redactor'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Notification
8
+ # Applies policy-aware redaction to a bounded notification value.
9
+ class Scrubber
10
+ WINDOW = 2_000
11
+
12
+ def initialize(policy)
13
+ @policy = policy
14
+ end
15
+
16
+ def call(value)
17
+ PolicyAwareRedactor.call(value.to_s[0...WINDOW], policy: @policy)[:text]
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../violation_labels'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Notification
8
+ # Derives stable machine-readable violation labels from a decision.
9
+ module ViolationLabels
10
+ module_function
11
+
12
+ def call(result)
13
+ labels = Guardrails::ViolationLabels.call(result)
14
+ labels.empty? ? ['policy_violation'] : labels
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'notification/deep_freezer'
4
+ require_relative 'notification/base_event'
5
+ require_relative 'notification/metadata'
6
+ require_relative 'notification/violation_labels'
7
+ require_relative 'notification_sanitizer'
8
+
9
+ module Olyx
10
+ # Vendor-neutral AI safety checks and notification events.
11
+ module Guardrails
12
+ # Composes the bounded, immutable, vendor-neutral notification event.
13
+ class NotificationEventBuilder
14
+ SCHEMA_VERSION = 1
15
+
16
+ def self.call(result, input:, metadata:, policy:)
17
+ new(result, input, metadata, policy).call
18
+ end
19
+
20
+ def initialize(result, input, metadata, policy)
21
+ @result = result
22
+ @input = input
23
+ @metadata = metadata.is_a?(Hash) ? metadata : {}
24
+ @policy = policy
25
+ @sanitizer = NotificationSanitizer.new(policy)
26
+ end
27
+
28
+ def call
29
+ event = Notification::BaseEvent.call(
30
+ @result, metadata: @metadata, policy: @policy, sanitizer: @sanitizer, schema_version: SCHEMA_VERSION
31
+ )
32
+ reason = @result.dig(:ai_analysis, :reason)
33
+ event[:ai_reason] = @sanitizer.field(reason) if reason
34
+ event[:input_preview] = @sanitizer.preview(@input) unless @input.nil?
35
+ Notification::DeepFreezer.call(event)
36
+ end
37
+ end
38
+
39
+ private_constant :NotificationEventBuilder
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'notification/key'
4
+ require_relative 'notification/preview'
5
+ require_relative 'notification/scrubber'
6
+
7
+ module Olyx
8
+ # Guardrail policy types and evaluation services.
9
+ module Guardrails
10
+ # Applies the configured confidentiality policy to notification text.
11
+ class NotificationSanitizer
12
+ FIELD_LENGTH = 300
13
+
14
+ def initialize(policy)
15
+ @scrubber = Notification::Scrubber.new(policy)
16
+ end
17
+
18
+ def field(value, max_length: FIELD_LENGTH)
19
+ @scrubber.call(value).gsub(/[\r\n\t]+/, ' ')[0...max_length]
20
+ end
21
+
22
+ def preview(value)
23
+ Notification::Preview.call(value, @scrubber)
24
+ end
25
+
26
+ def key(value)
27
+ Notification::Key.call(value, @scrubber)
28
+ end
29
+ end
30
+
31
+ private_constant :NotificationSanitizer
32
+ end
33
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'notification_event_builder'
4
+ require_relative 'notification/delivery_dispatcher'
5
+ require_relative 'notification/delivery_summary'
6
+ require_relative 'notification/notifier_setup'
7
+ require_relative 'notification_sanitizer'
8
+ require_relative 'notifier_configuration'
9
+
10
+ module Olyx
11
+ # Guardrail policy types and evaluation services.
12
+ module Guardrails
13
+ # Dispatches one sanitized guardrail event to named application callables.
14
+ # Handlers run synchronously and independently; applications can enqueue
15
+ # asynchronous work from a handler when required.
16
+ class Notifier
17
+ # @param policy [Policy] the policy used to produce and sanitize results.
18
+ # @param handlers [Hash<String, #call>, Hash<Symbol, #call>] one to 20
19
+ # named callables accepting the immutable event Hash.
20
+ # @raise [ArgumentError] when policy or handler configuration is invalid.
21
+ def initialize(policy:, handlers:)
22
+ @policy, @dispatcher = Notification::NotifierSetup.call(policy, handlers)
23
+ end
24
+
25
+ # Dispatches a sanitized event when the result has non-zero risk.
26
+ # Handler and event-building failures are returned and never raised.
27
+ #
28
+ # @param result [Hash] a result from {Guardrails.check}.
29
+ # @param input [#to_s, nil] original input for a redacted preview.
30
+ # @param metadata [Hash] caller context, capped and policy-redacted.
31
+ # @return [Hash, nil] delivery summary, or nil for zero-risk results.
32
+ def notify(result, input: nil, metadata: {})
33
+ return nil unless deliverable?(result)
34
+
35
+ event = build_event(result, input, metadata)
36
+ deliveries = @dispatcher.call(event)
37
+ Notification::DeliverySummary.call(event, deliveries)
38
+ rescue StandardError => error
39
+ @dispatcher.failure(error)
40
+ end
41
+
42
+ private
43
+
44
+ def build_event(result, input, metadata)
45
+ NotificationEventBuilder.call(result, input: input, metadata: metadata, policy: @policy)
46
+ end
47
+
48
+ def deliverable?(result)
49
+ result.is_a?(Hash) && result[:risk_score].to_f.positive?
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'policy'
4
+ require_relative 'notification/handler_collection'
5
+
6
+ module Olyx
7
+ # Guardrail policy types and evaluation services.
8
+ module Guardrails
9
+ # Validates and freezes the generic notifier's policy and named handlers.
10
+ class NotifierConfiguration
11
+ attr_reader :policy, :handlers
12
+
13
+ def initialize(policy:, handlers:)
14
+ @policy = validate_policy(policy)
15
+ @handlers = Notification::HandlerCollection.call(handlers)
16
+ freeze
17
+ end
18
+
19
+ private
20
+
21
+ def validate_policy(policy)
22
+ return policy if policy.is_a?(Policy)
23
+
24
+ raise ArgumentError, 'policy must be an Olyx::Guardrails::Policy'
25
+ end
26
+ end
27
+
28
+ private_constant :NotifierConfiguration
29
+ end
30
+ end
@@ -0,0 +1,32 @@
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
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'hash_key'
4
+ require_relative 'text_scrubber'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module Pii
9
+ # Redacts a text content block while preserving its Hash shape.
10
+ module BlockScrubber
11
+ module_function
12
+
13
+ def call(block)
14
+ return [block, false] unless block.is_a?(Hash)
15
+
16
+ key = HashKey.call(block, 'text')
17
+ text = block[key] if key
18
+ return [block, false] unless text.is_a?(String)
19
+
20
+ result(block, key, text, TextScrubber.call(text))
21
+ end
22
+
23
+ def result(block, key, text, redacted)
24
+ changed = redacted != text
25
+ [changed ? block.merge(key => redacted) : block, changed]
26
+ end
27
+ private_class_method :result
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'block_scrubber'
4
+ require_relative 'text_scrubber'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module Pii
9
+ # Redacts String or array-style message content.
10
+ module ContentScrubber
11
+ module_function
12
+
13
+ def call(content)
14
+ return scrub_text(content) if content.is_a?(String)
15
+ return scrub_blocks(content) if content.is_a?(Array)
16
+
17
+ [content, false]
18
+ end
19
+
20
+ def scrub_text(text)
21
+ redacted = TextScrubber.call(text)
22
+ [redacted, redacted != text]
23
+ end
24
+
25
+ def scrub_blocks(blocks)
26
+ results = blocks.map { |block| BlockScrubber.call(block) }
27
+ [results.map(&:first), results.any?(&:last)]
28
+ end
29
+ private_class_method :scrub_text, :scrub_blocks
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require_relative 'named_date_validator'
5
+ require_relative 'numeric_date_validator'
6
+
7
+ module Olyx
8
+ module Guardrails
9
+ module Pii
10
+ # Validates labeled date-of-birth candidates as real calendar dates.
11
+ module DateValidator
12
+ DATE = %r{(?:\d{1,4}[/-]\d{1,2}[/-]\d{1,4}|[A-Za-z]+\.?\s+\d{1,2},?\s+\d{4})}
13
+
14
+ module_function
15
+
16
+ def call(value)
17
+ candidate = value[DATE]
18
+ return false unless candidate
19
+
20
+ candidate.match?(/\A\d/) ? NumericDateValidator.call(candidate) : NamedDateValidator.call(candidate)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Pii
6
+ # Calculates ISO 13616's streaming mod-97 remainder.
7
+ module IbanRemainder
8
+ module_function
9
+
10
+ def call(value)
11
+ value.each_char.reduce(0) { |current, character| append(current, digits(character)) }
12
+ end
13
+
14
+ def append(current, characters)
15
+ characters.each_char.reduce(current) { |memo, digit| ((memo * 10) + digit.to_i) % 97 }
16
+ end
17
+
18
+ def digits(character)
19
+ character.match?(/[A-Z]/) ? (character.ord - 55).to_s : character
20
+ end
21
+ private_class_method :append, :digits
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'iban_remainder'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Pii
8
+ # Applies ISO 13616 mod-97 validation to IBAN candidates.
9
+ module IbanValidator
10
+ FORMAT = /\A[A-Z]{2}\d{2}[A-Z0-9]{11,30}\z/
11
+
12
+ module_function
13
+
14
+ def call(iban)
15
+ compact = compact(iban)
16
+ return false unless valid_format?(compact)
17
+
18
+ valid_checksum?(compact)
19
+ end
20
+
21
+ def compact(iban) = iban.delete(' ').upcase
22
+
23
+ def valid_format?(compact)
24
+ compact.length.between?(15, 34) && compact.match?(FORMAT)
25
+ end
26
+
27
+ def valid_checksum?(compact)
28
+ IbanRemainder.call(compact[4..] + compact[0, 4]) == 1
29
+ end
30
+ private_class_method :compact, :valid_checksum?, :valid_format?
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Pii
6
+ # Validates the structure and range of every IPv4 octet.
7
+ module Ipv4Validator
8
+ module_function
9
+
10
+ def call(address)
11
+ octets = address.split('.')
12
+ octets.length == 4 && octets.all? { |octet| octet.to_i.between?(0, 255) }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ipaddr'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Pii
8
+ # Validates IPv6 candidates with Ruby's address parser.
9
+ module Ipv6Validator
10
+ module_function
11
+
12
+ def call(address)
13
+ IPAddr.new(address).ipv6?
14
+ rescue IPAddr::InvalidAddressError
15
+ false
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Pii
6
+ # Shared Luhn digit-parsing and checksum validity, reused by every
7
+ # format that applies the Luhn algorithm at its own fixed or minimum
8
+ # length (cards, Canadian SIN) so that length rule is the only thing
9
+ # each caller needs to own.
10
+ module LuhnChecksum
11
+ module_function
12
+
13
+ def digits(number) = number.gsub(/\D/, '').chars.map(&:to_i)
14
+
15
+ def valid?(digits) = (checksum(digits) % 10).zero?
16
+
17
+ def checksum(digits)
18
+ digits.reverse.each_with_index.sum { |digit, index| index.even? ? digit : doubled(digit) }
19
+ end
20
+
21
+ def doubled(digit)
22
+ value = digit * 2
23
+ value > 9 ? value - 9 : value
24
+ end
25
+ private_class_method :doubled
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'luhn_checksum'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Pii
8
+ # Validates card-like digit sequences with the Luhn checksum.
9
+ module LuhnValidator
10
+ MINIMUM_LENGTH = 13
11
+
12
+ module_function
13
+
14
+ def call(number)
15
+ digits = LuhnChecksum.digits(number)
16
+ digits.length >= MINIMUM_LENGTH && LuhnChecksum.valid?(digits)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end