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