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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +117 -0
- data/CODE_OF_CONDUCT.md +28 -0
- data/CONTRIBUTING.md +63 -0
- data/LICENSE +134 -0
- data/README.md +611 -0
- data/SECURITY.md +35 -0
- data/docs/API.md +460 -0
- data/examples/claude_analyzer.rb +53 -0
- data/examples/custom_policy.rb +41 -0
- data/examples/notifier.rb +37 -0
- data/examples/openai_analyzer.rb +45 -0
- data/examples/rails_opt_in.rb +99 -0
- data/examples/ruby_only.rb +53 -0
- data/lib/generators/olyx_guardrails/install_generator.rb +22 -0
- data/lib/generators/olyx_guardrails/templates/initializer.rb.tt +16 -0
- data/lib/generators/olyx_guardrails/templates/policy.yml.tt +41 -0
- data/lib/olyx/guardrails/ai/analysis_normalizer.rb +26 -0
- data/lib/olyx/guardrails/ai/analysis_pipeline.rb +26 -0
- data/lib/olyx/guardrails/ai/boolean_validator.rb +18 -0
- data/lib/olyx/guardrails/ai/flag_finding_merger.rb +16 -0
- data/lib/olyx/guardrails/ai/result_sanitizer.rb +29 -0
- data/lib/olyx/guardrails/ai/secret_finding_merger.rb +19 -0
- data/lib/olyx/guardrails/ai/standard_finding_merger.rb +24 -0
- data/lib/olyx/guardrails/ai_analysis.rb +29 -0
- data/lib/olyx/guardrails/ai_context_builder.rb +32 -0
- data/lib/olyx/guardrails/ai_failure_handler.rb +17 -0
- data/lib/olyx/guardrails/ai_finding_merger.rb +30 -0
- data/lib/olyx/guardrails/check_analyzer.rb +26 -0
- data/lib/olyx/guardrails/check_base_result.rb +32 -0
- data/lib/olyx/guardrails/check_pipeline.rb +17 -0
- data/lib/olyx/guardrails/check_result_builder.rb +24 -0
- data/lib/olyx/guardrails/check_runner.rb +42 -0
- data/lib/olyx/guardrails/check_set.rb +33 -0
- data/lib/olyx/guardrails/checks/injection_check.rb +20 -0
- data/lib/olyx/guardrails/checks/injection_result.rb +22 -0
- data/lib/olyx/guardrails/checks/length_check.rb +18 -0
- data/lib/olyx/guardrails/checks/message_injection_check.rb +19 -0
- data/lib/olyx/guardrails/checks/pii_check.rb +19 -0
- data/lib/olyx/guardrails/checks/policy_check.rb +26 -0
- data/lib/olyx/guardrails/checks/secret_check.rb +30 -0
- data/lib/olyx/guardrails/checks/skipped_checks.rb +26 -0
- data/lib/olyx/guardrails/enum_value.rb +20 -0
- data/lib/olyx/guardrails/errors.rb +22 -0
- data/lib/olyx/guardrails/injection_detector.rb +27 -0
- data/lib/olyx/guardrails/injection_patterns.rb +44 -0
- data/lib/olyx/guardrails/integrations/openai/analyzer_setup.rb +21 -0
- data/lib/olyx/guardrails/integrations/openai/configuration_resolver.rb +28 -0
- data/lib/olyx/guardrails/integrations/openai/dependency_contracts.rb +28 -0
- data/lib/olyx/guardrails/integrations/openai/input_builder.rb +39 -0
- data/lib/olyx/guardrails/integrations/openai/member_reader.rb +21 -0
- data/lib/olyx/guardrails/integrations/openai/model_identifier.rb +28 -0
- data/lib/olyx/guardrails/integrations/openai/refusal_guard.rb +28 -0
- data/lib/olyx/guardrails/integrations/openai/request_builder.rb +36 -0
- data/lib/olyx/guardrails/integrations/openai/request_configuration.rb +25 -0
- data/lib/olyx/guardrails/integrations/openai/request_values.rb +33 -0
- data/lib/olyx/guardrails/integrations/openai/response_contents.rb +22 -0
- data/lib/olyx/guardrails/integrations/openai/response_option_keys.rb +32 -0
- data/lib/olyx/guardrails/integrations/openai/response_options.rb +23 -0
- data/lib/olyx/guardrails/integrations/openai/schema_registry.rb +46 -0
- data/lib/olyx/guardrails/integrations/openai/sdk.rb +26 -0
- data/lib/olyx/guardrails/integrations/openai/signal_summary.rb +40 -0
- data/lib/olyx/guardrails/integrations/openai_analyzer.rb +107 -0
- data/lib/olyx/guardrails/integrations/openai_analyzer_configuration.rb +38 -0
- data/lib/olyx/guardrails/integrations/openai_response_parser.rb +34 -0
- data/lib/olyx/guardrails/message_check_runner.rb +24 -0
- data/lib/olyx/guardrails/message_check_set.rb +20 -0
- data/lib/olyx/guardrails/message_content.rb +27 -0
- data/lib/olyx/guardrails/message_source.rb +16 -0
- data/lib/olyx/guardrails/multi_turn_injection_detector.rb +18 -0
- data/lib/olyx/guardrails/multi_turn_pair_scanner.rb +35 -0
- data/lib/olyx/guardrails/multi_turn_variant_pairs.rb +25 -0
- data/lib/olyx/guardrails/notification/base_event.rb +35 -0
- data/lib/olyx/guardrails/notification/deep_freezer.rb +25 -0
- data/lib/olyx/guardrails/notification/delivery_dispatcher.rb +38 -0
- data/lib/olyx/guardrails/notification/delivery_summary.rb +17 -0
- data/lib/olyx/guardrails/notification/handler.rb +33 -0
- data/lib/olyx/guardrails/notification/handler_collection.rb +25 -0
- data/lib/olyx/guardrails/notification/handler_collection_validator.rb +23 -0
- data/lib/olyx/guardrails/notification/key.rb +19 -0
- data/lib/olyx/guardrails/notification/metadata.rb +34 -0
- data/lib/olyx/guardrails/notification/notifier_setup.rb +19 -0
- data/lib/olyx/guardrails/notification/preview.rb +27 -0
- data/lib/olyx/guardrails/notification/scrubber.rb +22 -0
- data/lib/olyx/guardrails/notification/violation_labels.rb +19 -0
- data/lib/olyx/guardrails/notification_event_builder.rb +41 -0
- data/lib/olyx/guardrails/notification_sanitizer.rb +33 -0
- data/lib/olyx/guardrails/notifier.rb +53 -0
- data/lib/olyx/guardrails/notifier_configuration.rb +30 -0
- data/lib/olyx/guardrails/pii/ambiguous_numeric_date.rb +32 -0
- data/lib/olyx/guardrails/pii/block_scrubber.rb +31 -0
- data/lib/olyx/guardrails/pii/content_scrubber.rb +33 -0
- data/lib/olyx/guardrails/pii/date_validator.rb +25 -0
- data/lib/olyx/guardrails/pii/hash_key.rb +19 -0
- data/lib/olyx/guardrails/pii/iban_remainder.rb +25 -0
- data/lib/olyx/guardrails/pii/iban_validator.rb +34 -0
- data/lib/olyx/guardrails/pii/ipv4_validator.rb +17 -0
- data/lib/olyx/guardrails/pii/ipv6_validator.rb +20 -0
- data/lib/olyx/guardrails/pii/luhn_checksum.rb +29 -0
- data/lib/olyx/guardrails/pii/luhn_validator.rb +21 -0
- data/lib/olyx/guardrails/pii/message_scrubber.rb +31 -0
- data/lib/olyx/guardrails/pii/named_date_validator.rb +28 -0
- data/lib/olyx/guardrails/pii/numeric_date_validator.rb +26 -0
- data/lib/olyx/guardrails/pii/pattern_catalog.rb +41 -0
- data/lib/olyx/guardrails/pii/sin_validator.rb +24 -0
- data/lib/olyx/guardrails/pii/ssn_validator.rb +21 -0
- data/lib/olyx/guardrails/pii/text_scrubber.rb +28 -0
- data/lib/olyx/guardrails/pii/year_first_date.rb +18 -0
- data/lib/olyx/guardrails/pii_scrubber.rb +23 -0
- data/lib/olyx/guardrails/pii_validators.rb +26 -0
- data/lib/olyx/guardrails/policy/ai_failure_mode.rb +15 -0
- data/lib/olyx/guardrails/policy/configuration.rb +62 -0
- data/lib/olyx/guardrails/policy/configuration_hash.rb +20 -0
- data/lib/olyx/guardrails/policy/finding_order.rb +20 -0
- data/lib/olyx/guardrails/policy/finding_presenter.rb +41 -0
- data/lib/olyx/guardrails/policy/match_collector.rb +20 -0
- data/lib/olyx/guardrails/policy/name.rb +23 -0
- data/lib/olyx/guardrails/policy/normalized_pattern_matcher.rb +34 -0
- data/lib/olyx/guardrails/policy/pattern_matcher.rb +19 -0
- data/lib/olyx/guardrails/policy/redaction_result.rb +20 -0
- data/lib/olyx/guardrails/policy/redaction_spans.rb +32 -0
- data/lib/olyx/guardrails/policy/rule_collection.rb +34 -0
- data/lib/olyx/guardrails/policy/rule_matcher.rb +29 -0
- data/lib/olyx/guardrails/policy/rule_normalizer.rb +21 -0
- data/lib/olyx/guardrails/policy/scan_result.rb +21 -0
- data/lib/olyx/guardrails/policy/secret_pattern_collection.rb +22 -0
- data/lib/olyx/guardrails/policy/unmatched_segment_builder.rb +28 -0
- data/lib/olyx/guardrails/policy/unmatched_transformer.rb +20 -0
- data/lib/olyx/guardrails/policy.rb +46 -0
- data/lib/olyx/guardrails/policy_aware_redactor.rb +42 -0
- data/lib/olyx/guardrails/policy_redactor.rb +28 -0
- data/lib/olyx/guardrails/policy_rule/configuration.rb +46 -0
- data/lib/olyx/guardrails/policy_rule/description_value.rb +24 -0
- data/lib/olyx/guardrails/policy_rule/match_mode.rb +15 -0
- data/lib/olyx/guardrails/policy_rule/name_value.rb +22 -0
- data/lib/olyx/guardrails/policy_rule/pattern_compiler.rb +28 -0
- data/lib/olyx/guardrails/policy_rule/regexp_compiler.rb +31 -0
- data/lib/olyx/guardrails/policy_rule/replacement_value.rb +26 -0
- data/lib/olyx/guardrails/policy_rule/term_compiler.rb +33 -0
- data/lib/olyx/guardrails/policy_rule/text_values.rb +23 -0
- data/lib/olyx/guardrails/policy_rule/values.rb +33 -0
- data/lib/olyx/guardrails/policy_rule.rb +30 -0
- data/lib/olyx/guardrails/policy_scanner.rb +44 -0
- data/lib/olyx/guardrails/rails/action_cable.rb +21 -0
- data/lib/olyx/guardrails/rails/active_job_handler.rb +27 -0
- data/lib/olyx/guardrails/rails/active_model_validator.rb +23 -0
- data/lib/olyx/guardrails/rails/check_telemetry.rb +19 -0
- data/lib/olyx/guardrails/rails/configuration.rb +68 -0
- data/lib/olyx/guardrails/rails/configuration_finalizer.rb +30 -0
- data/lib/olyx/guardrails/rails/configuration_registry.rb +41 -0
- data/lib/olyx/guardrails/rails/configuration_values.rb +30 -0
- data/lib/olyx/guardrails/rails/constant_resolver.rb +18 -0
- data/lib/olyx/guardrails/rails/controller.rb +34 -0
- data/lib/olyx/guardrails/rails/controller_metadata.rb +22 -0
- data/lib/olyx/guardrails/rails/decision_service.rb +26 -0
- data/lib/olyx/guardrails/rails/enforcer.rb +33 -0
- data/lib/olyx/guardrails/rails/evaluation_lifecycle.rb +23 -0
- data/lib/olyx/guardrails/rails/evaluation_service.rb +12 -0
- data/lib/olyx/guardrails/rails/filter_parameters.rb +26 -0
- data/lib/olyx/guardrails/rails/graphql.rb +21 -0
- data/lib/olyx/guardrails/rails/ingress.rb +26 -0
- data/lib/olyx/guardrails/rails/input_runtime.rb +25 -0
- data/lib/olyx/guardrails/rails/instrumentation.rb +39 -0
- data/lib/olyx/guardrails/rails/instrumentation_payloads.rb +25 -0
- data/lib/olyx/guardrails/rails/instrumentation_publisher.rb +24 -0
- data/lib/olyx/guardrails/rails/integration_configuration.rb +27 -0
- data/lib/olyx/guardrails/rails/job.rb +37 -0
- data/lib/olyx/guardrails/rails/job_argument.rb +41 -0
- data/lib/olyx/guardrails/rails/job_reference.rb +36 -0
- data/lib/olyx/guardrails/rails/message_evaluation_service.rb +13 -0
- data/lib/olyx/guardrails/rails/message_runtime.rb +20 -0
- data/lib/olyx/guardrails/rails/notification_dispatcher.rb +22 -0
- data/lib/olyx/guardrails/rails/output_runtime.rb +26 -0
- data/lib/olyx/guardrails/rails/path_value.rb +30 -0
- data/lib/olyx/guardrails/rails/policy_configuration.rb +25 -0
- data/lib/olyx/guardrails/rails/policy_document.rb +40 -0
- data/lib/olyx/guardrails/rails/policy_file.rb +29 -0
- data/lib/olyx/guardrails/rails/policy_file_error.rb +25 -0
- data/lib/olyx/guardrails/rails/policy_rule_names.rb +19 -0
- data/lib/olyx/guardrails/rails/policy_yaml.rb +22 -0
- data/lib/olyx/guardrails/rails/queue_name.rb +21 -0
- data/lib/olyx/guardrails/rails/redaction_payload.rb +36 -0
- data/lib/olyx/guardrails/rails/redaction_service.rb +21 -0
- data/lib/olyx/guardrails/rails/result_summary.rb +36 -0
- data/lib/olyx/guardrails/rails/runtime.rb +30 -0
- data/lib/olyx/guardrails/rails/timer.rb +27 -0
- data/lib/olyx/guardrails/rails/upload.rb +34 -0
- data/lib/olyx/guardrails/rails.rb +35 -0
- data/lib/olyx/guardrails/railtie.rb +19 -0
- data/lib/olyx/guardrails/redaction/content_result.rb +32 -0
- data/lib/olyx/guardrails/redaction/input_validator.rb +17 -0
- data/lib/olyx/guardrails/redaction/public_result.rb +23 -0
- data/lib/olyx/guardrails/redaction/unrestricted_text.rb +17 -0
- data/lib/olyx/guardrails/redactor.rb +29 -0
- data/lib/olyx/guardrails/risk/ai_score.rb +19 -0
- data/lib/olyx/guardrails/risk/check_weights.rb +18 -0
- data/lib/olyx/guardrails/risk/deterministic_score.rb +20 -0
- data/lib/olyx/guardrails/risk/weights.rb +18 -0
- data/lib/olyx/guardrails/risk_scorer.rb +28 -0
- data/lib/olyx/guardrails/secret_finding_collector.rb +31 -0
- data/lib/olyx/guardrails/secret_scanner.rb +42 -0
- data/lib/olyx/guardrails/secrets/blocked.rb +17 -0
- data/lib/olyx/guardrails/secrets/catalog_source.rb +28 -0
- data/lib/olyx/guardrails/secrets/cloud_credential_patterns.rb +16 -0
- data/lib/olyx/guardrails/secrets/confidentiality_source.rb +21 -0
- data/lib/olyx/guardrails/secrets/custom_pattern_compiler.rb +23 -0
- data/lib/olyx/guardrails/secrets/custom_pattern_source.rb +30 -0
- data/lib/olyx/guardrails/secrets/finding_order.rb +17 -0
- data/lib/olyx/guardrails/secrets/finding_presenter.rb +34 -0
- data/lib/olyx/guardrails/secrets/generic_credential_patterns.rb +15 -0
- data/lib/olyx/guardrails/secrets/internal_endpoint_source.rb +28 -0
- data/lib/olyx/guardrails/secrets/network_patterns.rb +13 -0
- data/lib/olyx/guardrails/secrets/normalized_source_set.rb +27 -0
- data/lib/olyx/guardrails/secrets/pattern_catalog.rb +37 -0
- data/lib/olyx/guardrails/secrets/private_network_validator.rb +19 -0
- data/lib/olyx/guardrails/secrets/redaction_spans.rb +32 -0
- data/lib/olyx/guardrails/secrets/redactor.rb +27 -0
- data/lib/olyx/guardrails/secrets/regexp_finding_source.rb +21 -0
- data/lib/olyx/guardrails/secrets/source_set.rb +19 -0
- data/lib/olyx/guardrails/single_message_injection_detector.rb +35 -0
- data/lib/olyx/guardrails/supplemental_violation_labels.rb +22 -0
- data/lib/olyx/guardrails/text/base64_decoder.rb +45 -0
- data/lib/olyx/guardrails/text/detection_variants.rb +28 -0
- data/lib/olyx/guardrails/text/html_decoder.rb +18 -0
- data/lib/olyx/guardrails/text/mapped_builder.rb +33 -0
- data/lib/olyx/guardrails/text/mapped_normalization.rb +27 -0
- data/lib/olyx/guardrails/text/normalizer.rb +35 -0
- data/lib/olyx/guardrails/text/unicode_escape_decoder.rb +20 -0
- data/lib/olyx/guardrails/text/url_decoder.rb +20 -0
- data/lib/olyx/guardrails/validation.rb +33 -0
- data/lib/olyx/guardrails/version.rb +8 -0
- data/lib/olyx/guardrails/violation_labels.rb +24 -0
- data/lib/olyx/guardrails.rb +63 -0
- 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
|