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.
- agent_control_evaluator_defenseclaw-8.3.0/.gitignore +93 -0
- agent_control_evaluator_defenseclaw-8.3.0/Makefile +33 -0
- agent_control_evaluator_defenseclaw-8.3.0/PKG-INFO +33 -0
- agent_control_evaluator_defenseclaw-8.3.0/README.md +16 -0
- agent_control_evaluator_defenseclaw-8.3.0/pyproject.toml +42 -0
- agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/__init__.py +28 -0
- agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/common.py +22 -0
- agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/opa_policy/__init__.py +6 -0
- agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/opa_policy/config.py +34 -0
- agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/opa_policy/evaluator.py +35 -0
- agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/py.typed +0 -0
- agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/rule_pack/__init__.py +11 -0
- agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/rule_pack/config.py +50 -0
- agent_control_evaluator_defenseclaw-8.3.0/src/agent_control_evaluator_defenseclaw/rule_pack/evaluator.py +35 -0
- agent_control_evaluator_defenseclaw-8.3.0/tests/__init__.py +0 -0
- agent_control_evaluator_defenseclaw-8.3.0/tests/test_configs.py +155 -0
- 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,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()
|
|
File without changes
|
|
@@ -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()
|
|
File without changes
|
|
@@ -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
|