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,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module PolicyComponents
6
+ # Presents policy matches as a scan decision.
7
+ module ScanResult
8
+ module_function
9
+
10
+ def call(findings)
11
+ violated = findings.any?
12
+ {
13
+ violated: violated,
14
+ blocked: violated && findings.any? { |finding| finding[:rule].block? },
15
+ findings: FindingPresenter.call(findings)
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../secret_scanner'
4
+ require_relative '../validation'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module PolicyComponents
9
+ # Validates custom secret patterns once during policy construction.
10
+ module SecretPatternCollection
11
+ module_function
12
+
13
+ def call(values)
14
+ Validation.array_of!(values, String, name: 'policy secret_patterns')
15
+ patterns = values.map { |pattern| pattern.dup.freeze }.freeze
16
+ SecretScanner.scan('', custom_patterns: patterns)
17
+ patterns
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module PolicyComponents
6
+ # Accumulates transformed gaps and fixed policy replacements in source order.
7
+ class UnmatchedSegmentBuilder
8
+ def initialize(source, transform)
9
+ @source = source
10
+ @transform = transform
11
+ @cursor = 0
12
+ @parts = []
13
+ end
14
+
15
+ def append(span)
16
+ @parts << @transform.call(@source[@cursor...span[:start]])
17
+ @parts << span[:replacement]
18
+ @cursor = span[:end]
19
+ end
20
+
21
+ def finish
22
+ @parts << @transform.call(@source[@cursor..])
23
+ @parts.join
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'unmatched_segment_builder'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module PolicyComponents
8
+ # Applies a secondary transformation only outside policy spans.
9
+ module UnmatchedTransformer
10
+ module_function
11
+
12
+ def call(source, spans, transform)
13
+ builder = UnmatchedSegmentBuilder.new(source, transform)
14
+ spans.each { |span| builder.append(span) }
15
+ builder.finish
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'policy/configuration_hash'
4
+ require_relative 'policy/configuration'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ # Immutable enforcement configuration for decisions and transformations.
9
+ class Policy
10
+ def self.default
11
+ @default ||= new
12
+ end
13
+
14
+ def self.from_h(config)
15
+ new(**PolicyComponents::ConfigurationHash.call(config))
16
+ end
17
+
18
+ def initialize(
19
+ name: 'default',
20
+ max_input_length: 10_000,
21
+ block_pii: false,
22
+ block_injections: true,
23
+ block_secrets: false,
24
+ ai_failure_mode: :allow,
25
+ secret_patterns: [],
26
+ rules: []
27
+ )
28
+ @configuration = PolicyComponents::Configuration.new(
29
+ identity: [name, max_input_length],
30
+ blocking: [block_pii, block_injections, block_secrets],
31
+ restrictions: [ai_failure_mode, secret_patterns, rules]
32
+ )
33
+ freeze
34
+ end
35
+
36
+ def name = @configuration.identity.name
37
+ def max_input_length = @configuration.identity.max_input_length
38
+ def ai_failure_mode = @configuration.restrictions.ai_failure_mode
39
+ def secret_patterns = @configuration.restrictions.secret_patterns
40
+ def rules = @configuration.restrictions.rules
41
+ def block_pii? = @configuration.blocking.pii?
42
+ def block_injections? = @configuration.blocking.injections?
43
+ def block_secrets? = @configuration.blocking.secrets?
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'pii_scrubber'
4
+ require_relative 'policy_scanner'
5
+ require_relative 'secret_scanner'
6
+ require_relative 'redaction/content_result'
7
+ require_relative 'redaction/unrestricted_text'
8
+
9
+ module Olyx
10
+ # Guardrail policy types and evaluation services.
11
+ module Guardrails
12
+ # Produces one policy-aware redaction result while preserving findings from
13
+ # the original source. Policy spans take precedence over narrower scanner
14
+ # substitutions; confidentiality markers remain fail-closed.
15
+ class PolicyAwareRedactor
16
+ def self.call(source, policy:)
17
+ new(source, policy).call
18
+ end
19
+
20
+ def initialize(source, policy)
21
+ @source = source.to_s
22
+ @policy = policy
23
+ end
24
+
25
+ def call
26
+ secret_scan = SecretScanner.scan(
27
+ @source,
28
+ custom_patterns: @policy.secret_patterns
29
+ )
30
+ policy_redaction = PolicyScanner.redact(@source, policy: @policy) do |text|
31
+ Redaction::UnrestrictedText.call(text, @policy)
32
+ end
33
+
34
+ Redaction::ContentResult.call(
35
+ @source, text: policy_redaction[:text], secret_scan: secret_scan, policy_redaction: policy_redaction
36
+ )
37
+ end
38
+ end
39
+
40
+ private_constant :PolicyAwareRedactor
41
+ end
42
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'policy/redaction_spans'
4
+ require_relative 'policy/unmatched_transformer'
5
+
6
+ module Olyx
7
+ # Policy-driven safety checks and redaction for Ruby applications.
8
+ module Guardrails
9
+ # Applies offset-aware replacements for private policy findings.
10
+ class PolicyRedactor
11
+ def self.call(source, findings, transform: nil)
12
+ spans = PolicyComponents::RedactionSpans.call(findings)
13
+ return PolicyComponents::UnmatchedTransformer.call(source, spans, transform) if transform
14
+
15
+ replace(source, spans)
16
+ end
17
+
18
+ def self.replace(source, spans)
19
+ spans.reverse_each.with_object(source.dup) do |span, output|
20
+ output[span[:start]...span[:end]] = span[:replacement]
21
+ end
22
+ end
23
+ private_class_method :replace
24
+ end
25
+
26
+ private_constant :PolicyRedactor
27
+ end
28
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'match_mode'
4
+ require_relative 'pattern_compiler'
5
+ require_relative 'values'
6
+
7
+ module Olyx
8
+ module Guardrails
9
+ module PolicyRuleComponents
10
+ # Validated identity and descriptive policy-rule fields.
11
+ class IdentityConfiguration
12
+ attr_reader :name, :description
13
+
14
+ def initialize(name, description)
15
+ @name = Values.name(name)
16
+ @description = Values.description(description)
17
+ freeze
18
+ end
19
+ end
20
+
21
+ # Validated policy-rule matching configuration.
22
+ class MatchConfiguration
23
+ attr_reader :mode, :patterns
24
+
25
+ def initialize(patterns, terms, match)
26
+ @mode = MatchMode.call(match)
27
+ @patterns = PatternCompiler.call(patterns: patterns, terms: terms, match: @mode)
28
+ freeze
29
+ end
30
+ end
31
+
32
+ # Validated policy-rule enforcement behavior.
33
+ class EnforcementConfiguration
34
+ attr_reader :replacement
35
+
36
+ def initialize(block, replacement)
37
+ @block = Values.boolean(block)
38
+ @replacement = Values.replacement(replacement)
39
+ freeze
40
+ end
41
+
42
+ def block? = @block
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module PolicyRuleComponents
6
+ # Validates an optional human-readable rule description.
7
+ module DescriptionValue
8
+ module_function
9
+
10
+ def call(value)
11
+ return nil if value.nil?
12
+ return value.dup.freeze if valid?(value)
13
+
14
+ raise ArgumentError, 'policy rule description must be a String of 1..500 characters or nil'
15
+ end
16
+
17
+ def valid?(value)
18
+ value.is_a?(String) && !value.strip.empty? && value.length <= 500
19
+ end
20
+ private_class_method :valid?
21
+ end
22
+ end
23
+ end
24
+ 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 PolicyRuleComponents
8
+ # Validates how configured terms are interpreted.
9
+ MatchMode = EnumValue.new(
10
+ allowed: %i[substring whole_word regexp],
11
+ error: 'policy rule match must be substring, whole_word, or regexp'
12
+ )
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module PolicyRuleComponents
6
+ # Validates and freezes a policy-rule identifier.
7
+ module NameValue
8
+ FORMAT = /\A[a-z][a-z0-9_.:-]*\z/i
9
+
10
+ module_function
11
+
12
+ def call(value)
13
+ normalized = value.to_s
14
+ valid = (value.is_a?(String) || value.is_a?(Symbol)) && normalized.match?(FORMAT)
15
+ raise ArgumentError, 'policy rule name must be a String or Symbol identifier' unless valid
16
+
17
+ normalized.dup.freeze
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'regexp_compiler'
4
+ require_relative 'term_compiler'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module PolicyRuleComponents
9
+ # Compiles bounded policy regexes and rejects empty matching patterns.
10
+ module PatternCompiler
11
+ module_function
12
+
13
+ def call(patterns:, terms:, match:)
14
+ validate_collections!(patterns, terms)
15
+ compiled = patterns.map { |value| RegexpCompiler.call(value) }
16
+ compiled.concat(terms.map { |term| TermCompiler.call(term, mode: match) }).freeze
17
+ end
18
+
19
+ def validate_collections!(patterns, terms)
20
+ valid = patterns.is_a?(Array) && terms.is_a?(Array) && !(patterns.empty? && terms.empty?)
21
+ raise ArgumentError, 'policy rule patterns or terms must be a non-empty Array' unless valid
22
+ end
23
+
24
+ private_class_method :validate_collections!
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module PolicyRuleComponents
6
+ # Compiles bounded regular-expression policy patterns.
7
+ module RegexpCompiler
8
+ TIMEOUT = 0.1
9
+
10
+ module_function
11
+
12
+ def call(value)
13
+ pattern = build(value)
14
+ raise ArgumentError, 'policy rule patterns must not match empty text' if pattern.match?('')
15
+
16
+ pattern
17
+ rescue RegexpError => error
18
+ raise ArgumentError, "invalid policy rule pattern #{value.inspect}: #{error.message}"
19
+ end
20
+
21
+ def build(value)
22
+ return Regexp.new(value, Regexp::IGNORECASE, timeout: TIMEOUT) if value.is_a?(String)
23
+ return Regexp.new(value.source, value.options, timeout: TIMEOUT) if value.is_a?(Regexp)
24
+
25
+ raise ArgumentError, 'policy rule patterns must contain only Strings or Regexps'
26
+ end
27
+ private_class_method :build
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module PolicyRuleComponents
6
+ # Validates a bounded single-line rule replacement.
7
+ module ReplacementValue
8
+ module_function
9
+
10
+ def call(value)
11
+ return value.dup.freeze if valid?(value)
12
+
13
+ raise ArgumentError, 'policy rule replacement must be a single-line String of 1..100 characters'
14
+ end
15
+
16
+ def valid?(value)
17
+ value.is_a?(String) && bounded?(value) && single_line?(value)
18
+ end
19
+
20
+ def bounded?(value) = !value.empty? && value.length <= 100
21
+ def single_line?(value) = !value.match?(/[\r\n\t]/)
22
+ private_class_method :bounded?, :single_line?, :valid?
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'regexp_compiler'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module PolicyRuleComponents
8
+ # Compiles literal terms without exposing regex semantics.
9
+ module TermCompiler
10
+ module_function
11
+
12
+ def call(term, mode:)
13
+ validate!(term)
14
+ return RegexpCompiler.call(term) if mode == :regexp
15
+
16
+ Regexp.new(source(term, mode), Regexp::IGNORECASE, timeout: RegexpCompiler::TIMEOUT)
17
+ end
18
+
19
+ def source(term, mode)
20
+ escaped = Regexp.escape(term)
21
+ mode == :whole_word ? "(?<![[:alnum:]_])#{escaped}(?![[:alnum:]_])" : escaped
22
+ end
23
+
24
+ def validate!(term)
25
+ return if term.is_a?(String) && !term.empty?
26
+
27
+ raise ArgumentError, 'policy rule terms must contain only non-empty Strings'
28
+ end
29
+ private_class_method :source, :validate!
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'description_value'
4
+ require_relative 'replacement_value'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module PolicyRuleComponents
9
+ # Validates human-readable rule description and replacement values.
10
+ module TextValues
11
+ module_function
12
+
13
+ def description(value)
14
+ DescriptionValue.call(value)
15
+ end
16
+
17
+ def replacement(value)
18
+ ReplacementValue.call(value)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'name_value'
4
+ require_relative 'text_values'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module PolicyRuleComponents
9
+ # Validates non-pattern policy-rule values.
10
+ module Values
11
+ module_function
12
+
13
+ def name(value)
14
+ NameValue.call(value)
15
+ end
16
+
17
+ def boolean(value)
18
+ return value if [true, false].include?(value)
19
+
20
+ raise ArgumentError, 'policy rule block must be true or false'
21
+ end
22
+
23
+ def description(value)
24
+ TextValues.description(value)
25
+ end
26
+
27
+ def replacement(value)
28
+ TextValues.replacement(value)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'policy_rule/configuration'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ # Immutable named restricted-content rule.
8
+ class PolicyRule
9
+ def initialize(name:, patterns: [], terms: [], match: :substring, block: true, description: nil, replacement: nil)
10
+ @identity = PolicyRuleComponents::IdentityConfiguration.new(name, description)
11
+ @matching = PolicyRuleComponents::MatchConfiguration.new(patterns, terms, match)
12
+ @enforcement = PolicyRuleComponents::EnforcementConfiguration.new(block, replacement || default_replacement)
13
+ freeze
14
+ end
15
+
16
+ def name = @identity.name
17
+ def description = @identity.description
18
+ def match = @matching.mode
19
+ def patterns = @matching.patterns
20
+ def replacement = @enforcement.replacement
21
+ def block? = @enforcement.block?
22
+
23
+ private
24
+
25
+ def default_replacement
26
+ "[RESTRICTED:#{@identity.name.upcase}]"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'policy/finding_presenter'
4
+ require_relative 'policy/match_collector'
5
+ require_relative 'policy/redaction_result'
6
+ require_relative 'policy/scan_result'
7
+ require_relative 'policy_redactor'
8
+
9
+ module Olyx
10
+ # Policy-driven safety checks and redaction for Ruby applications.
11
+ module Guardrails
12
+ # Presents restricted-content decisions while keeping matched text private.
13
+ class PolicyScanner
14
+ def self.scan(text, policy:)
15
+ new(text, policy).scan
16
+ end
17
+
18
+ def self.redact(text, policy:, &transform)
19
+ new(text, policy).redact(&transform)
20
+ end
21
+
22
+ def initialize(text, policy)
23
+ @source = text.to_s
24
+ @policy = policy
25
+ end
26
+
27
+ def scan
28
+ PolicyComponents::ScanResult.call(raw_findings)
29
+ end
30
+
31
+ def redact(&transform)
32
+ PolicyComponents::RedactionResult.call(@source, raw_findings, transform)
33
+ end
34
+
35
+ private
36
+
37
+ def raw_findings
38
+ PolicyComponents::MatchCollector.call(@source, @policy.rules)
39
+ end
40
+ end
41
+
42
+ private_constant :PolicyScanner
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ingress'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Rails
8
+ # Opt-in enforcement helpers for Action Cable channel payloads.
9
+ module ActionCable
10
+ include Ingress
11
+
12
+ private
13
+
14
+ alias guardrails_check_cable! guardrails_check_ingress!
15
+ alias guardrails_check_cable_output! guardrails_check_ingress_output!
16
+
17
+ def guardrails_ingress_key = :channel
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'job_reference'
4
+ require_relative 'queue_name'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module Rails
9
+ # Adapts immutable notification events to any Active Job-compatible job.
10
+ class ActiveJobHandler
11
+ def initialize(job:, queue: nil)
12
+ @job = JobReference.new(job)
13
+ @queue = QueueName.call(queue)
14
+ freeze
15
+ end
16
+
17
+ def call(event)
18
+ raise ArgumentError, 'Active Job notification event must be a Hash' unless event.is_a?(Hash)
19
+
20
+ target = @job.resolve
21
+ target = target.set(queue: @queue) if @queue
22
+ target.perform_later(event)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+
5
+ # Explicit Active Model validator for AI-bound text attributes.
6
+ class OlyxGuardrailsValidator < ActiveModel::EachValidator
7
+ def validate_each(record, attribute, value)
8
+ result = evaluate(record, attribute, value)
9
+ return if result[:allowed]
10
+
11
+ record.errors.add(attribute, options.fetch(:message, 'was rejected by guardrail policy'))
12
+ end
13
+
14
+ private
15
+
16
+ def evaluate(record, attribute, value)
17
+ policy = options[:policy]
18
+ return Olyx::Guardrails.check(value, policy: policy) if policy
19
+
20
+ metadata = { model: record.class.name, attribute: attribute }
21
+ Olyx::Guardrails::Rails.check(value, metadata: metadata)
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'instrumentation'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Rails
8
+ # Publishes check and violation telemetry from one decision summary.
9
+ module CheckTelemetry
10
+ module_function
11
+
12
+ def call(result, duration)
13
+ Instrumentation.publish_check(result, duration)
14
+ Instrumentation.publish_violation(result, duration) if result[:risk_score].to_f.positive?
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end