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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../message_source'
4
+ require_relative 'decision_service'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module Rails
9
+ # Evaluates structured messages with Rails telemetry and notifications.
10
+ MessageEvaluationService = DecisionService.new(:check_messages, notification_input: MessageSource)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'message_evaluation_service'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Rails
8
+ # Executes configured checks for structured conversation messages.
9
+ class MessageRuntime
10
+ def initialize(registry)
11
+ @registry = registry
12
+ end
13
+
14
+ def check_messages(messages, metadata: {})
15
+ MessageEvaluationService.call(messages, metadata: metadata, configuration: @registry.ready)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'instrumentation'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Rails
8
+ # Dispatches optional Rails notifications without coupling evaluation.
9
+ module NotificationDispatcher
10
+ module_function
11
+
12
+ def call(configuration, result, input, metadata)
13
+ notifier = configuration.notifier
14
+ return unless notifier
15
+
16
+ delivery = notifier.notify(result, input: input, metadata: metadata)
17
+ Instrumentation.publish_notification(delivery) if delivery
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,26 @@
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 completed model output.
10
+ class OutputRuntime
11
+ def initialize(registry)
12
+ @registry = registry
13
+ end
14
+
15
+ def check_output(output, metadata: {})
16
+ context = { direction: 'output' }.merge(metadata)
17
+ EvaluationService.call(output, metadata: context, configuration: @registry.ready)
18
+ end
19
+
20
+ def redact_output(output)
21
+ RedactionService.call(output, configuration: @registry.ready)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Rails
6
+ # Normalizes an optional path-like configuration value.
7
+ module PathValue
8
+ module_function
9
+
10
+ def call(value)
11
+ return nil if value.nil?
12
+
13
+ normalized = normalize(value)
14
+ return normalized.dup.freeze if valid?(normalized)
15
+
16
+ raise ArgumentError, 'Rails policy_path must be a path-like value'
17
+ end
18
+
19
+ def normalize(value)
20
+ value.respond_to?(:to_path) ? value.to_path : value
21
+ end
22
+
23
+ def valid?(value)
24
+ value.is_a?(String) && !value.empty?
25
+ end
26
+ private_class_method :normalize, :valid?
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Rails
6
+ # Owns policy and filtering assignments during Rails boot.
7
+ module PolicyConfiguration
8
+ def policy=(value)
9
+ mutable!
10
+ @policy = ConfigurationValues.policy(value)
11
+ end
12
+
13
+ def policy_path=(value)
14
+ mutable!
15
+ @policy_path = ConfigurationValues.path(value)
16
+ end
17
+
18
+ def filter_parameters=(value)
19
+ mutable!
20
+ @filter_parameters = ConfigurationValues.filter_parameters(value)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'policy_yaml'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Rails
8
+ # Parses and selects direct or environment-keyed policy YAML.
9
+ class PolicyDocument
10
+ POLICY_KEYS = %w[name max_input_length block_pii block_injections block_secrets ai_failure_mode secret_patterns
11
+ rules].freeze
12
+
13
+ def initialize(path, environment)
14
+ @path = path
15
+ @environment = environment
16
+ end
17
+
18
+ def call
19
+ document = PolicyYaml.load(@path)
20
+ return document if direct?(document)
21
+
22
+ environment_policy(document)
23
+ end
24
+
25
+ private
26
+
27
+ def environment_policy(document)
28
+ selected = document[@environment] || document[@environment.to_sym]
29
+ return selected if selected.is_a?(Hash)
30
+
31
+ raise ConfigurationError, "guardrail policy file has no #{@environment.inspect} policy: #{@path}"
32
+ end
33
+
34
+ def direct?(document)
35
+ document.keys.any? { |key| POLICY_KEYS.include?(key.to_s) }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../policy'
4
+ require_relative 'policy_document'
5
+ require_relative 'policy_file_error'
6
+
7
+ module Olyx
8
+ module Guardrails
9
+ module Rails
10
+ # Safely loads direct or environment-keyed policy YAML without ERB.
11
+ class PolicyFile
12
+ def self.load(path, environment:)
13
+ new(path, environment).load
14
+ end
15
+
16
+ def initialize(path, environment)
17
+ @path = path.to_s
18
+ @environment = environment.to_s
19
+ end
20
+
21
+ def load
22
+ Policy.from_h(PolicyDocument.new(@path, @environment).call)
23
+ rescue Errno::ENOENT, Errno::EACCES, Psych::Exception, ArgumentError => error
24
+ raise PolicyFileError.call(error, @path)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Rails
6
+ # Translates policy loading failures into stable boot configuration errors.
7
+ module PolicyFileError
8
+ module_function
9
+
10
+ def call(error, path)
11
+ ConfigurationError.new(message(error, path))
12
+ end
13
+
14
+ def message(error, path)
15
+ return "guardrail policy file does not exist: #{path}" if error.is_a?(Errno::ENOENT)
16
+ return "guardrail policy file is not readable: #{path}" if error.is_a?(Errno::EACCES)
17
+ return "guardrail policy file contains invalid or unsafe YAML: #{path}" if error.is_a?(Psych::Exception)
18
+
19
+ "invalid guardrail policy in #{path}: #{error.message}"
20
+ end
21
+ private_class_method :message
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Rails
6
+ # Extracts unique policy rule identifiers for safe telemetry.
7
+ module PolicyRuleNames
8
+ module_function
9
+
10
+ def call(result)
11
+ Array(result[:policy_findings]).filter_map { |finding| name(finding) }.uniq.freeze
12
+ end
13
+
14
+ def name(finding) = finding[:rule]&.to_s
15
+ private_class_method :name
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Rails
8
+ # Safely reads a YAML policy document without aliases or object types.
9
+ module PolicyYaml
10
+ module_function
11
+
12
+ def load(path)
13
+ document = YAML.safe_load_file(path, permitted_classes: [], permitted_symbols: [], aliases: false,
14
+ filename: path)
15
+ return document if document.is_a?(Hash)
16
+
17
+ raise ConfigurationError, "guardrail policy file must contain a Hash: #{path}"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Rails
6
+ # Normalizes an optional Active Job queue name.
7
+ module QueueName
8
+ module_function
9
+
10
+ def call(queue)
11
+ return nil if queue.nil?
12
+
13
+ name = queue.to_s
14
+ return name.freeze if (queue.is_a?(String) || queue.is_a?(Symbol)) && !name.empty?
15
+
16
+ raise ArgumentError, 'queue must be a non-empty String or Symbol'
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Rails
6
+ # Builds content-free redaction telemetry.
7
+ module RedactionPayload
8
+ module_function
9
+
10
+ def call(result, duration)
11
+ flags(result).merge(evaluation_duration_ms: duration).freeze
12
+ end
13
+
14
+ def flags(result)
15
+ identity(result).merge(detections(result))
16
+ end
17
+
18
+ def identity(result)
19
+ {
20
+ policy_name: result[:policy_name].to_s[0...100],
21
+ redacted: result[:redacted] == true
22
+ }
23
+ end
24
+
25
+ def detections(result)
26
+ {
27
+ pii_detected: result[:pii_detected] == true,
28
+ secret_leaked: result[:secret_leaked] == true,
29
+ policy_violated: result[:policy_violated] == true
30
+ }
31
+ end
32
+ private_class_method :detections, :flags, :identity
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'instrumentation'
4
+ require_relative 'timer'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module Rails
9
+ # Runs one configured transformation and publishes safe telemetry.
10
+ module RedactionService
11
+ module_function
12
+
13
+ def call(input, configuration:)
14
+ result, duration = Timer.call { Guardrails.redact(input, policy: configuration.policy) }
15
+ Instrumentation.publish_redaction(result, duration)
16
+ result
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../violation_labels'
4
+ require_relative '../notification/deep_freezer'
5
+ require_relative 'policy_rule_names'
6
+
7
+ module Olyx
8
+ module Guardrails
9
+ module Rails
10
+ # Builds the bounded decision payload used by exceptions and telemetry.
11
+ module ResultSummary
12
+ module_function
13
+
14
+ def call(result)
15
+ Notification::DeepFreezer.call(decision(result).merge(policy(result)))
16
+ end
17
+
18
+ def decision(result)
19
+ {
20
+ policy_name: result[:policy_name].to_s[0...100],
21
+ allowed: result[:allowed] == true,
22
+ risk_score: result[:risk_score].to_f
23
+ }
24
+ end
25
+
26
+ def policy(result)
27
+ {
28
+ violations: Guardrails::ViolationLabels.call(result).freeze,
29
+ policy_rules: PolicyRuleNames.call(result)
30
+ }
31
+ end
32
+ private_class_method :decision, :policy
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require_relative 'configuration_registry'
5
+ require_relative 'input_runtime'
6
+ require_relative 'message_runtime'
7
+ require_relative 'output_runtime'
8
+
9
+ module Olyx
10
+ module Guardrails
11
+ module Rails
12
+ # Composes the independent configuration, input, message, and output runtimes.
13
+ class Runtime
14
+ extend Forwardable
15
+
16
+ def_delegators :@registry, :configuration, :configure, :finalize!
17
+ def_delegators :@input, :check, :redact
18
+ def_delegator :@messages, :check_messages
19
+ def_delegators :@output, :check_output, :redact_output
20
+
21
+ def initialize
22
+ @registry = ConfigurationRegistry.new
23
+ @input = InputRuntime.new(@registry)
24
+ @messages = MessageRuntime.new(@registry)
25
+ @output = OutputRuntime.new(@registry)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Rails
6
+ # Measures adapter operations with a monotonic clock.
7
+ module Timer
8
+ module_function
9
+
10
+ def call
11
+ started = clock_time
12
+ result = yield
13
+ [result, elapsed_milliseconds(started)]
14
+ end
15
+
16
+ def clock_time
17
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
18
+ end
19
+
20
+ def elapsed_milliseconds(started)
21
+ ((clock_time - started) * 1_000).round(3)
22
+ end
23
+ private_class_method :clock_time, :elapsed_milliseconds
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'enforcer'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Rails
8
+ # Checks caller-extracted upload text without owning parsing or storage.
9
+ module Upload
10
+ module_function
11
+
12
+ def check(upload, extractor:, metadata: {})
13
+ text = extract(upload, extractor)
14
+ Guardrails::Rails.check(text, metadata: { ingress: 'upload' }.merge(metadata))
15
+ end
16
+
17
+ def check!(upload, extractor:, metadata: {})
18
+ text = extract(upload, extractor)
19
+ Enforcer.check!(text, metadata: { ingress: 'upload' }.merge(metadata))
20
+ end
21
+
22
+ def extract(upload, extractor)
23
+ raise ArgumentError, 'upload extractor must respond to call' unless extractor.respond_to?(:call)
24
+
25
+ text = extractor.call(upload)
26
+ raise ArgumentError, 'upload extractor must return a String' unless text.is_a?(String)
27
+
28
+ text
29
+ end
30
+ private_class_method :extract
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/notifications'
4
+ require 'forwardable'
5
+ require_relative '../guardrails'
6
+ require_relative 'rails/active_job_handler'
7
+ require_relative 'rails/action_cable'
8
+ require_relative 'rails/active_model_validator'
9
+ require_relative 'rails/controller'
10
+ require_relative 'rails/enforcer'
11
+ require_relative 'rails/graphql'
12
+ require_relative 'rails/job'
13
+ require_relative 'rails/runtime'
14
+ require_relative 'rails/upload'
15
+
16
+ module Olyx
17
+ module Guardrails
18
+ # Optional Rails-native configuration and enforcement facade.
19
+ module Rails
20
+ extend SingleForwardable
21
+
22
+ def self.runtime
23
+ @runtime ||= Runtime.new
24
+ end
25
+ private_class_method :runtime
26
+
27
+ def_single_delegators :runtime,
28
+ :configuration, :configure, :finalize!,
29
+ :check, :redact, :check_messages,
30
+ :check_output, :redact_output
31
+ end
32
+ end
33
+ end
34
+
35
+ require_relative 'railtie' if defined?(Rails::Railtie)
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ # Boots and validates the optional Rails adapter.
8
+ class Railtie < ::Rails::Railtie
9
+ initializer 'olyx_guardrails.filter_parameters', after: :load_config_initializers do |app|
10
+ filters = Guardrails::Rails.configuration.filter_parameters
11
+ app.config.filter_parameters.concat(filters).uniq!
12
+ end
13
+
14
+ config.after_initialize do
15
+ Guardrails::Rails.finalize!(environment: ::Rails.env)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Redaction
6
+ # Builds the stable internal redaction content contract.
7
+ module ContentResult
8
+ module_function
9
+
10
+ def call(source, text:, secret_scan:, policy_redaction:)
11
+ findings = secret_scan[:findings]
12
+ detection(source, secret_scan, policy_redaction).merge(content(text, findings))
13
+ end
14
+
15
+ def content(text, findings)
16
+ output = '[REDACTED]' if findings.any? { |finding| finding[:category] == 'confidentiality_marker' }
17
+ { text: output || text, findings: findings }
18
+ end
19
+
20
+ def detection(source, secret_scan, policy_redaction)
21
+ {
22
+ pii_detected: PiiScrubber.scrub(source) != source,
23
+ secret_leaked: secret_scan[:leaked],
24
+ policy_violated: policy_redaction[:violated],
25
+ policy_findings: policy_redaction[:findings]
26
+ }
27
+ end
28
+ private_class_method :content, :detection
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Redaction
6
+ # Validates redaction policy type and configured length limit.
7
+ module InputValidator
8
+ module_function
9
+
10
+ def call(source, policy)
11
+ raise ArgumentError, 'policy must be an Olyx::Guardrails::Policy' unless policy.is_a?(Policy)
12
+ raise ArgumentError, 'input exceeds max_input_length' if source.length > policy.max_input_length
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Redaction
6
+ # Presents the stable public redaction result contract.
7
+ module PublicResult
8
+ CONTENT_FIELDS = %i[pii_detected secret_leaked policy_violated policy_findings findings].freeze
9
+
10
+ module_function
11
+
12
+ def call(source, content, policy)
13
+ text = content[:text]
14
+ content.slice(*CONTENT_FIELDS).merge(
15
+ text: text,
16
+ redacted: text != source,
17
+ policy_name: policy.name
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Redaction
6
+ # Applies secret and PII transforms outside policy-owned spans.
7
+ module UnrestrictedText
8
+ module_function
9
+
10
+ def call(text, policy)
11
+ secret_safe = SecretScanner.redact(text, custom_patterns: policy.secret_patterns)[:text]
12
+ PiiScrubber.scrub(secret_safe)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'policy'
4
+ require_relative 'policy_aware_redactor'
5
+ require_relative 'redaction/input_validator'
6
+ require_relative 'redaction/public_result'
7
+
8
+ module Olyx
9
+ module Guardrails
10
+ # Applies the explicit PII, secret, and restricted-policy transformation
11
+ # used by {Guardrails.redact}.
12
+ class Redactor
13
+ def self.call(input, policy:)
14
+ new(input, policy).call
15
+ end
16
+
17
+ def initialize(input, policy)
18
+ @source = input.to_s
19
+ @policy = policy
20
+ end
21
+
22
+ def call
23
+ Redaction::InputValidator.call(@source, @policy)
24
+ content = PolicyAwareRedactor.call(@source, policy: @policy)
25
+ Redaction::PublicResult.call(@source, content, @policy)
26
+ end
27
+ end
28
+ end
29
+ end