Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ server/.generated/
tmp/
temp/
*.tmp
.codex-doc-qa/
.codex-doc-work/
.pnpm-store/

# OS
.DS_Store
Expand All @@ -87,4 +90,4 @@ Thumbs.db
# Local notes
rearch_plan.md

node_modules
node_modules
2 changes: 2 additions & 0 deletions evaluators/builtin/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies = [
galileo = ["agent-control-evaluator-galileo>=7.5.0"]
budget = ["agent-control-evaluator-budget>=7.5.0"]
cisco = ["agent-control-evaluator-cisco>=7.5.0"]
defenseclaw = ["agent-control-evaluator-defenseclaw>=8.2.0"]
dev = ["pytest>=8.0.0", "pytest-asyncio>=0.23.0"]

[project.entry-points."agent_control.evaluators"]
Expand All @@ -39,3 +40,4 @@ agent-control-models = { workspace = true }
agent-control-evaluator-galileo = { path = "../contrib/galileo", editable = true }
agent-control-evaluator-budget = { path = "../contrib/budget", editable = true }
agent-control-evaluator-cisco = { path = "../contrib/cisco", editable = true }
agent-control-evaluator-defenseclaw = { path = "../contrib/defenseclaw", editable = true }
1 change: 1 addition & 0 deletions evaluators/builtin/tests/test_contrib_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def test_discover_contrib_packages_returns_expected_metadata() -> None:
assert [(package.name, package.package, package.extra) for package in packages] == [
("budget", "agent-control-evaluator-budget", "budget"),
("cisco", "agent-control-evaluator-cisco", "cisco"),
("defenseclaw", "agent-control-evaluator-defenseclaw", "defenseclaw"),
("galileo", "agent-control-evaluator-galileo", "galileo"),
]

Expand Down
33 changes: 33 additions & 0 deletions evaluators/contrib/defenseclaw/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.PHONY: help sync test lint lint-fix typecheck check build

PACKAGE := agent-control-evaluator-defenseclaw

help:
@echo "Agent Control Evaluator - DefenseClaw - Makefile commands"
@echo " make sync - sync package dependencies"
@echo " make test - run pytest"
@echo " make lint - run ruff check"
@echo " make lint-fix - run ruff check --fix"
@echo " make typecheck - run mypy"
@echo " make check - run tests, lint, and typecheck"
@echo " make build - build package"

sync:
uv sync

test:
uv run --with pytest --with pytest-asyncio --with pytest-cov --package $(PACKAGE) pytest tests --cov=src --cov-report=xml:../../../coverage-evaluators-defenseclaw.xml -q

lint:
uv run --with ruff --package $(PACKAGE) ruff check --config ../../../pyproject.toml src/ tests/

lint-fix:
uv run --with ruff --package $(PACKAGE) ruff check --config ../../../pyproject.toml --fix src/ tests/

typecheck:
uv run --with mypy --package $(PACKAGE) mypy --config-file ../../../pyproject.toml src/

check: test lint typecheck

build:
uv build
16 changes: 16 additions & 0 deletions evaluators/contrib/defenseclaw/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# DefenseClaw evaluators

This package exposes two global external evaluators for Agent Control:

- `defenseclaw.rule_pack` validates a versioned DefenseClaw rule pack.
- `defenseclaw.opa_policy` validates a versioned DefenseClaw OPA policy configuration.

Install both evaluators with:

```bash
pip install "agent-control-evaluators[defenseclaw]"
```

The configuration contracts and JSON Schemas are implemented and discoverable. Both evaluator
classes intentionally execute as no-ops: they return `matched=False` without contacting or
installing any DefenseClaw runtime or OSS package.
42 changes: 42 additions & 0 deletions evaluators/contrib/defenseclaw/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[project]
name = "agent-control-evaluator-defenseclaw"
version = "8.2.0"
description = "DefenseClaw evaluators for agent-control"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "Apache-2.0" }
authors = [{ name = "Agent Control Team" }]
dependencies = [
"agent-control-evaluators>=8.2.0",
"agent-control-models>=8.2.0",
]

[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.0",
"pytest-cov>=4.0.0",
"ruff>=0.1.0",
"mypy>=1.8.0",
]

[project.entry-points."agent_control.evaluators"]
"defenseclaw.rule_pack" = "agent_control_evaluator_defenseclaw.rule_pack:DefenseClawRulePackEvaluator"
"defenseclaw.opa_policy" = "agent_control_evaluator_defenseclaw.opa_policy:DefenseClawOpaPolicyEvaluator"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/agent_control_evaluator_defenseclaw"]

[tool.uv.sources]
agent-control-evaluators = { path = "../../builtin", editable = true }
agent-control-models = { path = "../../../models", editable = true }

[dependency-groups]
dev = [
"pytest>=9.0.2",
"pytest-asyncio>=1.3.0",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Agent Control DefenseClaw external evaluators."""

from importlib.metadata import PackageNotFoundError, version

try:
__version__ = version("agent-control-evaluator-defenseclaw")
except PackageNotFoundError:
__version__ = "0.0.0.dev"

from .common import Severity
from .opa_policy import DefenseClawOpaPolicyConfig, DefenseClawOpaPolicyEvaluator, OpaPolicy
from .rule_pack import (
DefenseClawRulePackConfig,
DefenseClawRulePackEvaluator,
RuleConfig,
RulePack,
)

__all__ = [
"DefenseClawOpaPolicyConfig",
"DefenseClawOpaPolicyEvaluator",
"DefenseClawRulePackConfig",
"DefenseClawRulePackEvaluator",
"OpaPolicy",
"RuleConfig",
"RulePack",
"Severity",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Shared DefenseClaw evaluator types and runtime behavior."""

from __future__ import annotations

from typing import Annotated, Literal

from agent_control_models import EvaluatorResult
from pydantic import ConfigDict, StringConstraints

Severity = Literal["LOW", "MEDIUM", "HIGH", "CRITICAL"]
NonEmptyString = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]

STRICT_PROVIDER_CONFIG = ConfigDict(extra="forbid")


def no_op_result() -> EvaluatorResult:
"""Return the intentional no-op evaluation result."""
return EvaluatorResult(
matched=False,
confidence=1.0,
message="DefenseClaw configuration accepted; evaluator execution is a no-op",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""DefenseClaw OPA-policy evaluator exports."""

from .config import DefenseClawOpaPolicyConfig, OpaPolicy
from .evaluator import DefenseClawOpaPolicyEvaluator

__all__ = ["DefenseClawOpaPolicyConfig", "DefenseClawOpaPolicyEvaluator", "OpaPolicy"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Typed configuration for the DefenseClaw OPA-policy evaluator."""

from __future__ import annotations

from typing import Literal

from agent_control_evaluators import EvaluatorConfig
from pydantic import BaseModel

from ..common import STRICT_PROVIDER_CONFIG, Severity


class OpaPolicy(BaseModel):
"""DefenseClaw OPA policy settings supported by the v1 contract."""

model_config = STRICT_PROVIDER_CONFIG

domain: Literal["guardrail"] = "guardrail"
block_at: Severity = "HIGH"
alert_at: Severity = "MEDIUM"
cisco_trust_level: Literal["full"] = "full"


class DefenseClawOpaPolicyConfig(EvaluatorConfig):
"""Configuration envelope for `defenseclaw.opa_policy`."""

model_config = STRICT_PROVIDER_CONFIG

schema_version: Literal[1] = 1
policy: OpaPolicy
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""DefenseClaw OPA-policy evaluator."""

from __future__ import annotations

from importlib.metadata import PackageNotFoundError, version
from typing import Any

from agent_control_evaluators import Evaluator, EvaluatorMetadata, register_evaluator
from agent_control_models import EvaluatorResult

from ..common import no_op_result
from .config import DefenseClawOpaPolicyConfig


def _resolve_package_version() -> str:
try:
return version("agent-control-evaluator-defenseclaw")
except PackageNotFoundError:
return "0.0.0.dev"


@register_evaluator
class DefenseClawOpaPolicyEvaluator(Evaluator[DefenseClawOpaPolicyConfig]):
"""Evaluate selected data with a typed DefenseClaw OPA policy."""

metadata = EvaluatorMetadata(
name="defenseclaw.opa_policy",
version=_resolve_package_version(),
description="DefenseClaw OPA-policy evaluation",
)
config_model = DefenseClawOpaPolicyConfig

async def evaluate(self, data: Any) -> EvaluatorResult:
"""Return the intentional no-op result for all selected data."""
return no_op_result()
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""DefenseClaw rule-pack evaluator exports."""

from .config import DefenseClawRulePackConfig, RuleConfig, RulePack
from .evaluator import DefenseClawRulePackEvaluator

__all__ = [
"DefenseClawRulePackConfig",
"DefenseClawRulePackEvaluator",
"RuleConfig",
"RulePack",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Typed configuration for the DefenseClaw rule-pack evaluator."""

from __future__ import annotations

from typing import Literal, Self

from agent_control_evaluators import EvaluatorConfig
from pydantic import BaseModel, Field, model_validator

from ..common import STRICT_PROVIDER_CONFIG, NonEmptyString, Severity


class RuleConfig(BaseModel):
"""One DefenseClaw rule."""

model_config = STRICT_PROVIDER_CONFIG

id: NonEmptyString
pattern: NonEmptyString
title: NonEmptyString
severity: Severity
confidence: float = Field(ge=0.0, le=1.0)
tags: list[NonEmptyString]


class RulePack(BaseModel):
"""Versioned DefenseClaw rule pack."""

model_config = STRICT_PROVIDER_CONFIG

version: Literal[1] = 1
category: Literal["agent-control"] = "agent-control"
rules: list[RuleConfig] = Field(min_length=1)

@model_validator(mode="after")
def validate_unique_rule_ids(self) -> Self:
"""Reject ambiguous rule packs containing duplicate identifiers."""
rule_ids = [rule.id for rule in self.rules]
if len(rule_ids) != len(set(rule_ids)):
raise ValueError("rule ids must be unique within a rule pack")
return self


class DefenseClawRulePackConfig(EvaluatorConfig):
"""Configuration envelope for `defenseclaw.rule_pack`."""

model_config = STRICT_PROVIDER_CONFIG

schema_version: Literal[1] = 1
rule_pack: RulePack
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""DefenseClaw rule-pack evaluator."""

from __future__ import annotations

from importlib.metadata import PackageNotFoundError, version
from typing import Any

from agent_control_evaluators import Evaluator, EvaluatorMetadata, register_evaluator
from agent_control_models import EvaluatorResult

from ..common import no_op_result
from .config import DefenseClawRulePackConfig


def _resolve_package_version() -> str:
try:
return version("agent-control-evaluator-defenseclaw")
except PackageNotFoundError:
return "0.0.0.dev"


@register_evaluator
class DefenseClawRulePackEvaluator(Evaluator[DefenseClawRulePackConfig]):
"""Evaluate selected data with a typed DefenseClaw rule pack."""

metadata = EvaluatorMetadata(
name="defenseclaw.rule_pack",
version=_resolve_package_version(),
description="DefenseClaw rule-pack evaluation",
)
config_model = DefenseClawRulePackConfig

async def evaluate(self, data: Any) -> EvaluatorResult:
"""Return the intentional no-op result for all selected data."""
return no_op_result()
Empty file.
Loading
Loading