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,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../validation'
4
+ require_relative 'content_scrubber'
5
+ require_relative 'hash_key'
6
+
7
+ module Olyx
8
+ module Guardrails
9
+ module Pii
10
+ # Preserves chat message shape while redacting String content fields.
11
+ module MessageScrubber
12
+ module_function
13
+
14
+ def call(messages)
15
+ Validation.array_of!(messages, Hash, name: 'messages')
16
+ results = messages.map { |message| scrub_message(message) }
17
+ { messages: results.map(&:first), detected: results.any?(&:last) }
18
+ end
19
+
20
+ def scrub_message(message)
21
+ key = HashKey.call(message, 'content')
22
+ return [message, false] unless key
23
+
24
+ redacted, changed = ContentScrubber.call(message[key])
25
+ [changed ? message.merge(key => redacted) : message, changed]
26
+ end
27
+ private_class_method :scrub_message
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Pii
8
+ # Validates English abbreviated or full month dates.
9
+ module NamedDateValidator
10
+ FORMATS = ['%B %d, %Y', '%B %d %Y', '%b %d, %Y', '%b %d %Y'].freeze
11
+
12
+ module_function
13
+
14
+ def call(value)
15
+ FORMATS.any? { |format| valid?(value.delete('.'), format) }
16
+ end
17
+
18
+ def valid?(value, format)
19
+ Date.strptime(value, format)
20
+ true
21
+ rescue Date::Error
22
+ false
23
+ end
24
+ private_class_method :valid?
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ambiguous_numeric_date'
4
+ require_relative 'year_first_date'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module Pii
9
+ # Validates year-first or ambiguous day/month numeric dates.
10
+ module NumericDateValidator
11
+ PARTS = %r{\A(\d{1,4})[/-](\d{1,2})[/-](\d{1,4})\z}
12
+
13
+ module_function
14
+
15
+ def call(value)
16
+ parts = value.match(PARTS)&.captures
17
+ return false unless parts
18
+
19
+ return YearFirstDate.call(parts) if parts.first.length == 4
20
+
21
+ AmbiguousNumericDate.call(parts)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../pii_validators'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Pii
8
+ # Declarative PII pattern, replacement, and validator catalog.
9
+ module PatternCatalog
10
+ EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/
11
+ PHONE = /(?<!\d)(?:(?:\+|00)\d(?:[\s\-.]?\d){7,15}|\(?\d{3}\)?[\s\-.]\d{3}[\s\-.]\d{4})(?!\d)/
12
+ SSN = /\b\d{3}[- ]\d{2}[- ]\d{4}\b/
13
+ # Canadian Social Insurance Number: 9 digits, Luhn-checksummed like a
14
+ # card number, so (unlike SSN) it's not gated on a keyword — the
15
+ # checksum alone keeps false positives in line with CARD below.
16
+ SIN = /\b\d{3}[\s-]?\d{3}[\s-]?\d{3}\b/
17
+ CARD = /\b\d(?:[ -]?\d){12,18}\b/
18
+ IPV4 = /\b(?:\d{1,3}\.){3}\d{1,3}\b/
19
+ IPV6 = /(?<![A-Fa-f0-9:])(?:[A-Fa-f0-9]{0,4}:){2,7}[A-Fa-f0-9]{0,4}(?![A-Fa-f0-9:])/
20
+ TOKEN = /\b(?:Bearer\s+|sk-|ak_live_|fy-ent-)[A-Za-z0-9._-]{8,}\b/i
21
+ PASSPORT = /\b(?:passport(?:\s+(?:no|number|#))?[\s:]+)([A-Z]{1,2}\d{6,9})\b/i
22
+ IBAN = /\b(?:[A-Z]{2}\d{2}[A-Z0-9]{11,30}|[A-Z]{2}\d{2}(?: [A-Z0-9]{4}){2,7}(?: [A-Z0-9]{1,3})?)\b/
23
+ DOB = %r{\b(?:dob|date\s+of\s+birth|born\s+on|birthday)[\s:]+(?:\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|\d{4}[/-]\d{1,2}[/-]\d{1,2}|(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2},?\s+\d{4})}ix
24
+
25
+ ENTRIES = [
26
+ [EMAIL, '[EMAIL]'],
27
+ [SSN, '[SSN]', ->(value) { PiiValidators.ssn_valid?(value) }],
28
+ [SIN, '[SIN]', ->(value) { PiiValidators.sin_valid?(value) }],
29
+ [PASSPORT, '[PASSPORT]'],
30
+ [IBAN, '[IBAN]', ->(value) { PiiValidators.iban_valid?(value) }],
31
+ [DOB, '[DOB]', ->(value) { PiiValidators.date_valid?(value) }],
32
+ [IPV4, '[IP]', ->(value) { PiiValidators.ipv4_valid?(value) }],
33
+ [IPV6, '[IP]', ->(value) { PiiValidators.ipv6_valid?(value) }],
34
+ [TOKEN, '[TOKEN]'],
35
+ [CARD, '[CARD]', ->(value) { PiiValidators.luhn_valid?(value) }],
36
+ [PHONE, '[PHONE]']
37
+ ].freeze
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'luhn_checksum'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Pii
8
+ # Validates Canadian Social Insurance Numbers with the same Luhn
9
+ # checksum as card numbers, but at SIN's fixed 9-digit length —
10
+ # {LuhnValidator} enforces its own 13-digit floor for card use, so this
11
+ # applies SIN's length rule against the shared {LuhnChecksum} directly.
12
+ module SinValidator
13
+ LENGTH = 9
14
+
15
+ module_function
16
+
17
+ def call(sin)
18
+ digits = LuhnChecksum.digits(sin)
19
+ digits.length == LENGTH && LuhnChecksum.valid?(digits)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Pii
6
+ # Rejects structurally impossible U.S. Social Security numbers.
7
+ module SsnValidator
8
+ INVALID_AREA = /\A(?:000|666|9\d{2})\z/
9
+
10
+ module_function
11
+
12
+ def call(ssn)
13
+ area, group, serial = ssn.split(/[- ]/)
14
+ return false unless area && group && serial
15
+
16
+ !area.match?(INVALID_AREA) && group != '00' && serial != '0000'
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'pattern_catalog'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Pii
8
+ # Redacts recognized PII from one String.
9
+ module TextScrubber
10
+ module_function
11
+
12
+ def call(text)
13
+ return text unless text.is_a?(String)
14
+
15
+ PatternCatalog::ENTRIES.reduce(text) { |output, entry| replace(output, entry) }
16
+ end
17
+
18
+ def replace(text, entry)
19
+ pattern, replacement, validator = entry
20
+ return text.gsub(pattern, replacement) unless validator
21
+
22
+ text.gsub(pattern) { |match| validator.call(match) ? replacement : match }
23
+ end
24
+ private_class_method :replace
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Pii
8
+ # Validates numeric dates whose first component is a four-digit year.
9
+ module YearFirstDate
10
+ module_function
11
+
12
+ def call(parts)
13
+ Date.valid_date?(*parts.map(&:to_i))
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'pii/message_scrubber'
4
+ require_relative 'pii/text_scrubber'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ # Stable facade for free-text and chat-message PII redaction.
9
+ class PiiScrubber
10
+ def self.scrub(text)
11
+ Pii::TextScrubber.call(text)
12
+ end
13
+
14
+ def self.scrub_messages(messages)
15
+ scrub_messages_with_detection(messages)[:messages]
16
+ end
17
+
18
+ def self.scrub_messages_with_detection(messages)
19
+ Pii::MessageScrubber.call(messages)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'pii/iban_validator'
4
+ require_relative 'pii/date_validator'
5
+ require_relative 'pii/ipv4_validator'
6
+ require_relative 'pii/ipv6_validator'
7
+ require_relative 'pii/luhn_validator'
8
+ require_relative 'pii/ssn_validator'
9
+ require_relative 'pii/sin_validator'
10
+
11
+ module Olyx
12
+ module Guardrails
13
+ # Stable facade over format-specific PII validators.
14
+ module PiiValidators
15
+ module_function
16
+
17
+ def luhn_valid?(value) = Pii::LuhnValidator.call(value)
18
+ def ssn_valid?(value) = Pii::SsnValidator.call(value)
19
+ def ipv4_valid?(value) = Pii::Ipv4Validator.call(value)
20
+ def ipv6_valid?(value) = Pii::Ipv6Validator.call(value)
21
+ def iban_valid?(value) = Pii::IbanValidator.call(value)
22
+ def date_valid?(value) = Pii::DateValidator.call(value)
23
+ def sin_valid?(value) = Pii::SinValidator.call(value)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../enum_value'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module PolicyComponents
8
+ # Validates analyzer failure behavior.
9
+ AiFailureMode = EnumValue.new(
10
+ allowed: %i[allow block raise],
11
+ error: 'policy ai_failure_mode must be allow, block, or raise'
12
+ )
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../validation'
4
+ require_relative 'ai_failure_mode'
5
+ require_relative 'name'
6
+ require_relative 'rule_collection'
7
+ require_relative 'secret_pattern_collection'
8
+
9
+ module Olyx
10
+ module Guardrails
11
+ module PolicyComponents
12
+ # Validated policy identity and bounded input limit.
13
+ class IdentityConfiguration
14
+ attr_reader :name, :max_input_length
15
+
16
+ def initialize(name, max_input_length)
17
+ @name = Name.call(name)
18
+ @max_input_length = Validation.non_negative_integer!(max_input_length, name: 'policy max_input_length')
19
+ freeze
20
+ end
21
+ end
22
+
23
+ # Validated built-in blocking switches.
24
+ class BlockingConfiguration
25
+ def initialize(pii, injections, secrets)
26
+ @pii = Validation.boolean!(pii, name: 'policy block_pii')
27
+ @injections = Validation.boolean!(injections, name: 'policy block_injections')
28
+ @secrets = Validation.boolean!(secrets, name: 'policy block_secrets')
29
+ freeze
30
+ end
31
+
32
+ def pii? = @pii
33
+ def injections? = @injections
34
+ def secrets? = @secrets
35
+ end
36
+
37
+ # Validated analyzer, secret, and restricted-content configuration.
38
+ class RestrictionConfiguration
39
+ attr_reader :ai_failure_mode, :secret_patterns, :rules
40
+
41
+ def initialize(ai_failure_mode, secret_patterns, rules)
42
+ @ai_failure_mode = AiFailureMode.call(ai_failure_mode)
43
+ @secret_patterns = SecretPatternCollection.call(secret_patterns)
44
+ @rules = RuleCollection.call(rules)
45
+ freeze
46
+ end
47
+ end
48
+
49
+ # Composes independent immutable policy configuration sections.
50
+ class Configuration
51
+ attr_reader :identity, :blocking, :restrictions
52
+
53
+ def initialize(identity:, blocking:, restrictions:)
54
+ @identity = IdentityConfiguration.new(*identity)
55
+ @blocking = BlockingConfiguration.new(*blocking)
56
+ @restrictions = RestrictionConfiguration.new(*restrictions)
57
+ freeze
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module PolicyComponents
6
+ # Normalizes String/Symbol keyed policy configuration hashes.
7
+ module ConfigurationHash
8
+ module_function
9
+
10
+ def call(config)
11
+ raise ArgumentError, 'policy configuration must be a Hash' unless config.is_a?(Hash)
12
+
13
+ config.to_h { |key, value| [key.to_sym, value] }
14
+ rescue NoMethodError
15
+ raise ArgumentError, 'policy configuration keys must be Strings or Symbols'
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module PolicyComponents
6
+ # Defines policy finding identity and deterministic ordering.
7
+ module FindingOrder
8
+ module_function
9
+
10
+ def identity(finding)
11
+ [finding[:rule].name, finding[:start], finding[:end]]
12
+ end
13
+
14
+ def key(finding)
15
+ [finding[:start], finding[:end], finding[:rule_index]]
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module PolicyComponents
8
+ # Converts private policy matches to masked public findings.
9
+ module FindingPresenter
10
+ module_function
11
+
12
+ def call(findings)
13
+ findings.map { |finding| present(finding) }
14
+ end
15
+
16
+ def present(finding)
17
+ rule = finding[:rule]
18
+ identity(rule, finding).merge(location(finding)).compact
19
+ end
20
+
21
+ def identity(rule, finding)
22
+ {
23
+ rule: rule.name,
24
+ description: rule.description,
25
+ blocked: rule.block?,
26
+ matched: '[REDACTED]',
27
+ fingerprint: "sha256:#{Digest::SHA256.hexdigest(finding[:full])[0, 12]}"
28
+ }
29
+ end
30
+
31
+ def location(finding)
32
+ {
33
+ start: finding[:start],
34
+ end: finding[:end]
35
+ }
36
+ end
37
+ private_class_method :identity, :location, :present
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'finding_order'
4
+ require_relative 'rule_matcher'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module PolicyComponents
9
+ # Collects, deduplicates, and orders private policy matches.
10
+ module MatchCollector
11
+ module_function
12
+
13
+ def call(source, rules)
14
+ findings = rules.each_with_index.flat_map { |rule, index| RuleMatcher.call(source, rule, index) }
15
+ findings.uniq { |finding| FindingOrder.identity(finding) }.sort_by { |finding| FindingOrder.key(finding) }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module PolicyComponents
6
+ # Validates and freezes an operator-facing policy identifier.
7
+ module Name
8
+ module_function
9
+
10
+ def call(value)
11
+ return value.dup.freeze if valid?(value)
12
+
13
+ raise ArgumentError, 'policy name must be a String of 1..100 characters'
14
+ end
15
+
16
+ def valid?(value)
17
+ value.is_a?(String) && !value.strip.empty? && value.length <= 100
18
+ end
19
+ private_class_method :valid?
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../text/mapped_normalization'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module PolicyComponents
8
+ # Finds normalized policy matches and maps them to original offsets.
9
+ module NormalizedPatternMatcher
10
+ module_function
11
+
12
+ def call(source, rule, index, pattern)
13
+ mapped = Text::MappedNormalization.new(source)
14
+ return [] unless mapped.changed?
15
+
16
+ findings(source, mapped, rule, index, pattern)
17
+ end
18
+
19
+ def findings(source, mapped, rule, index, pattern)
20
+ mapped.text.to_enum(:scan, pattern).map do
21
+ match = Regexp.last_match
22
+ finding(source, mapped, rule, index, match)
23
+ end
24
+ end
25
+
26
+ def finding(source, mapped, rule, index, match)
27
+ starting, ending = mapped.original_span(match.begin(0), match.end(0))
28
+ { rule: rule, rule_index: index, full: source[starting...ending], start: starting, end: ending }
29
+ end
30
+ private_class_method :finding, :findings
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 PolicyComponents
6
+ # Enumerates private offsets for one compiled policy pattern.
7
+ module PatternMatcher
8
+ module_function
9
+
10
+ def call(source, rule, index, pattern)
11
+ source.to_enum(:scan, pattern).map do
12
+ match = Regexp.last_match
13
+ { rule: rule, rule_index: index, full: match[0].to_s, start: match.begin(0), end: match.end(0) }
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module PolicyComponents
6
+ # Presents policy matches and their redacted output.
7
+ module RedactionResult
8
+ module_function
9
+
10
+ def call(source, findings, transform)
11
+ {
12
+ text: PolicyRedactor.call(source, findings, transform: transform),
13
+ violated: findings.any?,
14
+ findings: FindingPresenter.call(findings)
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module PolicyComponents
6
+ # Merges overlapping policy findings while preserving the first rule.
7
+ module RedactionSpans
8
+ module_function
9
+
10
+ def call(findings)
11
+ findings.each_with_object([]) { |finding, spans| append(spans, finding) }
12
+ end
13
+
14
+ def append(spans, finding)
15
+ previous = spans.last
16
+ return spans << span(finding) unless overlaps?(previous, finding)
17
+
18
+ previous[:end] = [previous[:end], finding[:end]].max
19
+ end
20
+
21
+ def overlaps?(previous, finding)
22
+ previous && finding[:start] < previous[:end]
23
+ end
24
+
25
+ def span(finding)
26
+ { start: finding[:start], end: finding[:end], replacement: finding[:rule].replacement }
27
+ end
28
+ private_class_method :append, :overlaps?, :span
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../policy_rule'
4
+ require_relative 'configuration_hash'
5
+ require_relative 'rule_normalizer'
6
+
7
+ module Olyx
8
+ module Guardrails
9
+ module PolicyComponents
10
+ # Normalizes policy rules and enforces unique identifiers.
11
+ module RuleCollection
12
+ module_function
13
+
14
+ def call(values)
15
+ validate_collection!(values)
16
+
17
+ rules = values.map { |value| RuleNormalizer.call(value) }
18
+ validate_unique_names!(rules)
19
+ rules.freeze
20
+ end
21
+
22
+ def validate_collection!(values)
23
+ raise ArgumentError, RuleNormalizer::ERROR unless values.is_a?(Array)
24
+ end
25
+
26
+ def validate_unique_names!(rules)
27
+ names = rules.map(&:name)
28
+ raise ArgumentError, 'policy rule names must be unique' unless names.uniq.length == names.length
29
+ end
30
+ private_class_method :validate_collection!, :validate_unique_names!
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'pattern_matcher'
4
+ require_relative 'normalized_pattern_matcher'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module PolicyComponents
9
+ # Collects original and normalized matches for one policy rule.
10
+ module RuleMatcher
11
+ TIMEOUT_ERROR = defined?(Regexp::TimeoutError) ? Regexp::TimeoutError : RegexpError
12
+
13
+ module_function
14
+
15
+ def call(source, rule, index)
16
+ rule.patterns.flat_map { |pattern| matches(source, rule, index, pattern) }
17
+ rescue TIMEOUT_ERROR
18
+ raise ArgumentError, "policy rule #{rule.name.inspect} timed out"
19
+ end
20
+
21
+ def matches(source, rule, index, pattern)
22
+ PatternMatcher.call(source, rule, index, pattern) +
23
+ NormalizedPatternMatcher.call(source, rule, index, pattern)
24
+ end
25
+ private_class_method :matches
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module PolicyComponents
6
+ # Converts policy rule Hashes into immutable PolicyRule values.
7
+ module RuleNormalizer
8
+ ERROR = 'policy rules must be an Array of PolicyRule or Hash values'
9
+
10
+ module_function
11
+
12
+ def call(value)
13
+ return value if value.is_a?(PolicyRule)
14
+ return PolicyRule.new(**ConfigurationHash.call(value)) if value.is_a?(Hash)
15
+
16
+ raise ArgumentError, ERROR
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end