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,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../notifier'
4
+ require_relative '../policy'
5
+ require_relative '../validation'
6
+ require_relative 'policy_file'
7
+ require_relative 'configuration_values'
8
+ require_relative 'configuration_finalizer'
9
+ require_relative 'integration_configuration'
10
+ require_relative 'policy_configuration'
11
+
12
+ module Olyx
13
+ module Guardrails
14
+ module Rails
15
+ # Mutable-at-boot, immutable-at-runtime Rails adapter configuration.
16
+ class Configuration
17
+ include IntegrationConfiguration
18
+ include PolicyConfiguration
19
+
20
+ DEFAULT_FILTER_PARAMETERS = %i[prompt system_prompt ai_input llm_input].freeze
21
+
22
+ attr_reader :ai_analyzer, :enabled, :filter_parameters, :notifier,
23
+ :notifier_handlers, :policy, :policy_path
24
+
25
+ def initialize
26
+ @enabled = true
27
+ @filter_parameters = DEFAULT_FILTER_PARAMETERS
28
+ @notifier_handlers = {}.freeze
29
+ @policy = nil
30
+ @policy_path = nil
31
+ @finalized = false
32
+ end
33
+
34
+ # Resolves and validates the policy and handlers once at boot.
35
+ def finalize!(environment:)
36
+ return self if finalized?
37
+
38
+ @policy, @notifier = ConfigurationFinalizer.call(
39
+ policy: @policy,
40
+ path: @policy_path,
41
+ handlers: @notifier_handlers,
42
+ environment: environment
43
+ )
44
+ @finalized = true
45
+ freeze
46
+ end
47
+
48
+ def finalized?
49
+ @finalized
50
+ end
51
+
52
+ def ensure_enabled!
53
+ return if @enabled
54
+
55
+ raise ConfigurationError, 'Olyx Guardrails Rails integration is disabled'
56
+ end
57
+
58
+ private
59
+
60
+ def mutable!
61
+ return unless finalized?
62
+
63
+ raise ConfigurationError, 'Olyx Guardrails Rails configuration is already finalized'
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Rails
6
+ # Resolves policy sources and constructs the optional notifier at boot.
7
+ module ConfigurationFinalizer
8
+ module_function
9
+
10
+ def call(policy:, path:, handlers:, environment:)
11
+ raise ConfigurationError, 'configure either Rails policy or policy_path, not both' if policy && path
12
+
13
+ resolved_policy = policy || load_policy(path, environment)
14
+ [resolved_policy, build_notifier(resolved_policy, handlers)]
15
+ end
16
+
17
+ def load_policy(path, environment)
18
+ path ? PolicyFile.load(path, environment: environment) : Policy.default
19
+ end
20
+
21
+ def build_notifier(policy, handlers)
22
+ Notifier.new(policy: policy, handlers: handlers) unless handlers.empty?
23
+ rescue ArgumentError => error
24
+ raise ConfigurationError, "invalid Rails notifier configuration: #{error.message}"
25
+ end
26
+ private_class_method :load_policy, :build_notifier
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'configuration'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Rails
8
+ # Owns the Rails adapter's boot configuration lifecycle.
9
+ class ConfigurationRegistry
10
+ attr_reader :configuration
11
+
12
+ def initialize
13
+ @configuration = Configuration.new
14
+ end
15
+
16
+ def configure
17
+ yield configuration
18
+ configuration
19
+ end
20
+
21
+ def finalize!(environment:)
22
+ configuration.finalize!(environment: environment)
23
+ end
24
+
25
+ def ready
26
+ finalize!(environment: current_environment) unless configuration.finalized?
27
+ configuration.ensure_enabled!
28
+ configuration
29
+ end
30
+
31
+ private
32
+
33
+ def current_environment
34
+ return ::Rails.env if defined?(::Rails) && ::Rails.respond_to?(:env)
35
+
36
+ ENV.fetch('RAILS_ENV', 'development')
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../policy'
4
+ require_relative 'filter_parameters'
5
+ require_relative 'path_value'
6
+
7
+ module Olyx
8
+ module Guardrails
9
+ module Rails
10
+ # Normalizes individual Rails configuration values before boot freezes.
11
+ module ConfigurationValues
12
+ module_function
13
+
14
+ def policy(value)
15
+ return value if value.is_a?(Policy)
16
+
17
+ raise ArgumentError, 'Rails policy must be an Olyx::Guardrails::Policy'
18
+ end
19
+
20
+ def path(value)
21
+ PathValue.call(value)
22
+ end
23
+
24
+ def filter_parameters(value)
25
+ FilterParameters.call(value)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Rails
6
+ # Resolves a namespaced constant without inherited lookup.
7
+ module ConstantResolver
8
+ module_function
9
+
10
+ def call(name)
11
+ name.split('::').reduce(Object) { |namespace, constant| namespace.const_get(constant, false) }
12
+ rescue NameError
13
+ raise ArgumentError, "configured Active Job class #{name.inspect} is not defined"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'result_summary'
4
+ require_relative 'controller_metadata'
5
+ require_relative 'enforcer'
6
+
7
+ module Olyx
8
+ module Guardrails
9
+ module Rails
10
+ # Explicit Action Controller helpers; no parameters are scanned globally.
11
+ module Controller
12
+ private
13
+
14
+ def guardrails_check(input, metadata: {})
15
+ Guardrails::Rails.check(input, metadata: request_metadata(metadata))
16
+ end
17
+
18
+ def guardrails_check!(input, metadata: {})
19
+ Enforcer.check!(input, metadata: request_metadata(metadata))
20
+ end
21
+
22
+ def guardrails_redact(input)
23
+ Guardrails::Rails.redact(input)
24
+ end
25
+
26
+ def request_metadata(metadata)
27
+ raise ArgumentError, 'guardrail metadata must be a Hash' unless metadata.is_a?(Hash)
28
+
29
+ ControllerMetadata.call(self).merge(metadata)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Rails
6
+ # Builds bounded request identity metadata from a controller instance.
7
+ module ControllerMetadata
8
+ module_function
9
+
10
+ def call(controller)
11
+ request = controller.request
12
+ {
13
+ request_id: request.request_id,
14
+ controller: controller.controller_name,
15
+ action: controller.action_name,
16
+ http_method: request.request_method
17
+ }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'evaluation_lifecycle'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Rails
8
+ # Configures a reusable Rails lifecycle around one public decision operation.
9
+ class DecisionService
10
+ def initialize(operation, notification_input: nil)
11
+ @operation = operation
12
+ @notification_input = notification_input
13
+ end
14
+
15
+ def call(input, metadata:, configuration:)
16
+ notification_input = @notification_input ? @notification_input.call(input) : input
17
+ EvaluationLifecycle.call(notification_input, metadata: metadata, configuration: configuration) do
18
+ Guardrails.public_send(
19
+ @operation, input, policy: configuration.policy, ai_analyzer: configuration.ai_analyzer
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'result_summary'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Rails
8
+ # Provides reusable exception-driven enforcement for Rails entry points.
9
+ module Enforcer
10
+ module_function
11
+
12
+ def check!(input, metadata: {})
13
+ enforce(Guardrails::Rails.check(input, metadata: metadata))
14
+ end
15
+
16
+ def check_messages!(messages, metadata: {})
17
+ enforce(Guardrails::Rails.check_messages(messages, metadata: metadata))
18
+ end
19
+
20
+ def check_output!(output, metadata: {})
21
+ enforce(Guardrails::Rails.check_output(output, metadata: metadata))
22
+ end
23
+
24
+ def enforce(result)
25
+ return result if result[:allowed]
26
+
27
+ raise Blocked, ResultSummary.call(result)
28
+ end
29
+ private_class_method :enforce
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'check_telemetry'
4
+ require_relative 'notification_dispatcher'
5
+ require_relative 'timer'
6
+
7
+ module Olyx
8
+ module Guardrails
9
+ module Rails
10
+ # Applies the common telemetry and notification lifecycle to a decision.
11
+ module EvaluationLifecycle
12
+ module_function
13
+
14
+ def call(notification_input, metadata:, configuration:, &)
15
+ result, duration = Timer.call(&)
16
+ CheckTelemetry.call(result, duration)
17
+ NotificationDispatcher.call(configuration, result, notification_input, metadata)
18
+ result
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'decision_service'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Rails
8
+ # Runs one configured text decision and its post-decision integrations.
9
+ EvaluationService = DecisionService.new(:check)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Rails
6
+ # Normalizes Rails parameter-filter names.
7
+ module FilterParameters
8
+ module_function
9
+
10
+ def call(value)
11
+ unless valid?(value)
12
+ raise ArgumentError,
13
+ 'Rails filter_parameters must be an Array of String or Symbol values'
14
+ end
15
+
16
+ value.map(&:to_sym).uniq.freeze
17
+ end
18
+
19
+ def valid?(value)
20
+ value.is_a?(Array) && value.all? { |item| item.is_a?(String) || item.is_a?(Symbol) }
21
+ end
22
+ private_class_method :valid?
23
+ end
24
+ end
25
+ end
26
+ 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 GraphQL resolvers and mutations.
9
+ module GraphQL
10
+ include Ingress
11
+
12
+ private
13
+
14
+ alias guardrails_check_graphql! guardrails_check_ingress!
15
+ alias guardrails_check_graphql_output! guardrails_check_ingress_output!
16
+
17
+ def guardrails_ingress_key = :graphql_resolver
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'enforcer'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Rails
8
+ # Shared enforcement behavior for explicit non-controller ingress concerns.
9
+ module Ingress
10
+ private
11
+
12
+ def guardrails_check_ingress!(value, metadata: {})
13
+ Enforcer.check!(value, metadata: guardrails_ingress_metadata(metadata))
14
+ end
15
+
16
+ def guardrails_check_ingress_output!(value, metadata: {})
17
+ Enforcer.check_output!(value, metadata: guardrails_ingress_metadata(metadata))
18
+ end
19
+
20
+ def guardrails_ingress_metadata(metadata)
21
+ { guardrails_ingress_key => self.class.name }.merge(metadata)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'evaluation_service'
4
+ require_relative 'redaction_service'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module Rails
9
+ # Executes configured checks and redaction for inbound text.
10
+ class InputRuntime
11
+ def initialize(registry)
12
+ @registry = registry
13
+ end
14
+
15
+ def check(input, metadata: {})
16
+ EvaluationService.call(input, metadata: metadata, configuration: @registry.ready)
17
+ end
18
+
19
+ def redact(input)
20
+ RedactionService.call(input, configuration: @registry.ready)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/notifications'
4
+ require_relative 'instrumentation_payloads'
5
+ require_relative 'instrumentation_publisher'
6
+ require_relative 'result_summary'
7
+
8
+ module Olyx
9
+ module Guardrails
10
+ module Rails
11
+ # Publishes sanitized, content-free Active Support events.
12
+ module Instrumentation
13
+ module_function
14
+
15
+ def publish_check(result, duration)
16
+ InstrumentationPublisher.call(
17
+ 'check.olyx_guardrails',
18
+ ResultSummary.call(result).merge(evaluation_duration_ms: duration)
19
+ )
20
+ end
21
+
22
+ def publish_violation(result, duration)
23
+ InstrumentationPublisher.call(
24
+ 'violation.olyx_guardrails',
25
+ ResultSummary.call(result).merge(evaluation_duration_ms: duration)
26
+ )
27
+ end
28
+
29
+ def publish_redaction(result, duration)
30
+ InstrumentationPublisher.call('redact.olyx_guardrails', InstrumentationPayloads.redaction(result, duration))
31
+ end
32
+
33
+ def publish_notification(delivery)
34
+ InstrumentationPublisher.call('notification.olyx_guardrails', InstrumentationPayloads.notification(delivery))
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'redaction_payload'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Rails
8
+ # Builds content-free redaction and delivery telemetry payloads.
9
+ module InstrumentationPayloads
10
+ module_function
11
+
12
+ def redaction(result, duration)
13
+ RedactionPayload.call(result, duration)
14
+ end
15
+
16
+ def notification(delivery)
17
+ {
18
+ success: delivery[:success] == true,
19
+ delivery_count: Array(delivery[:deliveries]).length
20
+ }.freeze
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Rails
6
+ # Publishes Active Support events and isolates observability failures.
7
+ module InstrumentationPublisher
8
+ module_function
9
+
10
+ def call(name, payload)
11
+ ActiveSupport::Notifications.instrument(name, payload)
12
+ rescue StandardError => error
13
+ logger = rails_logger
14
+ logger&.warn("Olyx Guardrails instrumentation #{name} failed (#{error.class})")
15
+ end
16
+
17
+ def rails_logger
18
+ ::Rails.logger if defined?(::Rails) && ::Rails.respond_to?(:logger)
19
+ end
20
+ private_class_method :rails_logger
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Rails
6
+ # Owns runtime integration assignments during Rails boot.
7
+ module IntegrationConfiguration
8
+ def enabled=(value)
9
+ mutable!
10
+ @enabled = Validation.boolean!(value, name: 'Rails integration enabled')
11
+ end
12
+
13
+ def ai_analyzer=(value)
14
+ mutable!
15
+ @ai_analyzer = Validation.callable_or_nil!(value, name: 'Rails integration ai_analyzer')
16
+ end
17
+
18
+ def notifier_handlers=(value)
19
+ mutable!
20
+ raise ArgumentError, 'Rails notifier_handlers must be a Hash' unless value.is_a?(Hash)
21
+
22
+ @notifier_handlers = value.dup.freeze
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'enforcer'
4
+ require_relative 'job_argument'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module Rails
9
+ # Opt-in Active Job callback for declared AI-input arguments.
10
+ module Job
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+
15
+ # Class-level declaration for protected argument indexes or keyword names.
16
+ module ClassMethods
17
+ attr_reader :guardrails_argument_selectors
18
+
19
+ def guardrails_input_arguments(*selectors)
20
+ @guardrails_argument_selectors = selectors.freeze
21
+ before_perform { |job| job.send(:guardrails_enforce_arguments!) }
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def guardrails_enforce_arguments!
28
+ job_class = self.class
29
+ job_class.guardrails_argument_selectors.each do |selector|
30
+ input = JobArgument.call(arguments, selector)
31
+ Enforcer.check!(input, metadata: { job: job_class.name, argument: selector })
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Rails
6
+ # Extracts a declared positional or keyword Active Job argument.
7
+ module JobArgument
8
+ module_function
9
+
10
+ def call(arguments, selector)
11
+ return arguments.fetch(selector) if selector.is_a?(Integer)
12
+
13
+ keyword(arguments, selector)
14
+ end
15
+
16
+ def keyword(arguments, selector)
17
+ validate_selector!(selector)
18
+
19
+ keywords = keyword_arguments(arguments)
20
+ raise ArgumentError, "guardrail job keyword #{selector.inspect} is missing" unless keywords
21
+
22
+ fetch_keyword(keywords, selector)
23
+ end
24
+ private_class_method :keyword
25
+
26
+ def fetch_keyword(keywords, selector)
27
+ keywords.fetch(selector) { keywords.fetch(selector.to_s) }
28
+ end
29
+
30
+ def keyword_arguments(arguments)
31
+ arguments.reverse.find { |argument| argument.is_a?(Hash) }
32
+ end
33
+
34
+ def validate_selector!(selector)
35
+ raise ArgumentError, 'job guardrail selectors must be Integer or Symbol' unless selector.is_a?(Symbol)
36
+ end
37
+ private_class_method :fetch_keyword, :keyword_arguments, :validate_selector!
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'constant_resolver'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Rails
8
+ # Validates and resolves an Active Job class reference.
9
+ class JobReference
10
+ NAME = /\A[A-Z]\w*(?:::[A-Z]\w*)*\z/
11
+
12
+ def initialize(job)
13
+ @job = normalize(job)
14
+ end
15
+
16
+ def resolve
17
+ target = @job.is_a?(String) ? ConstantResolver.call(@job) : @job
18
+ return target if target.respond_to?(:perform_later)
19
+
20
+ raise ArgumentError, 'configured job must respond to perform_later'
21
+ end
22
+
23
+ private
24
+
25
+ def normalize(job)
26
+ return job if job.respond_to?(:perform_later)
27
+
28
+ name = job.to_s
29
+ return name.freeze if (job.is_a?(String) || job.is_a?(Symbol)) && name.match?(NAME)
30
+
31
+ raise ArgumentError, 'job must be an Active Job class or a constant name'
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end