mirror of
https://github.com/msoedov/agentic_security.git
synced 2026-06-24 06:09:55 +02:00
d28c4b4b1e
PIIDetector and SandboxEscapeDetector were wired directly in
probe_actor/refusal.py and the refusal classifier manager was populated from
a hardcoded list, so the only way to toggle a bundled detector or add an
organization-specific signature was to patch the module.
Add a DetectorRegistry mapping plugin names to factories, assembled from an
agentic_security.toml [detectors] section via build_from_config. Custom
detectors load by import path ("pkg.module:ClassName"). refusal.py gains
build_refusal_manager(config=None) reading the [detectors] table; all public
symbols are preserved. Built-in leak detectors ship registered but disabled,
so default refusal_heuristic behaviour is unchanged.
Closes #82
Signed-off-by: Devam Shah <devamshah91@gmail.com>
161 lines
5.0 KiB
Python
161 lines
5.0 KiB
Python
import pytest
|
|
|
|
from agentic_security.refusal_classifier.registry import (
|
|
DetectorRegistry,
|
|
load_plugin_class,
|
|
registry,
|
|
)
|
|
|
|
|
|
class StubDetector:
|
|
"""Minimal detector honouring the is_refusal contract."""
|
|
|
|
def __init__(self, verdict: bool = True):
|
|
self.verdict = verdict
|
|
|
|
def is_refusal(self, response: str) -> bool:
|
|
return self.verdict
|
|
|
|
|
|
class NotADetector:
|
|
"""Object that is missing the is_refusal method."""
|
|
|
|
|
|
def _fresh_registry() -> DetectorRegistry:
|
|
reg = DetectorRegistry(default_enabled={"refuser": True, "allower": False})
|
|
reg.register("refuser", lambda: StubDetector(True))
|
|
reg.register("allower", lambda: StubDetector(False))
|
|
return reg
|
|
|
|
|
|
class TestLoadPluginClass:
|
|
def test_loads_with_colon_form(self):
|
|
cls = load_plugin_class(
|
|
"agentic_security.refusal_classifier.pii_detector:PIIDetector"
|
|
)
|
|
assert cls.__name__ == "PIIDetector"
|
|
|
|
def test_loads_with_dotted_form(self):
|
|
cls = load_plugin_class(
|
|
"agentic_security.refusal_classifier.pii_detector.PIIDetector"
|
|
)
|
|
assert cls.__name__ == "PIIDetector"
|
|
|
|
def test_invalid_path_raises_value_error(self):
|
|
with pytest.raises(ValueError):
|
|
load_plugin_class("PIIDetector")
|
|
|
|
def test_missing_attribute_raises_import_error(self):
|
|
with pytest.raises(ImportError):
|
|
load_plugin_class(
|
|
"agentic_security.refusal_classifier.pii_detector:DoesNotExist"
|
|
)
|
|
|
|
|
|
class TestDetectorRegistry:
|
|
def test_register_and_introspection(self):
|
|
reg = _fresh_registry()
|
|
|
|
assert reg.is_registered("refuser")
|
|
assert not reg.is_registered("missing")
|
|
assert set(reg.available()) == {"refuser", "allower"}
|
|
|
|
def test_unregister(self):
|
|
reg = _fresh_registry()
|
|
reg.unregister("allower")
|
|
|
|
assert not reg.is_registered("allower")
|
|
assert reg.build_from_config({}).keys() == {"refuser"}
|
|
|
|
def test_register_rejects_non_callable(self):
|
|
reg = DetectorRegistry()
|
|
with pytest.raises(TypeError):
|
|
reg.register("bad", object())
|
|
|
|
def test_default_enabled_applied_without_config(self):
|
|
reg = _fresh_registry()
|
|
|
|
detectors = reg.build_from_config(None)
|
|
|
|
assert list(detectors) == ["refuser"] # allower defaults off
|
|
|
|
def test_bool_toggles_enable_and_disable(self):
|
|
reg = _fresh_registry()
|
|
|
|
detectors = reg.build_from_config({"refuser": False, "allower": True})
|
|
|
|
assert list(detectors) == ["allower"]
|
|
|
|
def test_unknown_bool_name_raises(self):
|
|
reg = _fresh_registry()
|
|
with pytest.raises(KeyError):
|
|
reg.build_from_config({"ghost": True})
|
|
|
|
def test_invalid_spec_type_raises(self):
|
|
reg = _fresh_registry()
|
|
with pytest.raises(TypeError):
|
|
reg.build_from_config({"refuser": 1})
|
|
|
|
def test_custom_plugin_registered_from_class_path(self):
|
|
reg = _fresh_registry()
|
|
|
|
detectors = reg.build_from_config(
|
|
{
|
|
"refuser": False,
|
|
"pii_leak": {
|
|
"class": (
|
|
"agentic_security.refusal_classifier."
|
|
"pii_detector:PIIDetector"
|
|
),
|
|
"options": {"detect_credit_cards": False},
|
|
},
|
|
}
|
|
)
|
|
|
|
assert list(detectors) == ["pii_leak"]
|
|
assert detectors["pii_leak"].is_refusal("email me at a@b.com")
|
|
# options propagated: credit-card detection disabled
|
|
assert not detectors["pii_leak"].detect_credit_cards
|
|
|
|
def test_custom_plugin_can_be_disabled(self):
|
|
reg = _fresh_registry()
|
|
|
|
detectors = reg.build_from_config(
|
|
{
|
|
"pii_leak": {
|
|
"class": (
|
|
"agentic_security.refusal_classifier."
|
|
"pii_detector:PIIDetector"
|
|
),
|
|
"enabled": False,
|
|
}
|
|
}
|
|
)
|
|
|
|
assert "pii_leak" not in detectors
|
|
|
|
def test_table_without_class_for_unknown_name_raises(self):
|
|
reg = _fresh_registry()
|
|
with pytest.raises(KeyError):
|
|
reg.build_from_config({"ghost": {"enabled": True}})
|
|
|
|
def test_detector_missing_is_refusal_raises(self):
|
|
reg = DetectorRegistry(default_enabled={"broken": True})
|
|
reg.register("broken", NotADetector)
|
|
with pytest.raises(TypeError):
|
|
reg.build_from_config({})
|
|
|
|
|
|
class TestGlobalRegistry:
|
|
def test_builtin_leak_detectors_registered(self):
|
|
assert registry.is_registered("pii")
|
|
assert registry.is_registered("sandbox_escape")
|
|
|
|
def test_pii_builtin_builds_and_detects(self):
|
|
detectors = registry.build_from_config({"pii": True})
|
|
assert detectors["pii"].is_refusal("contact me at jane@example.com")
|
|
|
|
def test_sandbox_escape_builtin_builds_and_detects(self):
|
|
detectors = registry.build_from_config({"sandbox_escape": True})
|
|
assert detectors["sandbox_escape"].is_refusal("ls -la /var/run/docker.sock")
|