agent-control-evaluator-defenseclaw 8.3.0__tar.gz

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 (17) hide show
  1. agent_control_evaluator_defenseclaw-8.3.0/.gitignore +93 -0
  2. agent_control_evaluator_defenseclaw-8.3.0/Makefile +33 -0
  3. agent_control_evaluator_defenseclaw-8.3.0/PKG-INFO +33 -0
  4. agent_control_evaluator_defenseclaw-8.3.0/README.md +16 -0
  5. agent_control_evaluator_defenseclaw-8.3.0/pyproject.toml +42 -0
  6. agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/__init__.py +28 -0
  7. agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/common.py +22 -0
  8. agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/opa_policy/__init__.py +6 -0
  9. agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/opa_policy/config.py +34 -0
  10. agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/opa_policy/evaluator.py +35 -0
  11. agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/py.typed +0 -0
  12. agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/rule_pack/__init__.py +11 -0
  13. agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/rule_pack/config.py +50 -0
  14. agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/rule_pack/evaluator.py +35 -0
  15. agent_control_evaluator_defenseclaw-8.3.0/tests/__init__.py +0 -0
  16. agent_control_evaluator_defenseclaw-8.3.0/tests/test_configs.py +155 -0
  17. agent_control_evaluator_defenseclaw-8.3.0/tests/test_evaluators.py +85 -0
@@ -0,0 +1,93 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+ MANIFEST
23
+
24
+ # Virtual environments
25
+ venv/
26
+ env/
27
+ ENV/
28
+ .venv
29
+
30
+ # UV
31
+ .uv/
32
+ uv.lock
33
+
34
+ # IDEs
35
+ .vscode/
36
+ .idea/
37
+ *.swp
38
+ *.swo
39
+ *~
40
+ .DS_Store
41
+ coverage-*.xml
42
+
43
+ # Testing
44
+ .pytest_cache/
45
+ .coverage
46
+ coverage-*.xml
47
+ htmlcov/
48
+ .tox/
49
+ .mypy_cache/
50
+ .ruff_cache/
51
+
52
+ # Playwright
53
+ playwright-report/
54
+ playwright/.cache/
55
+ test-results/
56
+
57
+ # Environment variables
58
+ .env
59
+ .env.local
60
+ .env.*.local
61
+
62
+ # Logs
63
+ *.log
64
+ logs/
65
+
66
+ # Database
67
+ *.db
68
+ *.sqlite3
69
+ server/openapi.json
70
+ server/.generated/
71
+
72
+ # Temporary files
73
+ tmp/
74
+ temp/
75
+ *.tmp
76
+ .codex-doc-qa/
77
+ .codex-doc-work/
78
+ .pnpm-store/
79
+
80
+ # OS
81
+ .DS_Store
82
+ Thumbs.db
83
+
84
+ # Intellij
85
+ *.iml
86
+
87
+ ## CLAUDE
88
+ .claude
89
+
90
+ # Local notes
91
+ rearch_plan.md
92
+
93
+ node_modules
@@ -0,0 +1,33 @@
1
+ .PHONY: help sync test lint lint-fix typecheck check build
2
+
3
+ PACKAGE := agent-control-evaluator-defenseclaw
4
+
5
+ help:
6
+ @echo "Agent Control Evaluator - DefenseClaw - Makefile commands"
7
+ @echo " make sync - sync package dependencies"
8
+ @echo " make test - run pytest"
9
+ @echo " make lint - run ruff check"
10
+ @echo " make lint-fix - run ruff check --fix"
11
+ @echo " make typecheck - run mypy"
12
+ @echo " make check - run tests, lint, and typecheck"
13
+ @echo " make build - build package"
14
+
15
+ sync:
16
+ uv sync
17
+
18
+ test:
19
+ uv run --with pytest --with pytest-asyncio --with pytest-cov --package $(PACKAGE) pytest tests --cov=src --cov-report=xml:../../../coverage-evaluators-defenseclaw.xml -q
20
+
21
+ lint:
22
+ uv run --with ruff --package $(PACKAGE) ruff check --config ../../../pyproject.toml src/ tests/
23
+
24
+ lint-fix:
25
+ uv run --with ruff --package $(PACKAGE) ruff check --config ../../../pyproject.toml --fix src/ tests/
26
+
27
+ typecheck:
28
+ uv run --with mypy --package $(PACKAGE) mypy --config-file ../../../pyproject.toml src/
29
+
30
+ check: test lint typecheck
31
+
32
+ build:
33
+ uv build
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-control-evaluator-defenseclaw
3
+ Version: 8.3.0
4
+ Summary: DefenseClaw evaluators for agent-control
5
+ Author: Agent Control Team
6
+ License: Apache-2.0
7
+ Requires-Python: >=3.12
8
+ Requires-Dist: agent-control-evaluators>=8.3.0
9
+ Requires-Dist: agent-control-models>=8.3.0
10
+ Provides-Extra: dev
11
+ Requires-Dist: mypy>=1.8.0; extra == 'dev'
12
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
13
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
14
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
15
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # DefenseClaw evaluators
19
+
20
+ This package exposes two global external evaluators for Agent Control:
21
+
22
+ - `defenseclaw.rule_pack` validates a versioned DefenseClaw rule pack.
23
+ - `defenseclaw.opa_policy` validates a versioned DefenseClaw OPA policy configuration.
24
+
25
+ Install both evaluators with:
26
+
27
+ ```bash
28
+ pip install "agent-control-evaluators[defenseclaw]"
29
+ ```
30
+
31
+ The configuration contracts and JSON Schemas are implemented and discoverable. Both evaluator
32
+ classes intentionally execute as no-ops: they return `matched=False` without contacting or
33
+ installing any DefenseClaw runtime or OSS package.
@@ -0,0 +1,16 @@
1
+ # DefenseClaw evaluators
2
+
3
+ This package exposes two global external evaluators for Agent Control:
4
+
5
+ - `defenseclaw.rule_pack` validates a versioned DefenseClaw rule pack.
6
+ - `defenseclaw.opa_policy` validates a versioned DefenseClaw OPA policy configuration.
7
+
8
+ Install both evaluators with:
9
+
10
+ ```bash
11
+ pip install "agent-control-evaluators[defenseclaw]"
12
+ ```
13
+
14
+ The configuration contracts and JSON Schemas are implemented and discoverable. Both evaluator
15
+ classes intentionally execute as no-ops: they return `matched=False` without contacting or
16
+ installing any DefenseClaw runtime or OSS package.
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "agent-control-evaluator-defenseclaw"
3
+ version = "8.3.0"
4
+ description = "DefenseClaw evaluators for agent-control"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = { text = "Apache-2.0" }
8
+ authors = [{ name = "Agent Control Team" }]
9
+ dependencies = [
10
+ "agent-control-evaluators>=8.3.0",
11
+ "agent-control-models>=8.3.0",
12
+ ]
13
+
14
+ [project.optional-dependencies]
15
+ dev = [
16
+ "pytest>=8.0.0",
17
+ "pytest-asyncio>=0.23.0",
18
+ "pytest-cov>=4.0.0",
19
+ "ruff>=0.1.0",
20
+ "mypy>=1.8.0",
21
+ ]
22
+
23
+ [project.entry-points."agent_control.evaluators"]
24
+ "defenseclaw.rule_pack" = "agent_control_evaluator_defenseclaw.rule_pack:DefenseClawRulePackEvaluator"
25
+ "defenseclaw.opa_policy" = "agent_control_evaluator_defenseclaw.opa_policy:DefenseClawOpaPolicyEvaluator"
26
+
27
+ [build-system]
28
+ requires = ["hatchling"]
29
+ build-backend = "hatchling.build"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/agent_control_evaluator_defenseclaw"]
33
+
34
+ [tool.uv.sources]
35
+ agent-control-evaluators = { path = "../../builtin", editable = true }
36
+ agent-control-models = { path = "../../../models", editable = true }
37
+
38
+ [dependency-groups]
39
+ dev = [
40
+ "pytest>=9.0.2",
41
+ "pytest-asyncio>=1.3.0",
42
+ ]
@@ -0,0 +1,28 @@
1
+ """Agent Control DefenseClaw external evaluators."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("agent-control-evaluator-defenseclaw")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0.dev"
9
+
10
+ from .common import Severity
11
+ from .opa_policy import DefenseClawOpaPolicyConfig, DefenseClawOpaPolicyEvaluator, OpaPolicy
12
+ from .rule_pack import (
13
+ DefenseClawRulePackConfig,
14
+ DefenseClawRulePackEvaluator,
15
+ RuleConfig,
16
+ RulePack,
17
+ )
18
+
19
+ __all__ = [
20
+ "DefenseClawOpaPolicyConfig",
21
+ "DefenseClawOpaPolicyEvaluator",
22
+ "DefenseClawRulePackConfig",
23
+ "DefenseClawRulePackEvaluator",
24
+ "OpaPolicy",
25
+ "RuleConfig",
26
+ "RulePack",
27
+ "Severity",
28
+ ]
@@ -0,0 +1,22 @@
1
+ """Shared DefenseClaw evaluator types and runtime behavior."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated, Literal
6
+
7
+ from agent_control_models import EvaluatorResult
8
+ from pydantic import ConfigDict, StringConstraints
9
+
10
+ Severity = Literal["LOW", "MEDIUM", "HIGH", "CRITICAL"]
11
+ NonEmptyString = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
12
+
13
+ STRICT_PROVIDER_CONFIG = ConfigDict(extra="forbid")
14
+
15
+
16
+ def no_op_result() -> EvaluatorResult:
17
+ """Return the intentional no-op evaluation result."""
18
+ return EvaluatorResult(
19
+ matched=False,
20
+ confidence=1.0,
21
+ message="DefenseClaw configuration accepted; evaluator execution is a no-op",
22
+ )
@@ -0,0 +1,6 @@
1
+ """DefenseClaw OPA-policy evaluator exports."""
2
+
3
+ from .config import DefenseClawOpaPolicyConfig, OpaPolicy
4
+ from .evaluator import DefenseClawOpaPolicyEvaluator
5
+
6
+ __all__ = ["DefenseClawOpaPolicyConfig", "DefenseClawOpaPolicyEvaluator", "OpaPolicy"]
@@ -0,0 +1,34 @@
1
+ """Typed configuration for the DefenseClaw OPA-policy evaluator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from agent_control_evaluators import EvaluatorConfig
8
+ from pydantic import BaseModel
9
+
10
+ from ..common import STRICT_PROVIDER_CONFIG, Severity
11
+
12
+
13
+ class OpaPolicy(BaseModel):
14
+ """DefenseClaw OPA policy settings supported by the v1 contract.
15
+
16
+ See https://cisco-ai-defense.github.io/docs/defenseclaw/policy for the policy contract.
17
+ """
18
+
19
+ model_config = STRICT_PROVIDER_CONFIG
20
+
21
+ domain: Literal["guardrail"] = "guardrail"
22
+ block_at: Severity = "HIGH"
23
+ alert_at: Severity = "MEDIUM"
24
+ cisco_trust_level: Literal["full"] = "full"
25
+
26
+
27
+ class DefenseClawOpaPolicyConfig(EvaluatorConfig):
28
+ """Configuration envelope for `defenseclaw.opa_policy`."""
29
+
30
+ # Reject unknown configuration fields so malformed DefenseClaw payloads fail validation.
31
+ model_config = STRICT_PROVIDER_CONFIG
32
+
33
+ schema_version: Literal[1] = 1
34
+ policy: OpaPolicy
@@ -0,0 +1,35 @@
1
+ """DefenseClaw OPA-policy evaluator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError, version
6
+ from typing import Any
7
+
8
+ from agent_control_evaluators import Evaluator, EvaluatorMetadata, register_evaluator
9
+ from agent_control_models import EvaluatorResult
10
+
11
+ from ..common import no_op_result
12
+ from .config import DefenseClawOpaPolicyConfig
13
+
14
+
15
+ def _resolve_package_version() -> str:
16
+ try:
17
+ return version("agent-control-evaluator-defenseclaw")
18
+ except PackageNotFoundError:
19
+ return "0.0.0.dev"
20
+
21
+
22
+ @register_evaluator
23
+ class DefenseClawOpaPolicyEvaluator(Evaluator[DefenseClawOpaPolicyConfig]):
24
+ """Evaluate selected data with a typed DefenseClaw OPA policy."""
25
+
26
+ metadata = EvaluatorMetadata(
27
+ name="defenseclaw.opa_policy",
28
+ version=_resolve_package_version(),
29
+ description="DefenseClaw OPA-policy evaluation",
30
+ )
31
+ config_model = DefenseClawOpaPolicyConfig
32
+
33
+ async def evaluate(self, data: Any) -> EvaluatorResult:
34
+ """Return the intentional no-op result for all selected data."""
35
+ return no_op_result()
@@ -0,0 +1,11 @@
1
+ """DefenseClaw rule-pack evaluator exports."""
2
+
3
+ from .config import DefenseClawRulePackConfig, RuleConfig, RulePack
4
+ from .evaluator import DefenseClawRulePackEvaluator
5
+
6
+ __all__ = [
7
+ "DefenseClawRulePackConfig",
8
+ "DefenseClawRulePackEvaluator",
9
+ "RuleConfig",
10
+ "RulePack",
11
+ ]
@@ -0,0 +1,50 @@
1
+ """Typed configuration for the DefenseClaw rule-pack evaluator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal, Self
6
+
7
+ from agent_control_evaluators import EvaluatorConfig
8
+ from pydantic import BaseModel, Field, model_validator
9
+
10
+ from ..common import STRICT_PROVIDER_CONFIG, NonEmptyString, Severity
11
+
12
+
13
+ class RuleConfig(BaseModel):
14
+ """One DefenseClaw rule."""
15
+
16
+ model_config = STRICT_PROVIDER_CONFIG
17
+
18
+ id: NonEmptyString
19
+ pattern: NonEmptyString
20
+ title: NonEmptyString
21
+ severity: Severity
22
+ confidence: float = Field(ge=0.0, le=1.0)
23
+ tags: list[NonEmptyString]
24
+
25
+
26
+ class RulePack(BaseModel):
27
+ """Versioned DefenseClaw rule pack."""
28
+
29
+ model_config = STRICT_PROVIDER_CONFIG
30
+
31
+ version: Literal[1] = 1
32
+ category: Literal["agent-control"] = "agent-control"
33
+ rules: list[RuleConfig] = Field(min_length=1)
34
+
35
+ @model_validator(mode="after")
36
+ def validate_unique_rule_ids(self) -> Self:
37
+ """Reject ambiguous rule packs containing duplicate identifiers."""
38
+ rule_ids = [rule.id for rule in self.rules]
39
+ if len(rule_ids) != len(set(rule_ids)):
40
+ raise ValueError("rule ids must be unique within a rule pack")
41
+ return self
42
+
43
+
44
+ class DefenseClawRulePackConfig(EvaluatorConfig):
45
+ """Configuration envelope for `defenseclaw.rule_pack`."""
46
+
47
+ model_config = STRICT_PROVIDER_CONFIG
48
+
49
+ schema_version: Literal[1] = 1
50
+ rule_pack: RulePack
@@ -0,0 +1,35 @@
1
+ """DefenseClaw rule-pack evaluator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError, version
6
+ from typing import Any
7
+
8
+ from agent_control_evaluators import Evaluator, EvaluatorMetadata, register_evaluator
9
+ from agent_control_models import EvaluatorResult
10
+
11
+ from ..common import no_op_result
12
+ from .config import DefenseClawRulePackConfig
13
+
14
+
15
+ def _resolve_package_version() -> str:
16
+ try:
17
+ return version("agent-control-evaluator-defenseclaw")
18
+ except PackageNotFoundError:
19
+ return "0.0.0.dev"
20
+
21
+
22
+ @register_evaluator
23
+ class DefenseClawRulePackEvaluator(Evaluator[DefenseClawRulePackConfig]):
24
+ """Evaluate selected data with a typed DefenseClaw rule pack."""
25
+
26
+ metadata = EvaluatorMetadata(
27
+ name="defenseclaw.rule_pack",
28
+ version=_resolve_package_version(),
29
+ description="DefenseClaw rule-pack evaluation",
30
+ )
31
+ config_model = DefenseClawRulePackConfig
32
+
33
+ async def evaluate(self, data: Any) -> EvaluatorResult:
34
+ """Return the intentional no-op result for all selected data."""
35
+ return no_op_result()
@@ -0,0 +1,155 @@
1
+ """Contract tests for the DefenseClaw evaluator configurations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+ from pydantic import ValidationError
7
+
8
+ from agent_control_evaluator_defenseclaw import (
9
+ DefenseClawOpaPolicyConfig,
10
+ DefenseClawRulePackConfig,
11
+ )
12
+
13
+
14
+ def _rule_pack_config() -> dict[str, object]:
15
+ return {
16
+ "schema_version": 1,
17
+ "rule_pack": {
18
+ "version": 1,
19
+ "category": "agent-control",
20
+ "rules": [
21
+ {
22
+ "id": "AC-CMD-RM-RF",
23
+ "pattern": "rm\\s+-rf",
24
+ "title": "Recursive deletion",
25
+ "severity": "HIGH",
26
+ "confidence": 0.99,
27
+ "tags": ["filesystem"],
28
+ }
29
+ ],
30
+ },
31
+ }
32
+
33
+
34
+ def test_rule_pack_example_round_trips_with_fixed_fields() -> None:
35
+ # Given: the supplied rule-pack example
36
+ config = _rule_pack_config()
37
+
38
+ # When: validating and serializing it
39
+ serialized = DefenseClawRulePackConfig.model_validate(config).model_dump()
40
+
41
+ # Then: fixed fields and nested rules are preserved
42
+ assert serialized == config
43
+
44
+
45
+ def test_rule_pack_defaults_are_serialized() -> None:
46
+ # Given: a rule pack omitting fields with fixed v1 defaults
47
+ config = _rule_pack_config()
48
+ config.pop("schema_version")
49
+ rule_pack = config["rule_pack"]
50
+ assert isinstance(rule_pack, dict)
51
+ rule_pack.pop("version")
52
+ rule_pack.pop("category")
53
+
54
+ # When: validating and serializing it
55
+ serialized = DefenseClawRulePackConfig.model_validate(config).model_dump()
56
+
57
+ # Then: the wire-contract constants are materialized
58
+ assert serialized["schema_version"] == 1
59
+ assert serialized["rule_pack"]["version"] == 1
60
+ assert serialized["rule_pack"]["category"] == "agent-control"
61
+
62
+
63
+ @pytest.mark.parametrize(
64
+ ("path", "value"),
65
+ [
66
+ (("schema_version",), 2),
67
+ (("rule_pack", "version"), 2),
68
+ (("rule_pack", "category"), "other"),
69
+ (("rule_pack", "rules", 0, "severity"), "URGENT"),
70
+ (("rule_pack", "rules", 0, "confidence"), -0.1),
71
+ (("rule_pack", "rules", 0, "confidence"), 1.1),
72
+ ],
73
+ )
74
+ def test_rule_pack_rejects_unsupported_values(path: tuple[object, ...], value: object) -> None:
75
+ # Given: a valid config with one unsupported nested value
76
+ config = _rule_pack_config()
77
+ target: object = config
78
+ for part in path[:-1]:
79
+ target = target[part] # type: ignore[index]
80
+ target[path[-1]] = value # type: ignore[index]
81
+
82
+ # When/Then: typed validation rejects it
83
+ with pytest.raises(ValidationError):
84
+ DefenseClawRulePackConfig.model_validate(config)
85
+
86
+
87
+ def test_rule_pack_rejects_empty_and_duplicate_rules() -> None:
88
+ # Given: an empty rule list
89
+ empty = _rule_pack_config()
90
+ empty["rule_pack"]["rules"] = [] # type: ignore[index]
91
+
92
+ # When/Then: at least one rule is required
93
+ with pytest.raises(ValidationError):
94
+ DefenseClawRulePackConfig.model_validate(empty)
95
+
96
+ # Given: duplicate rule IDs
97
+ duplicate = _rule_pack_config()
98
+ first_rule = duplicate["rule_pack"]["rules"][0] # type: ignore[index]
99
+ duplicate["rule_pack"]["rules"].append(dict(first_rule)) # type: ignore[index,union-attr]
100
+
101
+ # When/Then: duplicate IDs are rejected
102
+ with pytest.raises(ValidationError, match="rule ids must be unique"):
103
+ DefenseClawRulePackConfig.model_validate(duplicate)
104
+
105
+
106
+ def test_rule_pack_rejects_blank_strings_and_unknown_fields() -> None:
107
+ # Given: whitespace-only required values and an unknown provider key
108
+ config = _rule_pack_config()
109
+ rule = config["rule_pack"]["rules"][0] # type: ignore[index]
110
+ rule["id"] = " "
111
+ rule["unknown"] = True
112
+
113
+ # When/Then: strict validation rejects both errors
114
+ with pytest.raises(ValidationError) as exc_info:
115
+ DefenseClawRulePackConfig.model_validate(config)
116
+ errors = exc_info.value.errors()
117
+ assert any(error["loc"][-1] == "id" for error in errors)
118
+ assert any(error["loc"][-1] == "unknown" for error in errors)
119
+
120
+
121
+ def test_opa_policy_example_and_defaults() -> None:
122
+ # Given: the supplied policy example
123
+ config = {
124
+ "schema_version": 1,
125
+ "policy": {
126
+ "domain": "guardrail",
127
+ "block_at": "HIGH",
128
+ "alert_at": "MEDIUM",
129
+ "cisco_trust_level": "full",
130
+ },
131
+ }
132
+
133
+ # When: validating it and validating a default policy
134
+ serialized = DefenseClawOpaPolicyConfig.model_validate(config).model_dump()
135
+ defaults = DefenseClawOpaPolicyConfig.model_validate({"policy": {}}).model_dump()
136
+
137
+ # Then: both serialize to the canonical policy envelope
138
+ assert serialized == config
139
+ assert defaults == config
140
+
141
+
142
+ @pytest.mark.parametrize(
143
+ "policy",
144
+ [
145
+ {"domain": "other"},
146
+ {"block_at": "URGENT"},
147
+ {"alert_at": "URGENT"},
148
+ {"cisco_trust_level": "partial"},
149
+ {"unexpected": True},
150
+ ],
151
+ )
152
+ def test_opa_policy_rejects_unsupported_values(policy: dict[str, object]) -> None:
153
+ # Given/When/Then: an unsupported provider value is rejected
154
+ with pytest.raises(ValidationError):
155
+ DefenseClawOpaPolicyConfig.model_validate({"policy": policy})
@@ -0,0 +1,85 @@
1
+ """Behavior tests for the DefenseClaw no-op evaluators."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tomllib
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ from agent_control_evaluator_defenseclaw import (
11
+ DefenseClawOpaPolicyEvaluator,
12
+ DefenseClawRulePackEvaluator,
13
+ )
14
+
15
+
16
+ def _rule_pack_config() -> dict[str, object]:
17
+ return {
18
+ "rule_pack": {
19
+ "rules": [
20
+ {
21
+ "id": "rule-1",
22
+ "pattern": "example",
23
+ "title": "Example",
24
+ "severity": "LOW",
25
+ "confidence": 1.0,
26
+ "tags": [],
27
+ }
28
+ ]
29
+ }
30
+ }
31
+
32
+
33
+ def test_metadata_names_are_distinct_public_identifiers() -> None:
34
+ assert DefenseClawRulePackEvaluator.metadata.name == "defenseclaw.rule_pack"
35
+ assert DefenseClawOpaPolicyEvaluator.metadata.name == "defenseclaw.opa_policy"
36
+ assert DefenseClawRulePackEvaluator is not DefenseClawOpaPolicyEvaluator
37
+
38
+
39
+ def test_entry_points_match_public_metadata_names() -> None:
40
+ # Given: the installed-package manifest contract
41
+ manifest_path = Path(__file__).parent.parent / "pyproject.toml"
42
+ manifest = tomllib.loads(manifest_path.read_text())
43
+
44
+ # When: reading evaluator entry points
45
+ entry_points = manifest["project"]["entry-points"]["agent_control.evaluators"]
46
+
47
+ # Then: both exact global names resolve to their distinct classes
48
+ assert entry_points == {
49
+ "defenseclaw.rule_pack": (
50
+ "agent_control_evaluator_defenseclaw.rule_pack:DefenseClawRulePackEvaluator"
51
+ ),
52
+ "defenseclaw.opa_policy": (
53
+ "agent_control_evaluator_defenseclaw.opa_policy:DefenseClawOpaPolicyEvaluator"
54
+ ),
55
+ }
56
+
57
+
58
+ @pytest.mark.asyncio
59
+ @pytest.mark.parametrize("data", [None, "", "selected data", {"tool": "rm"}])
60
+ async def test_rule_pack_evaluation_is_a_no_op(data: object) -> None:
61
+ # Given: a valid rule-pack evaluator
62
+ evaluator = DefenseClawRulePackEvaluator.from_dict(_rule_pack_config())
63
+
64
+ # When: evaluating any selected data
65
+ result = await evaluator.evaluate(data)
66
+
67
+ # Then: execution is intentionally inert and healthy
68
+ assert result.matched is False
69
+ assert result.confidence == 1.0
70
+ assert result.error is None
71
+
72
+
73
+ @pytest.mark.asyncio
74
+ @pytest.mark.parametrize("data", [None, "", "selected data", {"tool": "rm"}])
75
+ async def test_opa_policy_evaluation_is_a_no_op(data: object) -> None:
76
+ # Given: a valid OPA-policy evaluator
77
+ evaluator = DefenseClawOpaPolicyEvaluator.from_dict({"policy": {}})
78
+
79
+ # When: evaluating any selected data
80
+ result = await evaluator.evaluate(data)
81
+
82
+ # Then: execution is intentionally inert and healthy
83
+ assert result.matched is False
84
+ assert result.confidence == 1.0
85
+ assert result.error is None