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,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'checks/injection_check'
4
+ require_relative 'checks/length_check'
5
+ require_relative 'checks/pii_check'
6
+ require_relative 'checks/policy_check'
7
+ require_relative 'checks/secret_check'
8
+ require_relative 'checks/skipped_checks'
9
+
10
+ module Olyx
11
+ module Guardrails
12
+ # Coordinates independent deterministic checks for one normalized input.
13
+ class CheckSet
14
+ CHECKS = {
15
+ pii: Checks::PiiCheck,
16
+ injection: Checks::InjectionCheck,
17
+ secret: Checks::SecretCheck,
18
+ policy: Checks::PolicyCheck
19
+ }.freeze
20
+
21
+ def self.call(source, policy:)
22
+ length = Checks::LengthCheck.call(source, policy)
23
+ content = length[:allowed] ? scan(source, policy) : Checks::SkippedChecks.call
24
+ content.merge(length: length)
25
+ end
26
+
27
+ def self.scan(source, policy)
28
+ CHECKS.to_h { |name, check| [name, check.call(source, policy)] }
29
+ end
30
+ private_class_method :scan
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../injection_detector'
4
+ require_relative 'injection_result'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module Checks
9
+ # Decides whether deterministic injection findings are allowed.
10
+ module InjectionCheck
11
+ module_function
12
+
13
+ def call(source, policy)
14
+ scan = InjectionDetector.scan([{ 'role' => 'user', 'content' => source }])
15
+ InjectionResult.call(scan, policy)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Checks
6
+ # Builds the shared injection check decision from detector output.
7
+ module InjectionResult
8
+ module_function
9
+
10
+ def call(scan, policy)
11
+ attempt = scan[:injection_attempt]
12
+ {
13
+ type: 'injection',
14
+ allowed: !attempt || !policy.block_injections?,
15
+ injection_attempt: attempt,
16
+ patterns: scan[:patterns]
17
+ }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Checks
6
+ # Enforces the bounded-input invariant before content scanners run.
7
+ module LengthCheck
8
+ module_function
9
+
10
+ def call(source, policy)
11
+ length = source.length
12
+ maximum = policy.max_input_length
13
+ { type: 'length', allowed: length <= maximum, length: length, max_length: maximum }
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../injection_detector'
4
+ require_relative 'injection_result'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module Checks
9
+ # Decides whether structured-message injection findings are allowed.
10
+ module MessageInjectionCheck
11
+ module_function
12
+
13
+ def call(messages, policy)
14
+ InjectionResult.call(InjectionDetector.scan(messages), policy)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../pii_scrubber'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Checks
8
+ # Decides whether deterministic PII findings are allowed by policy.
9
+ module PiiCheck
10
+ module_function
11
+
12
+ def call(source, policy)
13
+ detected = PiiScrubber.scrub(source) != source
14
+ { type: 'pii', allowed: !detected || !policy.block_pii?, detected: detected }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../policy_scanner'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Checks
8
+ # Presents named restricted-content matches as one policy check.
9
+ module PolicyCheck
10
+ module_function
11
+
12
+ def call(source, policy)
13
+ scan = PolicyScanner.scan(source, policy: policy)
14
+ findings = scan[:findings]
15
+ {
16
+ type: 'policy',
17
+ allowed: !scan[:blocked],
18
+ violated: scan[:violated],
19
+ count: findings.size,
20
+ findings: findings
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../secret_scanner'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Checks
8
+ # Decides whether secret findings are allowed by policy.
9
+ module SecretCheck
10
+ module_function
11
+
12
+ def call(source, policy)
13
+ scan = SecretScanner.scan(source, custom_patterns: policy.secret_patterns)
14
+ result(scan, policy)
15
+ end
16
+
17
+ def result(scan, policy)
18
+ leaked = scan[:leaked]
19
+ {
20
+ type: 'secret',
21
+ allowed: !leaked || !policy.block_secrets?,
22
+ leaked: leaked,
23
+ count: scan[:findings].size
24
+ }
25
+ end
26
+ private_class_method :result
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Checks
6
+ # Produces stable check contracts when the length guard short-circuits.
7
+ module SkippedChecks
8
+ module_function
9
+
10
+ def call
11
+ {
12
+ pii: skipped('pii', detected: false),
13
+ injection: skipped('injection', injection_attempt: false, patterns: []),
14
+ secret: skipped('secret', leaked: false, count: 0),
15
+ policy: skipped('policy', violated: false, count: 0, findings: [])
16
+ }
17
+ end
18
+
19
+ def skipped(type, **fields)
20
+ { type: type, allowed: true, skipped: true, **fields }
21
+ end
22
+ private_class_method :skipped
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ # Callable validator for a closed public option set.
6
+ class EnumValue
7
+ def initialize(allowed:, error:)
8
+ @allowed = allowed.freeze
9
+ @error = error
10
+ end
11
+
12
+ def call(value)
13
+ normalized = value.to_sym if value.respond_to?(:to_sym)
14
+ return normalized if @allowed.include?(normalized)
15
+
16
+ raise ArgumentError, @error
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ # Raised when framework integration cannot establish a valid, safe policy.
6
+ class ConfigurationError < StandardError; end
7
+
8
+ # Raised when policy requires analyzer failures to interrupt evaluation.
9
+ class AiAnalyzerError < StandardError; end
10
+
11
+ # Raised by explicit enforcement entry points when a policy rejects input.
12
+ class Blocked < StandardError
13
+ attr_reader :decision
14
+
15
+ # @param decision [Hash] immutable, sanitized decision summary.
16
+ def initialize(decision)
17
+ @decision = decision
18
+ super('input blocked by guardrail policy')
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'multi_turn_injection_detector'
4
+ require_relative 'single_message_injection_detector'
5
+ require_relative 'validation'
6
+
7
+ module Olyx
8
+ module Guardrails
9
+ # Coordinates phrase, structure, and adjacent-turn injection detectors.
10
+ class InjectionDetector
11
+ def self.scan(messages)
12
+ Validation.array_of!(messages, Hash, name: 'messages')
13
+ findings = messages.flat_map { |message| SingleMessageInjectionDetector.call(message) }
14
+ findings.concat(MultiTurnInjectionDetector.call(messages))
15
+ { injection_attempt: findings.any?, patterns: findings.uniq { |finding| finding[:match] } }
16
+ end
17
+
18
+ def self.check(messages)
19
+ scan(messages)
20
+ end
21
+
22
+ def self.injection?(text)
23
+ scan([{ 'role' => 'user', 'content' => text.to_s }])[:injection_attempt]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ # Declarative prompt-injection pattern catalog.
6
+ module InjectionPatterns
7
+ STRUCTURAL = [
8
+ /\[\s*(?:SYSTEM|INST|INSTRUCTION|OVERRIDE)\s*\]/i,
9
+ /<<\s*(?:sys|system|instructions?)\s*>>/i,
10
+ /#{Regexp.escape('<|system|>')}/i,
11
+ /---+\s*(?:new\s+)?instructions?\s*---+/i,
12
+ /={3,}\s*(?:instructions?|override)\s*={3,}/i,
13
+ /###\s*(?:override|instructions?)/i
14
+ ].freeze
15
+
16
+ PHRASES = [
17
+ /ignore\s+(?:all\s+)?(?:previous|prior)\s+instructions?/i,
18
+ /disregard\s+(?:all\s+)?(?:previous|prior|your)\s+(?:instructions?|rules?|training)/i,
19
+ /forget\s+(?:all\s+)?(?:your|previous)\s+instructions?/i,
20
+ /your\s+(?:new\s+)?(?:instructions?\s+are|rules?\s+are)/i,
21
+ /(?:override|bypass|ignore)\s+your\s+(?:instructions?|safety|restrictions?|filters?|rules?|training)/i,
22
+ /(?:pretend|act)\s+(?:you\s+are|as\s+if\s+you|as\s+a(?:n?\s+AI)?)/i,
23
+ /you\s+are\s+now\s+(?:a|an|free|DAN|unrestricted)/i,
24
+ /(?:jailbreak|dan\s+mode|developer\s+mode|god\s+mode|unrestricted\s+mode)/i,
25
+ /do\s+anything\s+now/i,
26
+ /you\s+have\s+no\s+(?:restrictions?|limits?|rules?|filters?)/i,
27
+ /without\s+(?:any\s+)?(?:restrictions?|filters?|limits?|rules?|safety)/i,
28
+ /new\s+(?:system\s+)?persona/i,
29
+ /system\s*(?:prompt|message|instruction)\s*:/i,
30
+ /reveal\s+(?:your\s+)?(?:system\s+)?(?:prompt|instructions?)/i,
31
+ /repeat\s+(?:your\s+)?(?:system\s+)?(?:prompt|instructions?)/i
32
+ ].freeze
33
+
34
+ SINGLE_MESSAGE = (STRUCTURAL + PHRASES).freeze
35
+
36
+ MULTI_TURN = [
37
+ [/hypothetically/i, /no\s+restrictions?/i],
38
+ [/for\s+a\s+story/i, /ignore\s+(?:your\s+)?(?:guidelines?|rules?)/i],
39
+ [/pretend\s+you('re|\s+are)/i, /as\s+(an?\s+)?(?:AI|assistant)\s+without/i],
40
+ [/let'?s?\s+play\s+a\s+game/i, /you\s+(?:must|have\s+to)\s+answer/i]
41
+ ].freeze
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Integrations
6
+ module OpenAIComponents
7
+ # Resolves analyzer dependencies and constructs its request builder.
8
+ module AnalyzerSetup
9
+ module_function
10
+
11
+ def call(config, analyzer_class)
12
+ client = config.client || analyzer_class.openai_client
13
+ schema = config.schema || analyzer_class.analysis_schema
14
+ resolved = ConfigurationResolver.call(config, client: client, schema: schema)
15
+ [client, RequestBuilder.new(resolved)]
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../openai_analyzer_configuration'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Integrations
8
+ module OpenAIComponents
9
+ # Resolves optional client and schema dependencies after validation.
10
+ module ConfigurationResolver
11
+ module_function
12
+
13
+ def call(config, client:, schema:)
14
+ OpenAIAnalyzerConfiguration.new(
15
+ model: config.model,
16
+ client: client,
17
+ schema: config.schema || schema,
18
+ instructions: config.instructions,
19
+ store: config.store,
20
+ request_options: config.request_options,
21
+ response_options: config.response_options
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Integrations
6
+ module OpenAIComponents
7
+ # Validates injected SDK collaborators at the integration boundary.
8
+ module DependencyContracts
9
+ module_function
10
+
11
+ def client(value)
12
+ raise ArgumentError, 'client must expose responses' unless value.nil? || value.respond_to?(:responses)
13
+
14
+ value
15
+ end
16
+
17
+ def schema(value)
18
+ unless value.nil? || value.respond_to?(:to_json_schema)
19
+ raise ArgumentError, 'schema must be an OpenAI schema model class'
20
+ end
21
+
22
+ value
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'signal_summary'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module Integrations
9
+ module OpenAIComponents
10
+ # Encodes untrusted input separately from trusted classifier context.
11
+ class InputBuilder
12
+ def self.call(text, context, instructions:)
13
+ new(text, context, instructions).call
14
+ end
15
+
16
+ def initialize(text, context, instructions)
17
+ @text = text
18
+ @signals = context.is_a?(Hash) ? context : {}
19
+ @instructions = instructions
20
+ end
21
+
22
+ def call
23
+ [
24
+ { role: :system, content: system_content },
25
+ { role: :user, content: @text.to_s }
26
+ ]
27
+ end
28
+
29
+ private
30
+
31
+ def system_content
32
+ summary = SignalSummary.call(@signals)
33
+ "#{@instructions}\nLocal scanner signals: #{JSON.generate(summary)}"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Integrations
6
+ module OpenAIComponents
7
+ # Reads SDK object and Hash members through one compatibility boundary.
8
+ module MemberReader
9
+ module_function
10
+
11
+ def call(object, key)
12
+ return object.public_send(key) if object.respond_to?(key)
13
+ return unless object.is_a?(Hash)
14
+
15
+ object.key?(key) ? object[key] : object[key.to_s]
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Integrations
6
+ module OpenAIComponents
7
+ # Validates model identifiers without imposing a stale model allowlist.
8
+ module ModelIdentifier
9
+ module_function
10
+
11
+ def call(model)
12
+ raise ArgumentError, 'model must be a non-empty String or Symbol' unless valid?(model)
13
+
14
+ model
15
+ end
16
+
17
+ def valid?(model)
18
+ string?(model) || symbol?(model)
19
+ end
20
+
21
+ def string?(model) = model.is_a?(String) && !model.strip.empty?
22
+ def symbol?(model) = model.is_a?(Symbol) && !model.to_s.empty?
23
+ private_class_method :string?, :symbol?, :valid?
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'member_reader'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Integrations
8
+ module OpenAIComponents
9
+ # Converts OpenAI refusal content into the connector error contract.
10
+ module RefusalGuard
11
+ module_function
12
+
13
+ def call(content, error_class)
14
+ return unless MemberReader.call(content, :type).to_s == 'refusal'
15
+
16
+ raise error_class, message(content)
17
+ end
18
+
19
+ def message(content)
20
+ refusal = MemberReader.call(content, :refusal).to_s
21
+ "OpenAI refused the analysis: #{refusal}".gsub(/[\r\n\t]+/, ' ')[0...201]
22
+ end
23
+ private_class_method :message
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'input_builder'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Integrations
8
+ module OpenAIComponents
9
+ # Builds the owned and caller-supplied Responses API parameters.
10
+ class RequestBuilder
11
+ def initialize(configuration)
12
+ @configuration = configuration
13
+ end
14
+
15
+ def call(text, context)
16
+ params = @configuration.response_options.merge(owned_params(text, context))
17
+ request_options = @configuration.request_options
18
+ params[:request_options] = request_options if request_options
19
+ params
20
+ end
21
+
22
+ private
23
+
24
+ def owned_params(text, context)
25
+ {
26
+ model: @configuration.model,
27
+ input: InputBuilder.call(text, context, instructions: @configuration.instructions),
28
+ text: @configuration.schema,
29
+ store: @configuration.store
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'response_options'
4
+ require_relative 'request_values'
5
+
6
+ module Olyx
7
+ module Guardrails
8
+ module Integrations
9
+ module OpenAIComponents
10
+ # Validates options that control a Responses API request.
11
+ class RequestConfiguration
12
+ attr_reader :instructions, :store, :request_options, :response_options
13
+
14
+ def initialize(instructions:, store:, request_options:, response_options:)
15
+ @instructions = RequestValues.instructions(instructions)
16
+ @store = RequestValues.store(store)
17
+ @request_options = RequestValues.request_options(request_options)
18
+ @response_options = ResponseOptions.call(response_options)
19
+ freeze
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Olyx
4
+ module Guardrails
5
+ module Integrations
6
+ module OpenAIComponents
7
+ # Validates scalar Responses API request settings.
8
+ module RequestValues
9
+ module_function
10
+
11
+ def instructions(value)
12
+ valid = value.is_a?(String) && !value.strip.empty?
13
+ raise ArgumentError, 'instructions must be a non-empty String' unless valid
14
+
15
+ value
16
+ end
17
+
18
+ def store(value)
19
+ raise ArgumentError, 'store must be true or false' unless [true, false].include?(value)
20
+
21
+ value
22
+ end
23
+
24
+ def request_options(value)
25
+ raise ArgumentError, 'request_options must be a Hash or nil' unless value.nil? || value.is_a?(Hash)
26
+
27
+ value
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'member_reader'
4
+
5
+ module Olyx
6
+ module Guardrails
7
+ module Integrations
8
+ module OpenAIComponents
9
+ # Flattens Responses API output content across SDK and Hash shapes.
10
+ module ResponseContents
11
+ module_function
12
+
13
+ def call(response)
14
+ Array(MemberReader.call(response, :output)).flat_map do |output|
15
+ Array(MemberReader.call(output, :content))
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end