Skip to content

Commit 2274ca8

Browse files
feat(evaluators): add defenseclaw evaluator (#248)
## Summary Added two global external evaluator integrations: - `defenseclaw.rule_pack` - `defenseclaw.opa_policy` - Added strict, typed Pydantic configuration models and distinct JSON Schemas. - Added dedicated Agent Control UI forms with canonical serialization of hidden protocol fields. - Both evaluator execution paths are intentionally no-ops and do not install or invoke DefenseClaw OSS packages. ## Scope **User-facing/API changes:** - Both evaluators appear independently in `GET /api/v1/evaluators`. - Added typed UI forms for rule-pack and OPA-policy configuration. - Rule-pack forms support adding and removing rules. - Fixed protocol fields remain hidden in Form mode but are always serialized. - DefenseClaw configurations are preserved in `get_server_controls()` output. **Internal changes:** - Added the `agent-control-evaluator-defenseclaw` contrib package. - Added evaluator entry-point registration and package/release wiring. - Added strict nested validation, including unique rule IDs and confidence bounds. - Added default configurations and UI registry entries. - Added focused backend and UI tests. **Out of scope:** - DefenseClaw transport, authentication, or provider invocation. - Installing or calling DefenseClaw OSS packages. - Changes to the separate `~/code/ui` Galileo UI repository. - Orbit production packaging changes. ## Risk and Rollout **Risk level:** medium **Primary risk:** - The evaluator package must be installed in the server runtime for catalog discovery and server execution. - It must also be installed in SDK runtimes for controls using `execution: "sdk"`. **Rollout requirements:** - Publish `agent-control-evaluator-defenseclaw`. - Add the package to the Orbit Agent Control image dependencies. - Verify both evaluator identifiers appear in `GET /api/v1/evaluators` before activating controls. **Rollback plan:** - Remove or disable controls referencing the DefenseClaw evaluator names. - Remove the DefenseClaw package from runtime dependencies. - Revert the evaluator registry and UI form registration changes. ## Testing - [x] Added or updated automated tests - Ran `make check` (full repository check was not run). - Ran 26 focused Python tests successfully. - Ran Ruff and mypy successfully. - Ran TypeScript, ESLint, and Prettier checks successfully. - Verified contrib package wiring, evaluator registration, and server catalog schemas. - Playwright could not run in the sandbox because it could not bind the local UI server port. - [x] Manually verified behavior - Registration and generated catalog schemas were verified programmatically. - Full browser-based Form/JSON interaction should be verified locally using the supplied testing guide. ## Checklist - [ ] **Linked issue/spec** - Technical spec: `defenseclaw-external-evaluators-technical-spec.md` - Add the appropriate issue/spec URL before merging. - [x] **Updated docs/examples for user-facing changes** - Added the contrib package README. - Created a first-time local setup and testing guide. - [x] **Included required follow-up tasks** - Add DefenseClaw packaging to `~/code/orbit`. - Implement the corresponding forms in `~/code/ui`. - Run the full Playwright suite. - Install the evaluator package in every SDK runtime that evaluates DefenseClaw controls.
1 parent 83188b6 commit 2274ca8

31 files changed

Lines changed: 1159 additions & 7 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ server/.generated/
7373
tmp/
7474
temp/
7575
*.tmp
76+
.codex-doc-qa/
77+
.codex-doc-work/
78+
.pnpm-store/
7679

7780
# OS
7881
.DS_Store
@@ -87,4 +90,4 @@ Thumbs.db
8790
# Local notes
8891
rearch_plan.md
8992

90-
node_modules
93+
node_modules

evaluators/builtin/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies = [
1818
galileo = ["agent-control-evaluator-galileo>=7.5.0"]
1919
budget = ["agent-control-evaluator-budget>=7.5.0"]
2020
cisco = ["agent-control-evaluator-cisco>=7.5.0"]
21+
defenseclaw = ["agent-control-evaluator-defenseclaw>=8.2.0"]
2122
dev = ["pytest>=8.0.0", "pytest-asyncio>=0.23.0"]
2223

2324
[project.entry-points."agent_control.evaluators"]
@@ -39,3 +40,4 @@ agent-control-models = { workspace = true }
3940
agent-control-evaluator-galileo = { path = "../contrib/galileo", editable = true }
4041
agent-control-evaluator-budget = { path = "../contrib/budget", editable = true }
4142
agent-control-evaluator-cisco = { path = "../contrib/cisco", editable = true }
43+
agent-control-evaluator-defenseclaw = { path = "../contrib/defenseclaw", editable = true }

evaluators/builtin/tests/test_contrib_packages.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def test_discover_contrib_packages_returns_expected_metadata() -> None:
3939
assert [(package.name, package.package, package.extra) for package in packages] == [
4040
("budget", "agent-control-evaluator-budget", "budget"),
4141
("cisco", "agent-control-evaluator-cisco", "cisco"),
42+
("defenseclaw", "agent-control-evaluator-defenseclaw", "defenseclaw"),
4243
("galileo", "agent-control-evaluator-galileo", "galileo"),
4344
]
4445

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
[project]
2+
name = "agent-control-evaluator-defenseclaw"
3+
version = "8.2.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.2.0",
11+
"agent-control-models>=8.2.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+
]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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"]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)