From 646557273058dc52c871a4f621d1b25405abc5a1 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Wed, 11 Mar 2026 22:03:41 -0700 Subject: [PATCH 01/19] feat!: implement recursive control condition trees --- engine/pyproject.toml | 6 +- engine/src/agent_control_engine/core.py | 333 +- engine/tests/test_core.py | 64 +- evaluators/builtin/pyproject.toml | 6 +- evaluators/contrib/cisco/pyproject.toml | 5 +- evaluators/contrib/galileo/pyproject.toml | 6 +- .../contrib/template/pyproject.toml.template | 6 +- models/pyproject.toml | 2 +- models/src/agent_control_models/__init__.py | 2 + models/src/agent_control_models/controls.py | 209 +- pyproject.toml | 3 +- sdks/python/Makefile | 18 +- sdks/python/pyproject.toml | 8 +- sdks/python/src/agent_control/__init__.py | 12 +- sdks/python/src/agent_control/client.py | 35 +- .../src/agent_control/control_decorators.py | 4 +- sdks/python/src/agent_control/controls.py | 16 +- sdks/python/src/agent_control/evaluation.py | 52 +- sdks/python/tests/test_integration_agents.py | 10 +- sdks/python/tests/test_local_evaluation.py | 16 +- .../tests/test_observability_updates.py | 24 +- sdks/typescript/gen.yaml | 2 +- sdks/typescript/package.json | 2 +- sdks/typescript/src/generated/lib/config.ts | 6 +- .../generated/models/condition-node-input.ts | 74 + .../generated/models/condition-node-output.ts | 68 + .../models/control-definition-input.ts | 45 +- .../models/control-definition-output.ts | 39 +- sdks/typescript/src/generated/models/index.ts | 2 + server/Makefile | 8 +- server/pyproject.toml | 7 +- .../agent_control_server/endpoints/agents.py | 105 +- .../endpoints/controls.py | 285 +- .../endpoints/evaluation.py | 18 +- server/src/agent_control_server/errors.py | 8 +- server/src/agent_control_server/main.py | 13 +- .../agent_control_server/scripts/__init__.py | 1 + .../scripts/migrate_control_conditions.py | 114 + .../services/control_migration.py | 93 + .../agent_control_server/services/controls.py | 3 +- .../services/validation_paths.py | 18 + server/tests/test_agents_additional.py | 47 +- server/tests/test_auth.py | 10 +- server/tests/test_control_migration.py | 60 + server/tests/test_controls.py | 14 +- server/tests/test_controls_additional.py | 32 +- server/tests/test_controls_validation.py | 32 +- server/tests/test_error_handling.py | 6 +- server/tests/test_evaluation_e2e.py | 9 +- server/tests/test_init_agent_conflict_mode.py | 2 +- server/tests/test_new_features.py | 10 +- server/tests/utils.py | 32 +- ui/src/core/api/generated/api-types.ts | 9172 +++++++++-------- ui/src/core/api/types.ts | 3 + .../modals/add-new-control/index.tsx | 14 +- .../modals/edit-control/condition-builder.ts | 420 + .../edit-control/condition-tree-editor.tsx | 540 + .../edit-control/control-definition-form.tsx | 21 - .../edit-control/edit-control-content.tsx | 302 +- .../agent-detail/modals/edit-control/types.ts | 1 - .../agent-detail/modals/edit-control/utils.ts | 36 +- ui/tests/fixtures.ts | 30 +- 62 files changed, 7348 insertions(+), 5193 deletions(-) create mode 100644 sdks/typescript/src/generated/models/condition-node-input.ts create mode 100644 sdks/typescript/src/generated/models/condition-node-output.ts create mode 100644 server/src/agent_control_server/scripts/__init__.py create mode 100644 server/src/agent_control_server/scripts/migrate_control_conditions.py create mode 100644 server/src/agent_control_server/services/control_migration.py create mode 100644 server/src/agent_control_server/services/validation_paths.py create mode 100644 server/tests/test_control_migration.py create mode 100644 ui/src/core/page-components/agent-detail/modals/edit-control/condition-builder.ts create mode 100644 ui/src/core/page-components/agent-detail/modals/edit-control/condition-tree-editor.tsx diff --git a/engine/pyproject.toml b/engine/pyproject.toml index 2dc4c15b..732070b9 100644 --- a/engine/pyproject.toml +++ b/engine/pyproject.toml @@ -1,11 +1,11 @@ [project] name = "agent-control-engine" -version = "2.1.0" +version = "3.0.0" description = "Control execution engine for Agent Control" requires-python = ">=3.12" dependencies = [ - "agent-control-models>=3.0.0", - "agent-control-evaluators>=3.0.0", + "agent-control-models>=7.0.0", + "agent-control-evaluators>=7.0.0", "google-re2>=1.1", ] authors = [ diff --git a/engine/src/agent_control_engine/core.py b/engine/src/agent_control_engine/core.py index c109cc80..d2537eae 100644 --- a/engine/src/agent_control_engine/core.py +++ b/engine/src/agent_control_engine/core.py @@ -14,6 +14,7 @@ import re2 from agent_control_evaluators import get_evaluator_instance from agent_control_models import ( + ConditionNode, ControlDefinition, ControlMatch, EvaluationRequest, @@ -54,11 +55,18 @@ class _EvalTask: """Internal container for evaluation task context.""" item: ControlWithIdentity - data: Any task: asyncio.Task[None] | None = None result: EvaluatorResult | None = None +@dataclass +class _ConditionEvaluation: + """Internal result for recursive condition evaluation.""" + + result: EvaluatorResult + trace: dict[str, Any] + + class ControlEngine: """Executes controls against requests with parallel evaluation. @@ -79,6 +87,256 @@ def __init__( self.controls = controls self.context = context + @staticmethod + def _truncated_message(message: str | None) -> str | None: + """Truncate long evaluator messages in condition traces.""" + if not message: + return None + if len(message) <= 200: + return message + return f"{message[:197]}..." + + def _skipped_trace(self, node: ConditionNode, reason: str) -> dict[str, Any]: + """Build an unevaluated trace subtree for short-circuited branches.""" + trace: dict[str, Any] = { + "type": node.kind(), + "evaluated": False, + "matched": None, + "short_circuit_reason": reason, + } + if node.is_leaf(): + selector, evaluator = node.leaf_parts() or (None, None) + trace["selector_path"] = selector.path if selector else None + trace["evaluator_name"] = evaluator.name if evaluator else None + trace["confidence"] = None + trace["error"] = None + return trace + + trace["children"] = [ + self._skipped_trace(child, reason) for child in node.children_in_order() + ] + return trace + + async def _evaluate_leaf( + self, + item: ControlWithIdentity, + node: ConditionNode, + request: EvaluationRequest, + semaphore: asyncio.Semaphore, + ) -> _ConditionEvaluation: + """Evaluate a leaf selector/evaluator pair.""" + selector, evaluator_spec = node.leaf_parts() or (None, None) + if selector is None or evaluator_spec is None: + raise ValueError("Leaf condition must contain selector and evaluator") + + selector_path = selector.path or "*" + data = select_data(request.step, selector_path) + + try: + async with semaphore: + evaluator = get_evaluator_instance(evaluator_spec) + timeout = evaluator.get_timeout_seconds() + if timeout <= 0: + timeout = DEFAULT_EVALUATOR_TIMEOUT + + result = await asyncio.wait_for( + evaluator.evaluate(data), + timeout=timeout, + ) + except TimeoutError: + error_msg = f"TimeoutError: Evaluator exceeded {timeout}s timeout" + logger.warning( + "Evaluator timeout for control '%s' (evaluator: %s): %s", + item.name, + evaluator_spec.name, + error_msg, + exc_info=True, + ) + result = EvaluatorResult( + matched=False, + confidence=0.0, + message=f"Evaluation failed: {error_msg}", + error=error_msg, + ) + except Exception as e: + error_msg = f"{type(e).__name__}: {e}" + logger.error( + "Evaluator error for control '%s' (evaluator: %s): %s", + item.name, + evaluator_spec.name, + error_msg, + exc_info=True, + ) + result = EvaluatorResult( + matched=False, + confidence=0.0, + message=f"Evaluation failed: {error_msg}", + error=error_msg, + ) + + trace = { + "type": "leaf", + "evaluated": True, + "matched": result.matched, + "selector_path": selector_path, + "evaluator_name": evaluator_spec.name, + "confidence": result.confidence, + "error": result.error, + "message": self._truncated_message(result.message), + } + metadata = dict(result.metadata or {}) + metadata["condition_trace"] = trace + return _ConditionEvaluation( + result=result.model_copy(update={"metadata": metadata}), + trace=trace, + ) + + def _build_composite_result( + self, + *, + matched: bool, + confidence: float, + trace: dict[str, Any], + error: str | None = None, + ) -> EvaluatorResult: + """Create a composite evaluator result with a condition trace.""" + if error is not None: + return EvaluatorResult( + matched=False, + confidence=0.0, + message=f"Condition evaluation failed: {error}", + metadata={"condition_trace": trace}, + error=error, + ) + + message = "Condition tree matched" if matched else "Condition tree did not match" + return EvaluatorResult( + matched=matched, + confidence=confidence, + message=message, + metadata={"condition_trace": trace}, + ) + + async def _evaluate_condition( + self, + item: ControlWithIdentity, + node: ConditionNode, + request: EvaluationRequest, + semaphore: asyncio.Semaphore, + ) -> _ConditionEvaluation: + """Evaluate a recursive condition tree.""" + if node.is_leaf(): + return await self._evaluate_leaf(item, node, request, semaphore) + + kind = node.kind() + children = node.children_in_order() + child_evaluations: list[_ConditionEvaluation] = [] + + if kind == "not": + child_eval = await self._evaluate_condition(item, children[0], request, semaphore) + trace = { + "type": "not", + "evaluated": True, + "matched": None if child_eval.result.error else (not child_eval.result.matched), + "children": [child_eval.trace], + } + if child_eval.result.error: + return _ConditionEvaluation( + result=self._build_composite_result( + matched=False, + confidence=0.0, + trace=trace, + error=child_eval.result.error, + ), + trace=trace, + ) + + result = self._build_composite_result( + matched=not child_eval.result.matched, + confidence=child_eval.result.confidence, + trace=trace, + ) + return _ConditionEvaluation(result=result, trace=trace) + + for index, child in enumerate(children): + child_eval = await self._evaluate_condition(item, child, request, semaphore) + child_evaluations.append(child_eval) + + if child_eval.result.error: + remaining = children[index + 1 :] + trace = { + "type": kind, + "evaluated": True, + "matched": False, + "children": [ + evaluation.trace for evaluation in child_evaluations + ] + + [self._skipped_trace(rest, "error") for rest in remaining], + "short_circuit_reason": "error", + } + return _ConditionEvaluation( + result=self._build_composite_result( + matched=False, + confidence=0.0, + trace=trace, + error=child_eval.result.error, + ), + trace=trace, + ) + + should_short_circuit = ( + kind == "and" and not child_eval.result.matched + ) or (kind == "or" and child_eval.result.matched) + if should_short_circuit: + remaining = children[index + 1 :] + matched = child_eval.result.matched if kind == "or" else False + trace = { + "type": kind, + "evaluated": True, + "matched": matched, + "children": [ + evaluation.trace for evaluation in child_evaluations + ] + + [ + self._skipped_trace( + rest, + "or_matched" if kind == "or" else "and_failed", + ) + for rest in remaining + ], + "short_circuit_reason": ( + "or_matched" if kind == "or" else "and_failed" + ), + } + confidence = min( + evaluation.result.confidence for evaluation in child_evaluations + ) + result = self._build_composite_result( + matched=matched, + confidence=confidence, + trace=trace, + ) + return _ConditionEvaluation(result=result, trace=trace) + + confidence = min(evaluation.result.confidence for evaluation in child_evaluations) + matched = all( + evaluation.result.matched for evaluation in child_evaluations + ) if kind == "and" else any( + evaluation.result.matched for evaluation in child_evaluations + ) + trace = { + "type": kind, + "evaluated": True, + "matched": matched, + "children": [evaluation.trace for evaluation in child_evaluations], + } + result = self._build_composite_result( + matched=matched, + confidence=confidence, + trace=trace, + ) + return _ConditionEvaluation(result=result, trace=trace) + def get_applicable_controls( self, request: EvaluationRequest, @@ -169,12 +427,7 @@ async def process(self, request: EvaluationRequest) -> EvaluationResponse: ) # Prepare evaluation tasks - eval_tasks: list[_EvalTask] = [] - for item in applicable: - control_def = item.control - sel_path = control_def.selector.path or "*" - data = select_data(request.step, sel_path) - eval_tasks.append(_EvalTask(item=item, data=data)) + eval_tasks: list[_EvalTask] = [_EvalTask(item=item) for item in applicable] # Run evaluations in parallel with cancel-on-deny matches: list[ControlMatch] = [] @@ -184,58 +437,22 @@ async def process(self, request: EvaluationRequest) -> EvaluationResponse: async def evaluate_control(eval_task: _EvalTask) -> None: """Evaluate a single control, respecting cancellation and timeout.""" - async with semaphore: - try: - evaluator = get_evaluator_instance(eval_task.item.control.evaluator) - # Use evaluator's timeout or fall back to default - timeout = evaluator.get_timeout_seconds() - if timeout <= 0: - timeout = DEFAULT_EVALUATOR_TIMEOUT - - eval_task.result = await asyncio.wait_for( - evaluator.evaluate(eval_task.data), - timeout=timeout, - ) + try: + evaluation = await self._evaluate_condition( + eval_task.item, + eval_task.item.control.condition, + request, + semaphore, + ) + eval_task.result = evaluation.result - # Signal if this is a deny match - only deny should trigger cancellation - # to preserve deny-first semantics - if ( - eval_task.result.matched - and eval_task.item.control.action.decision == "deny" - ): - deny_found.set() - except asyncio.CancelledError: - # Task was cancelled due to another deny - that's OK - raise - except TimeoutError: - # Evaluator timed out - error_msg = f"TimeoutError: Evaluator exceeded {timeout}s timeout" - logger.warning( - f"Evaluator timeout for control '{eval_task.item.name}' " - f"(evaluator: {eval_task.item.control.evaluator.name}): {error_msg}", - exc_info=True, - ) - eval_task.result = EvaluatorResult( - matched=False, - confidence=0.0, - message=f"Evaluation failed: {error_msg}", - error=error_msg, - ) - except Exception as e: - # Evaluation error - fail open but mark as error - # The error field signals to callers that this was not a real evaluation - error_msg = f"{type(e).__name__}: {e}" - logger.error( - f"Evaluator error for control '{eval_task.item.name}' " - f"(evaluator: {eval_task.item.control.evaluator.name}): {error_msg}", - exc_info=True, - ) - eval_task.result = EvaluatorResult( - matched=False, - confidence=0.0, - message=f"Evaluation failed: {error_msg}", - error=error_msg, - ) + if ( + eval_task.result.matched + and eval_task.item.control.action.decision == "deny" + ): + deny_found.set() + except asyncio.CancelledError: + raise # Create and start all tasks for eval_task in eval_tasks: diff --git a/engine/tests/test_core.py b/engine/tests/test_core.py index d5e7418f..5e0ef62a 100644 --- a/engine/tests/test_core.py +++ b/engine/tests/test_core.py @@ -210,11 +210,13 @@ def make_control( enabled=True, execution=execution, scope=scope, - selector=selector or {"path": "*"}, - evaluator=EvaluatorSpec( - name=evaluator, - config={"value": config_value}, - ), + condition={ + "selector": selector or {"path": "*"}, + "evaluator": EvaluatorSpec( + name=evaluator, + config={"value": config_value}, + ), + }, action=( ControlAction(decision=action, steering_context=steering_context) if steering_context @@ -1063,8 +1065,10 @@ def test_invalid_step_name_regex_rejected(self): enabled=True, execution="server", scope={"step_types": ["tool"], "stages": ["pre"], "step_name_regex": "("}, - selector={"path": "input"}, - evaluator=EvaluatorSpec(name="test-allow", config={"value": "x"}), + condition={ + "selector": {"path": "input"}, + "evaluator": EvaluatorSpec(name="test-allow", config={"value": "x"}), + }, action={"decision": "log"}, ) @@ -1100,11 +1104,13 @@ async def test_evaluator_timeout_is_enforced(self): enabled=True, execution="server", scope={"step_types": ["llm"], "stages": ["pre"]}, - selector={"path": "input"}, - evaluator=EvaluatorSpec( - name="test-timeout", - config={"value": "t1", "timeout_ms": 100}, - ), + condition={ + "selector": {"path": "input"}, + "evaluator": EvaluatorSpec( + name="test-timeout", + config={"value": "t1", "timeout_ms": 100}, + ), + }, action={"decision": "deny"}, ), ) @@ -1158,11 +1164,13 @@ async def test_timeout_does_not_affect_fast_evaluators(self): enabled=True, execution="server", scope={"step_types": ["llm"], "stages": ["pre"]}, - selector={"path": "input"}, - evaluator=EvaluatorSpec( - name="test-timeout", - config={"value": "slow", "timeout_ms": 100}, - ), + condition={ + "selector": {"path": "input"}, + "evaluator": EvaluatorSpec( + name="test-timeout", + config={"value": "slow", "timeout_ms": 100}, + ), + }, action={"decision": "log"}, # Log, not deny - so fails open ), ), @@ -1304,11 +1312,13 @@ def make_control_with_execution( enabled=True, execution=execution, scope=scope, - selector={"path": path}, - evaluator=EvaluatorSpec( - name=evaluator, - config={"value": config_value}, - ), + condition={ + "selector": {"path": path}, + "evaluator": EvaluatorSpec( + name=evaluator, + config={"value": config_value}, + ), + }, action={"decision": action}, ), ) @@ -1802,10 +1812,12 @@ class MockControl: "step_types": ["llm"], "step_name_regex": "[invalid(regex", # Invalid regex pattern }, - "selector": {"path": "input"}, - "evaluator": { - "name": "test-allow", - "config": {"value": "test"}, + "condition": { + "selector": {"path": "input"}, + "evaluator": { + "name": "test-allow", + "config": {"value": "test"}, + }, }, "action": {"decision": "deny"}, } diff --git a/evaluators/builtin/pyproject.toml b/evaluators/builtin/pyproject.toml index 079063cf..bafb6322 100644 --- a/evaluators/builtin/pyproject.toml +++ b/evaluators/builtin/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "agent-control-evaluators" -version = "6.7.2" +version = "7.0.0" description = "Builtin evaluators for agent-control" readme = "README.md" requires-python = ">=3.12" license = { text = "Apache-2.0" } authors = [{ name = "Agent Control Team" }] dependencies = [ - "agent-control-models", + "agent-control-models>=7.0.0", "pydantic>=2.12.4", "google-re2>=1.1", "jsonschema>=4.0.0", @@ -15,7 +15,7 @@ dependencies = [ ] [project.optional-dependencies] -galileo = ["agent-control-evaluator-galileo>=3.0.0"] +galileo = ["agent-control-evaluator-galileo>=7.0.0"] cisco = ["agent-control-evaluator-cisco>=0.1.0"] dev = ["pytest>=8.0.0", "pytest-asyncio>=0.23.0"] diff --git a/evaluators/contrib/cisco/pyproject.toml b/evaluators/contrib/cisco/pyproject.toml index e53b7d89..0f2d47d0 100644 --- a/evaluators/contrib/cisco/pyproject.toml +++ b/evaluators/contrib/cisco/pyproject.toml @@ -7,8 +7,8 @@ requires-python = ">=3.12" license = { text = "Apache-2.0" } authors = [{ name = "Cisco AI Defense Team" }] dependencies = [ - "agent-control-evaluators>=3.0.0", - "agent-control-models>=3.0.0", + "agent-control-evaluators>=7.0.0", + "agent-control-models>=7.0.0", "httpx>=0.24.0", ] @@ -41,4 +41,3 @@ select = ["E", "F", "I"] [tool.uv.sources] agent-control-evaluators = { path = "../../builtin", editable = true } agent-control-models = { path = "../../../models", editable = true } - diff --git a/evaluators/contrib/galileo/pyproject.toml b/evaluators/contrib/galileo/pyproject.toml index ad511c05..0dd5e0d8 100644 --- a/evaluators/contrib/galileo/pyproject.toml +++ b/evaluators/contrib/galileo/pyproject.toml @@ -1,14 +1,14 @@ [project] name = "agent-control-evaluator-galileo" -version = "6.7.2" +version = "7.0.0" description = "Galileo Luna2 evaluator for agent-control" readme = "README.md" requires-python = ">=3.12" license = { text = "Apache-2.0" } authors = [{ name = "Agent Control Team" }] dependencies = [ - "agent-control-evaluators>=3.0.0", - "agent-control-models>=3.0.0", + "agent-control-evaluators>=7.0.0", + "agent-control-models>=7.0.0", "httpx>=0.24.0", "pydantic>=2.12.4", ] diff --git a/evaluators/contrib/template/pyproject.toml.template b/evaluators/contrib/template/pyproject.toml.template index 484864bb..e5973018 100644 --- a/evaluators/contrib/template/pyproject.toml.template +++ b/evaluators/contrib/template/pyproject.toml.template @@ -1,13 +1,13 @@ [project] name = "agent-control-evaluator-{{ORG}}" -version = "3.0.0" +version = "7.0.0" description = "{{ORG}} evaluators for agent-control" requires-python = ">=3.12" license = { text = "Apache-2.0" } authors = [{ name = "{{AUTHOR}}" }] dependencies = [ - "agent-control-evaluators>=3.0.0", - "agent-control-models>=3.0.0", + "agent-control-evaluators>=7.0.0", + "agent-control-models>=7.0.0", # Add your package-specific dependencies here ] diff --git a/models/pyproject.toml b/models/pyproject.toml index 9d448330..a7a88c28 100644 --- a/models/pyproject.toml +++ b/models/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agent-control-models" -version = "6.7.2" +version = "7.0.0" description = "Shared data models for Agent Control server and SDK" requires-python = ">=3.12" dependencies = [ diff --git a/models/src/agent_control_models/__init__.py b/models/src/agent_control_models/__init__.py index 77ee5615..09cd3ffb 100644 --- a/models/src/agent_control_models/__init__.py +++ b/models/src/agent_control_models/__init__.py @@ -18,6 +18,7 @@ StepSchema, ) from .controls import ( + ConditionNode, ControlAction, ControlDefinition, ControlMatch, @@ -103,6 +104,7 @@ "EvaluationResult", # Controls "ControlDefinition", + "ConditionNode", "ControlAction", "ControlMatch", "ControlScope", diff --git a/models/src/agent_control_models/controls.py b/models/src/agent_control_models/controls.py index 62b3ad2e..f0b01f89 100644 --- a/models/src/agent_control_models/controls.py +++ b/models/src/agent_control_models/controls.py @@ -1,10 +1,13 @@ """Control definition models for agent protection.""" +from __future__ import annotations + +from collections.abc import Iterator from typing import Any, Literal, Self from uuid import uuid4 import re2 -from pydantic import Field, ValidationInfo, field_validator, model_validator +from pydantic import ConfigDict, Field, ValidationInfo, field_validator, model_validator from .base import BaseModel @@ -260,6 +263,159 @@ class ControlAction(BaseModel): ) +MAX_CONDITION_DEPTH = 6 + + +class ConditionNode(BaseModel): + """Recursive boolean condition tree for control evaluation.""" + + selector: ControlSelector | None = Field( + default=None, + description="Leaf selector. Must be provided together with evaluator.", + ) + evaluator: EvaluatorSpec | None = Field( + default=None, + description="Leaf evaluator. Must be provided together with selector.", + ) + and_: list[ConditionNode] | None = Field( + default=None, + alias="and", + serialization_alias="and", + description="Logical AND over child conditions.", + ) + or_: list[ConditionNode] | None = Field( + default=None, + alias="or", + serialization_alias="or", + description="Logical OR over child conditions.", + ) + not_: ConditionNode | None = Field( + default=None, + alias="not", + serialization_alias="not", + description="Logical NOT over a single child condition.", + ) + + model_config = ConfigDict( + populate_by_name=True, + use_enum_values=True, + validate_assignment=True, + extra="ignore", + serialize_by_alias=True, + ) + + @model_validator(mode="after") + def validate_shape(self) -> Self: + """Ensure each node is exactly one of leaf/and/or/not.""" + has_selector = self.selector is not None + has_evaluator = self.evaluator is not None + has_leaf = has_selector and has_evaluator + if has_selector != has_evaluator: + raise ValueError("Leaf condition requires both selector and evaluator") + + populated = sum( + 1 + for present in ( + has_leaf, + self.and_ is not None, + self.or_ is not None, + self.not_ is not None, + ) + if present + ) + if populated != 1: + raise ValueError("Condition node must contain exactly one of leaf, and, or, not") + + if self.and_ is not None and len(self.and_) == 0: + raise ValueError("'and' must contain at least one child condition") + if self.or_ is not None and len(self.or_) == 0: + raise ValueError("'or' must contain at least one child condition") + + return self + + def kind(self) -> Literal["leaf", "and", "or", "not"]: + """Return the logical node type.""" + if self.is_leaf(): + return "leaf" + if self.and_ is not None: + return "and" + if self.or_ is not None: + return "or" + return "not" + + def is_leaf(self) -> bool: + """Return True when this node is a leaf selector/evaluator pair.""" + return self.selector is not None and self.evaluator is not None + + def children_in_order(self) -> list[ConditionNode]: + """Return child conditions in evaluation order.""" + if self.and_ is not None: + return self.and_ + if self.or_ is not None: + return self.or_ + if self.not_ is not None: + return [self.not_] + return [] + + def iter_leaves(self) -> Iterator[ConditionNode]: + """Yield leaf nodes in left-to-right traversal order.""" + if self.is_leaf(): + yield self + return + + for child in self.children_in_order(): + yield from child.iter_leaves() + + def max_depth(self) -> int: + """Return the maximum nesting depth of this condition tree.""" + children = self.children_in_order() + if not children: + return 1 + return 1 + max(child.max_depth() for child in children) + + def leaf_parts(self) -> tuple[ControlSelector, EvaluatorSpec] | None: + """Return the selector/evaluator pair for leaf nodes.""" + if not self.is_leaf(): + return None + selector = self.selector + evaluator = self.evaluator + if selector is None or evaluator is None: + return None + return selector, evaluator + + model_config["json_schema_extra"] = { + "examples": [ + { + "selector": {"path": "output"}, + "evaluator": {"name": "regex", "config": {"pattern": r"\d{3}-\d{2}-\d{4}"}}, + }, + { + "and": [ + { + "selector": {"path": "context.risk_level"}, + "evaluator": { + "name": "list", + "config": {"values": ["high", "critical"]}, + }, + }, + { + "not": { + "selector": {"path": "context.user_role"}, + "evaluator": { + "name": "list", + "config": {"values": ["admin", "security"]}, + }, + } + }, + ] + }, + ] + } + + +ConditionNode.model_rebuild() + + class ControlDefinition(BaseModel): """A control definition to evaluate agent interactions. @@ -280,10 +436,13 @@ class ControlDefinition(BaseModel): ) # What to check - selector: ControlSelector = Field(..., description="What data to select from the payload") - - # How to check (unified evaluator-based system) - evaluator: EvaluatorSpec = Field(..., description="How to evaluate the selected data") + condition: ConditionNode = Field( + ..., + description=( + "Recursive boolean condition tree. Leaf nodes contain selector + evaluator; " + "composite nodes contain and/or/not." + ), + ) # What to do action: ControlAction = Field(..., description="What action to take when control matches") @@ -291,6 +450,34 @@ class ControlDefinition(BaseModel): # Metadata tags: list[str] = Field(default_factory=list, description="Tags for categorization") + @model_validator(mode="after") + def validate_condition_constraints(self) -> Self: + """Validate cross-field control constraints.""" + if self.condition.max_depth() > MAX_CONDITION_DEPTH: + raise ValueError( + f"Condition nesting depth exceeds maximum of {MAX_CONDITION_DEPTH}" + ) + + if ( + self.action.decision == "steer" + and not self.condition.is_leaf() + and self.action.steering_context is None + ): + raise ValueError( + "Composite steer controls require action.steering_context" + ) + return self + + def iter_condition_leaves(self) -> Iterator[ConditionNode]: + """Yield leaf conditions in evaluation order.""" + yield from self.condition.iter_leaves() + + def primary_leaf(self) -> ConditionNode | None: + """Return the single leaf node when the whole condition is just one leaf.""" + if self.condition.is_leaf(): + return self.condition + return None + model_config = { "json_schema_extra": { "examples": [ @@ -299,11 +486,13 @@ class ControlDefinition(BaseModel): "enabled": True, "execution": "server", "scope": {"step_types": ["llm"], "stages": ["post"]}, - "selector": {"path": "output"}, - "evaluator": { - "name": "regex", - "config": { - "pattern": r"\b\d{3}-\d{2}-\d{4}\b", + "condition": { + "selector": {"path": "output"}, + "evaluator": { + "name": "regex", + "config": { + "pattern": r"\b\d{3}-\d{2}-\d{4}\b", + }, }, }, "action": { diff --git a/pyproject.toml b/pyproject.toml index 3ee8b7d9..25bcc28f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ [project] name = "agent-control" -version = "6.7.2" +version = "7.0.0" description = "Agent Control - protect your AI agents with controls" requires-python = ">=3.12" @@ -66,6 +66,7 @@ ignore_missing_imports = true version_toml = [ "pyproject.toml:project.version", "models/pyproject.toml:project.version", + "engine/pyproject.toml:project.version", "sdks/python/pyproject.toml:project.version", "server/pyproject.toml:project.version", "evaluators/builtin/pyproject.toml:project.version", diff --git a/sdks/python/Makefile b/sdks/python/Makefile index 991e1f93..c55a113c 100644 --- a/sdks/python/Makefile +++ b/sdks/python/Makefile @@ -1,6 +1,9 @@ .PHONY: help test lint lint-fix typecheck build publish TEST_DB ?= agent_control_test +TEST_SERVER_PORT ?= 18000 +TEST_SERVER_HOST ?= 127.0.0.1 +TEST_SERVER_URL := http://$(TEST_SERVER_HOST):$(TEST_SERVER_PORT) help: @echo "Agent Control SDK - Makefile commands" @@ -22,16 +25,17 @@ test: DB_DATABASE=$(TEST_DB) uv run --package agent-control-server python scripts/reset_test_db.py DB_DATABASE=$(TEST_DB) $(MAKE) -C ../../ server-alembic-upgrade @# Start server in background and save PID - @DB_DATABASE=$(TEST_DB) uv run --package agent-control-server uvicorn agent_control_server.main:app --port 8000 --host 0.0.0.0 > server.log 2>&1 & echo $$! > server.pid + @DB_DATABASE=$(TEST_DB) uv run --package agent-control-server uvicorn agent_control_server.main:app --port $(TEST_SERVER_PORT) --host $(TEST_SERVER_HOST) > server.log 2>&1 & echo $$! > server.pid @echo "Waiting for server..." - @bash -c 'for i in {1..30}; do if curl -s http://localhost:8000/health >/dev/null; then echo "Server up!"; exit 0; fi; sleep 1; done; echo "Server failed"; cat server.log; exit 1' + @bash -c 'for i in {1..30}; do if curl -s $(TEST_SERVER_URL)/health >/dev/null; then echo "Server up!"; exit 0; fi; sleep 1; done; echo "Server failed"; cat server.log; exit 1' @# Run tests, capture exit code, and ensure cleanup @set -e; \ - DB_DATABASE=$(TEST_DB) uv run pytest --cov=src --cov-report=xml:../../coverage-sdk.xml -q; \ - TEST_EXIT_CODE=$$?; \ - echo "Stopping server..."; \ - if [ -f server.pid ]; then kill `cat server.pid` && rm server.pid; fi; \ - exit $$TEST_EXIT_CODE + cleanup() { \ + echo "Stopping server..."; \ + if [ -f server.pid ]; then kill `cat server.pid` 2>/dev/null || true; rm -f server.pid; fi; \ + }; \ + trap cleanup EXIT; \ + DB_DATABASE=$(TEST_DB) AGENT_CONTROL_TEST_URL=$(TEST_SERVER_URL) uv run pytest --cov=src --cov-report=xml:../../coverage-sdk.xml -q lint: uv run ruff check --config ../../pyproject.toml src/ diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml index 2d915def..30efa061 100644 --- a/sdks/python/pyproject.toml +++ b/sdks/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agent-control-sdk" -version = "6.7.2" +version = "7.0.0" description = "Python SDK for Agent Control - protect your AI agents with controls" requires-python = ">=3.12" # Note: agent-control-models and agent-control-engine are bundled at build time @@ -13,7 +13,7 @@ dependencies = [ "docstring-parser>=0.15", # For @tool decorator schema inference "google-re2>=1.1", # For engine (bundled) "jsonschema>=4.0.0", # For models/engine (bundled) - "agent-control-evaluators>=3.0.0", # NOT vendored - avoid conflict with galileo + "agent-control-evaluators>=7.0.0", # NOT vendored - avoid conflict with galileo ] authors = [ {name = "Agent Control Team"} @@ -37,7 +37,7 @@ Repository = "https://github.com/yourusername/agent-control" [project.optional-dependencies] strands-agents = ["strands-agents>=1.26.0"] google-adk = ["google-adk>=1.0.0"] -galileo = ["agent-control-evaluator-galileo>=3.0.0"] +galileo = ["agent-control-evaluator-galileo>=7.0.0"] [dependency-groups] dev = [ @@ -46,6 +46,7 @@ dev = [ "pytest-cov>=4.0.0", "ruff>=0.1.0", "mypy>=1.8.0", + "agent-control-server", "agent-control-models", "agent-control-engine", "agent-control-evaluators", @@ -78,6 +79,7 @@ known-first-party = ["agent_control"] # For local development, use workspace packages [tool.uv.sources] +agent-control-server = { workspace = true } agent-control-models = { workspace = true } agent-control-engine = { workspace = true } agent-control-evaluators = { workspace = true } diff --git a/sdks/python/src/agent_control/__init__.py b/sdks/python/src/agent_control/__init__.py index dd1a38be..b3e41d90 100644 --- a/sdks/python/src/agent_control/__init__.py +++ b/sdks/python/src/agent_control/__init__.py @@ -947,7 +947,7 @@ async def create_control( Args: name: Unique name for the control - data: Optional control definition with selector, evaluator, action, etc. + data: Optional control definition with a condition tree, action, scope, etc. server_url: Optional server URL (defaults to AGENT_CONTROL_URL env var) api_key: Optional API key for authentication (defaults to AGENT_CONTROL_API_KEY env var) @@ -972,10 +972,12 @@ async def main(): data={ "execution": "server", "scope": {"step_types": ["llm"], "stages": ["post"]}, - "selector": {"path": "output"}, - "evaluator": { - "name": "regex", - "config": {"pattern": r"\\d{3}-\\d{2}-\\d{4}"} + "condition": { + "selector": {"path": "output"}, + "evaluator": { + "name": "regex", + "config": {"pattern": r"\\d{3}-\\d{2}-\\d{4}"} + } }, "action": {"decision": "deny"} } diff --git a/sdks/python/src/agent_control/client.py b/sdks/python/src/agent_control/client.py index 208d1a50..9721eed0 100644 --- a/sdks/python/src/agent_control/client.py +++ b/sdks/python/src/agent_control/client.py @@ -1,10 +1,15 @@ """Base HTTP client for Agent Control server communication.""" +import logging import os from types import TracebackType import httpx +from . import __version__ as sdk_version + +_logger = logging.getLogger(__name__) + class AgentControlClient: """ @@ -53,6 +58,7 @@ def __init__( self.timeout = timeout self._api_key = api_key or os.environ.get(self.API_KEY_ENV_VAR) self._client: httpx.AsyncClient | None = None + self._server_version_warning_emitted = False @property def api_key(self) -> str | None: @@ -61,17 +67,43 @@ def api_key(self) -> str | None: def _get_headers(self) -> dict[str, str]: """Build request headers including authentication.""" - headers: dict[str, str] = {} + headers: dict[str, str] = { + "X-Agent-Control-SDK": "python", + "X-Agent-Control-SDK-Version": sdk_version, + } if self._api_key: headers["X-API-Key"] = self._api_key return headers + async def _check_server_version(self, response: httpx.Response) -> None: + """Warn once when the server major version differs from the SDK major.""" + if self._server_version_warning_emitted: + return + + server_version = response.headers.get("X-Agent-Control-Server-Version") + if not server_version: + return + + sdk_major = sdk_version.split(".", 1)[0] + server_major = server_version.split(".", 1)[0] + if sdk_major == server_major: + return + + _logger.warning( + "Agent Control SDK major version %s is talking to server major version %s. " + "Upgrade the SDK and server together to avoid control-schema mismatches.", + sdk_version, + server_version, + ) + self._server_version_warning_emitted = True + async def __aenter__(self) -> "AgentControlClient": """Async context manager entry.""" self._client = httpx.AsyncClient( base_url=self.base_url, timeout=self.timeout, headers=self._get_headers(), + event_hooks={"response": [self._check_server_version]}, ) return self @@ -108,4 +140,3 @@ def http_client(self) -> httpx.AsyncClient: if self._client is None: raise RuntimeError("Client not initialized. Use 'async with' context manager.") return self._client - diff --git a/sdks/python/src/agent_control/control_decorators.py b/sdks/python/src/agent_control/control_decorators.py index 569aecaa..1da113b1 100644 --- a/sdks/python/src/agent_control/control_decorators.py +++ b/sdks/python/src/agent_control/control_decorators.py @@ -5,7 +5,7 @@ Controls can be associated via policies and direct agent-control links. Architecture: - SERVER defines: Policies -> Controls (stage, selector, evaluator, action) + SERVER defines: Policies -> Controls (stage, condition tree, action) SDK decorator: just marks WHERE controls are evaluated Usage: @@ -744,7 +744,7 @@ def control(policy: str | None = None, step_name: str | None = None) -> Callable """ Decorator to apply server-defined controls at this code location. - Controls (stage, selector, evaluator, action) are defined on the SERVER. + Controls (stage, condition tree, action) are defined on the SERVER. This decorator marks WHERE to evaluate controls for the current agent. Args: diff --git a/sdks/python/src/agent_control/controls.py b/sdks/python/src/agent_control/controls.py index 7050cc5a..697ef60d 100644 --- a/sdks/python/src/agent_control/controls.py +++ b/sdks/python/src/agent_control/controls.py @@ -97,7 +97,7 @@ async def get_control( Dictionary containing: - id: Control ID - name: Control name - - data: Control definition (selector, evaluator, action) or None if not configured + - data: Control definition (condition, action, scope, etc.) or None if not configured Raises: httpx.HTTPError: If request fails @@ -129,7 +129,7 @@ async def create_control( Args: client: AgentControlClient instance name: Unique name for the control - data: Optional control definition (selector, evaluator, action, etc.) + data: Optional control definition (condition tree, action, scope, etc.) Returns: Dictionary containing: @@ -155,10 +155,12 @@ async def create_control( data={ "execution": "server", "scope": {"step_types": ["llm"], "stages": ["post"]}, - "selector": {"path": "output"}, - "evaluator": { - "name": "regex", - "config": {"pattern": r"\\d{3}-\\d{2}-\\d{4}"} + "condition": { + "selector": {"path": "output"}, + "evaluator": { + "name": "regex", + "config": {"pattern": r"\\d{3}-\\d{2}-\\d{4}"} + } }, "action": {"decision": "deny"} } @@ -192,7 +194,7 @@ async def set_control_data( """ Set the configuration data for a control. - This defines what the control actually does (selector, evaluator, action). + This defines what the control actually does (condition tree, action, scope). Args: client: AgentControlClient instance diff --git a/sdks/python/src/agent_control/evaluation.py b/sdks/python/src/agent_control/evaluation.py index e30bb3e2..1174b96b 100644 --- a/sdks/python/src/agent_control/evaluation.py +++ b/sdks/python/src/agent_control/evaluation.py @@ -30,6 +30,19 @@ _FALLBACK_SPAN_ID = "0" * 16 _trace_warning_logged = False + +def _primary_leaf_details( + control_def: ControlDefinition, +) -> tuple[str | None, str | None]: + """Return selector/evaluator identifiers for single-leaf controls only.""" + primary_leaf = control_def.primary_leaf() + primary_parts = primary_leaf.leaf_parts() if primary_leaf else None + if primary_parts is None: + return None, None + selector, evaluator = primary_parts + return selector.path, evaluator.name + + def _map_applies_to(step_type: str) -> Literal["llm_call", "tool_call"]: return "tool_call" if step_type == "tool" else "llm_call" @@ -77,6 +90,9 @@ def _emit_matches(matches: list[ControlMatch] | None, matched: bool) -> None: return for match in matches: ctrl = control_lookup.get(match.control_id) + selector_path, evaluator_name = ( + _primary_leaf_details(ctrl.control) if ctrl else (None, None) + ) add_event( ControlExecutionEvent( control_execution_id=match.control_execution_id, @@ -91,8 +107,8 @@ def _emit_matches(matches: list[ControlMatch] | None, matched: bool) -> None: matched=matched, confidence=match.result.confidence, timestamp=now, - evaluator_name=ctrl.control.evaluator.name if ctrl else None, - selector_path=ctrl.control.selector.path if ctrl else None, + evaluator_name=evaluator_name, + selector_path=selector_path, error_message=match.result.error if not matched else None, metadata=match.result.metadata or {}, ) @@ -225,20 +241,24 @@ async def check_evaluation_with_local( try: control_def = ControlDefinition.model_validate(control_data) - evaluator_name = control_def.evaluator.name - - if ":" in evaluator_name: - raise RuntimeError( - f"Control '{control['name']}' is marked execution='sdk' but uses " - f"agent-scoped evaluator '{evaluator_name}' which is server-only. " - "Set execution='server' or use a built-in evaluator." - ) - if evaluator_name not in list_evaluators(): - raise RuntimeError( - f"Control '{control['name']}' is marked execution='sdk' but evaluator " - f"'{evaluator_name}' is not available in the SDK. " - "Install the evaluator or set execution='server'." - ) + for leaf in control_def.iter_condition_leaves(): + _, evaluator_spec = leaf.leaf_parts() or (None, None) + if evaluator_spec is None: + continue + evaluator_name = evaluator_spec.name + + if ":" in evaluator_name: + raise RuntimeError( + f"Control '{control['name']}' is marked execution='sdk' but uses " + f"agent-scoped evaluator '{evaluator_name}' which is server-only. " + "Set execution='server' or use a built-in evaluator." + ) + if evaluator_name not in list_evaluators(): + raise RuntimeError( + f"Control '{control['name']}' is marked execution='sdk' but evaluator " + f"'{evaluator_name}' is not available in the SDK. " + "Install the evaluator or set execution='server'." + ) local_controls.append( _ControlAdapter( diff --git a/sdks/python/tests/test_integration_agents.py b/sdks/python/tests/test_integration_agents.py index 66a22c43..04ee5f86 100644 --- a/sdks/python/tests/test_integration_agents.py +++ b/sdks/python/tests/test_integration_agents.py @@ -214,10 +214,12 @@ async def test_convenience_agent_association_functions( "enabled": True, "execution": "server", "scope": {"step_types": ["tool"], "stages": ["pre"]}, - "selector": {"path": "input"}, - "evaluator": { - "name": "regex", - "config": {"pattern": ".*"}, + "condition": { + "selector": {"path": "input"}, + "evaluator": { + "name": "regex", + "config": {"pattern": ".*"}, + }, }, "action": {"decision": "allow"}, "tags": ["test"], diff --git a/sdks/python/tests/test_local_evaluation.py b/sdks/python/tests/test_local_evaluation.py index 94661ab0..95c0e07d 100644 --- a/sdks/python/tests/test_local_evaluation.py +++ b/sdks/python/tests/test_local_evaluation.py @@ -75,10 +75,12 @@ def make_control_dict( "enabled": True, "execution": execution, "scope": {"step_types": [step_type], "stages": [stage]}, - "selector": {"path": path}, - "evaluator": { - "name": evaluator, - "config": {"pattern": pattern}, + "condition": { + "selector": {"path": path}, + "evaluator": { + "name": evaluator, + "config": {"pattern": pattern}, + }, }, "action": {"decision": action}, }, @@ -766,8 +768,10 @@ async def test_local_evaluation_includes_steering_context(self, agent_name, llm_ "enabled": True, "execution": "sdk", "scope": {"step_types": ["llm"], "stages": ["pre"]}, - "selector": {"path": "input"}, - "evaluator": {"name": "regex", "config": {"pattern": "test"}}, + "condition": { + "selector": {"path": "input"}, + "evaluator": {"name": "regex", "config": {"pattern": "test"}}, + }, "action": { "decision": "steer", "steering_context": { diff --git a/sdks/python/tests/test_observability_updates.py b/sdks/python/tests/test_observability_updates.py index a99d6b7a..cc2a80ba 100644 --- a/sdks/python/tests/test_observability_updates.py +++ b/sdks/python/tests/test_observability_updates.py @@ -116,8 +116,10 @@ def _make_control_adapter(self, id, name, evaluator_name="regex", selector_path= control_def = ControlDefinition( execution="sdk", - evaluator={"name": evaluator_name, "config": {"pattern": "test"}}, - selector={"path": selector_path}, + condition={ + "evaluator": {"name": evaluator_name, "config": {"pattern": "test"}}, + "selector": {"path": selector_path}, + }, action={"decision": "deny"}, ) return _ControlAdapter(id=id, name=name, control=control_def) @@ -307,8 +309,10 @@ async def test_emits_events_when_trace_context_provided(self): "id": 1, "name": "test-ctrl", "control": { - "evaluator": {"name": "regex", "config": {"pattern": "test"}}, - "selector": {"path": "input"}, + "condition": { + "evaluator": {"name": "regex", "config": {"pattern": "test"}}, + "selector": {"path": "input"}, + }, "action": {"decision": "allow"}, "execution": "sdk", }, @@ -359,8 +363,10 @@ async def test_emits_events_without_trace_context(self): "id": 1, "name": "test-ctrl", "control": { - "evaluator": {"name": "regex", "config": {"pattern": "test"}}, - "selector": {"path": "input"}, + "condition": { + "evaluator": {"name": "regex", "config": {"pattern": "test"}}, + "selector": {"path": "input"}, + }, "action": {"decision": "allow"}, "execution": "sdk", }, @@ -396,8 +402,10 @@ async def test_forwards_trace_headers_to_server(self): "id": 1, "name": "server-ctrl", "control": { - "evaluator": {"name": "regex", "config": {"pattern": "test"}}, - "selector": {"path": "input"}, + "condition": { + "evaluator": {"name": "regex", "config": {"pattern": "test"}}, + "selector": {"path": "input"}, + }, "action": {"decision": "deny"}, "execution": "server", }, diff --git a/sdks/typescript/gen.yaml b/sdks/typescript/gen.yaml index cadd9170..2e4bc54b 100644 --- a/sdks/typescript/gen.yaml +++ b/sdks/typescript/gen.yaml @@ -10,7 +10,7 @@ generation: parameterOrderingFeb2024: true requestResponseComponentNamesFeb2024: true typescript: - version: 0.1.0 + version: 2.0.0 packageName: agent-control author: Agent Control moduleFormat: esm diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index 7ff6bdb6..b89cbf6c 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -1,6 +1,6 @@ { "name": "agent-control", - "version": "1.1.0", + "version": "2.0.0", "private": false, "description": "TypeScript SDK for Agent Control", "license": "Apache-2.0", diff --git a/sdks/typescript/src/generated/lib/config.ts b/sdks/typescript/src/generated/lib/config.ts index aacf1282..413cf8c5 100644 --- a/sdks/typescript/src/generated/lib/config.ts +++ b/sdks/typescript/src/generated/lib/config.ts @@ -57,8 +57,8 @@ export function serverURLFromOptions(options: SDKOptions): URL | null { export const SDK_METADATA = { language: "typescript", - openapiDocVersion: "0.1.0", - sdkVersion: "0.1.0", + openapiDocVersion: "7.0.0", + sdkVersion: "2.0.0", genVersion: "2.827.0", - userAgent: "speakeasy-sdk/typescript 0.1.0 2.827.0 0.1.0 agent-control", + userAgent: "speakeasy-sdk/typescript 2.0.0 2.827.0 7.0.0 agent-control", } as const; diff --git a/sdks/typescript/src/generated/models/condition-node-input.ts b/sdks/typescript/src/generated/models/condition-node-input.ts new file mode 100644 index 00000000..a156596b --- /dev/null +++ b/sdks/typescript/src/generated/models/condition-node-input.ts @@ -0,0 +1,74 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { + ControlSelector, + ControlSelector$Outbound, + ControlSelector$outboundSchema, +} from "./control-selector.js"; +import { + EvaluatorSpec, + EvaluatorSpec$Outbound, + EvaluatorSpec$outboundSchema, +} from "./evaluator-spec.js"; + +/** + * Recursive boolean condition tree for control evaluation. + */ +export type ConditionNodeInput = { + /** + * Logical AND over child conditions. + */ + and?: Array | null | undefined; + /** + * Leaf evaluator. Must be provided together with selector. + */ + evaluator?: EvaluatorSpec | null | undefined; + /** + * Logical NOT over a single child condition. + */ + not?: ConditionNodeInput | null | undefined; + /** + * Logical OR over child conditions. + */ + or?: Array | null | undefined; + /** + * Leaf selector. Must be provided together with evaluator. + */ + selector?: ControlSelector | null | undefined; +}; + +/** @internal */ +export type ConditionNodeInput$Outbound = { + and?: Array | null | undefined; + evaluator?: EvaluatorSpec$Outbound | null | undefined; + not?: ConditionNodeInput$Outbound | null | undefined; + or?: Array | null | undefined; + selector?: ControlSelector$Outbound | null | undefined; +}; + +/** @internal */ +export const ConditionNodeInput$outboundSchema: z.ZodMiniType< + ConditionNodeInput$Outbound, + ConditionNodeInput +> = z.object({ + and: z.optional( + z.nullable(z.array(z.lazy(() => ConditionNodeInput$outboundSchema))), + ), + evaluator: z.optional(z.nullable(EvaluatorSpec$outboundSchema)), + not: z.optional(z.nullable(z.lazy(() => ConditionNodeInput$outboundSchema))), + or: z.optional( + z.nullable(z.array(z.lazy(() => ConditionNodeInput$outboundSchema))), + ), + selector: z.optional(z.nullable(ControlSelector$outboundSchema)), +}); + +export function conditionNodeInputToJSON( + conditionNodeInput: ConditionNodeInput, +): string { + return JSON.stringify( + ConditionNodeInput$outboundSchema.parse(conditionNodeInput), + ); +} diff --git a/sdks/typescript/src/generated/models/condition-node-output.ts b/sdks/typescript/src/generated/models/condition-node-output.ts new file mode 100644 index 00000000..d0320504 --- /dev/null +++ b/sdks/typescript/src/generated/models/condition-node-output.ts @@ -0,0 +1,68 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { safeParse } from "../lib/schemas.js"; +import { Result as SafeParseResult } from "../types/fp.js"; +import { + ControlSelector, + ControlSelector$inboundSchema, +} from "./control-selector.js"; +import { SDKValidationError } from "./errors/sdk-validation-error.js"; +import { + EvaluatorSpec, + EvaluatorSpec$inboundSchema, +} from "./evaluator-spec.js"; + +/** + * Recursive boolean condition tree for control evaluation. + */ +export type ConditionNodeOutput = { + /** + * Logical AND over child conditions. + */ + and?: Array | null | undefined; + /** + * Leaf evaluator. Must be provided together with selector. + */ + evaluator?: EvaluatorSpec | null | undefined; + /** + * Logical NOT over a single child condition. + */ + not?: ConditionNodeOutput | null | undefined; + /** + * Logical OR over child conditions. + */ + or?: Array | null | undefined; + /** + * Leaf selector. Must be provided together with evaluator. + */ + selector?: ControlSelector | null | undefined; +}; + +/** @internal */ +export const ConditionNodeOutput$inboundSchema: z.ZodMiniType< + ConditionNodeOutput, + unknown +> = z.object({ + and: z.optional( + z.nullable(z.array(z.lazy(() => ConditionNodeOutput$inboundSchema))), + ), + evaluator: z.optional(z.nullable(EvaluatorSpec$inboundSchema)), + not: z.optional(z.nullable(z.lazy(() => ConditionNodeOutput$inboundSchema))), + or: z.optional( + z.nullable(z.array(z.lazy(() => ConditionNodeOutput$inboundSchema))), + ), + selector: z.optional(z.nullable(ControlSelector$inboundSchema)), +}); + +export function conditionNodeOutputFromJSON( + jsonString: string, +): SafeParseResult { + return safeParse( + jsonString, + (x) => ConditionNodeOutput$inboundSchema.parse(JSON.parse(x)), + `Failed to parse 'ConditionNodeOutput' from JSON`, + ); +} diff --git a/sdks/typescript/src/generated/models/control-definition-input.ts b/sdks/typescript/src/generated/models/control-definition-input.ts index a5c5c352..f385b0d5 100644 --- a/sdks/typescript/src/generated/models/control-definition-input.ts +++ b/sdks/typescript/src/generated/models/control-definition-input.ts @@ -4,6 +4,11 @@ import * as z from "zod/v4-mini"; import { ClosedEnum } from "../types/enums.js"; +import { + ConditionNodeInput, + ConditionNodeInput$Outbound, + ConditionNodeInput$outboundSchema, +} from "./condition-node-input.js"; import { ControlAction, ControlAction$Outbound, @@ -14,16 +19,6 @@ import { ControlScope$Outbound, ControlScope$outboundSchema, } from "./control-scope.js"; -import { - ControlSelector, - ControlSelector$Outbound, - ControlSelector$outboundSchema, -} from "./control-selector.js"; -import { - EvaluatorSpec, - EvaluatorSpec$Outbound, - EvaluatorSpec$outboundSchema, -} from "./evaluator-spec.js"; /** * Where this control executes @@ -52,6 +47,10 @@ export type ControlDefinitionInput = { * What to do when control matches. */ action: ControlAction; + /** + * Recursive boolean condition tree for control evaluation. + */ + condition: ConditionNodeInput; /** * Detailed description of the control */ @@ -60,17 +59,6 @@ export type ControlDefinitionInput = { * Whether this control is active */ enabled?: boolean | undefined; - /** - * Evaluator specification. See GET /evaluators for available evaluators and schemas. - * - * @remarks - * - * Evaluator reference formats: - * - Built-in: "regex", "list", "json", "sql" - * - External: "galileo.luna2" (requires agent-control-evaluators[galileo]) - * - Agent-scoped: "my-agent:my-evaluator" (validated in endpoint, not here) - */ - evaluator: EvaluatorSpec; /** * Where this control executes */ @@ -79,15 +67,6 @@ export type ControlDefinitionInput = { * Defines when a control applies to a Step. */ scope?: ControlScope | undefined; - /** - * Selects data from a Step payload. - * - * @remarks - * - * - path: which slice of the Step to feed into the evaluator. Optional, defaults to "*" - * meaning the entire Step object. - */ - selector: ControlSelector; /** * Tags for categorization */ @@ -102,12 +81,11 @@ export const ControlDefinitionInputExecution$outboundSchema: z.ZodMiniEnum< /** @internal */ export type ControlDefinitionInput$Outbound = { action: ControlAction$Outbound; + condition: ConditionNodeInput$Outbound; description?: string | null | undefined; enabled: boolean; - evaluator: EvaluatorSpec$Outbound; execution: string; scope?: ControlScope$Outbound | undefined; - selector: ControlSelector$Outbound; tags?: Array | undefined; }; @@ -117,12 +95,11 @@ export const ControlDefinitionInput$outboundSchema: z.ZodMiniType< ControlDefinitionInput > = z.object({ action: ControlAction$outboundSchema, + condition: ConditionNodeInput$outboundSchema, description: z.optional(z.nullable(z.string())), enabled: z._default(z.boolean(), true), - evaluator: EvaluatorSpec$outboundSchema, execution: ControlDefinitionInputExecution$outboundSchema, scope: z.optional(ControlScope$outboundSchema), - selector: ControlSelector$outboundSchema, tags: z.optional(z.array(z.string())), }); diff --git a/sdks/typescript/src/generated/models/control-definition-output.ts b/sdks/typescript/src/generated/models/control-definition-output.ts index ce4e2714..8199d5f3 100644 --- a/sdks/typescript/src/generated/models/control-definition-output.ts +++ b/sdks/typescript/src/generated/models/control-definition-output.ts @@ -8,20 +8,16 @@ import * as openEnums from "../types/enums.js"; import { OpenEnum } from "../types/enums.js"; import { Result as SafeParseResult } from "../types/fp.js"; import * as types from "../types/primitives.js"; +import { + ConditionNodeOutput, + ConditionNodeOutput$inboundSchema, +} from "./condition-node-output.js"; import { ControlAction, ControlAction$inboundSchema, } from "./control-action.js"; import { ControlScope, ControlScope$inboundSchema } from "./control-scope.js"; -import { - ControlSelector, - ControlSelector$inboundSchema, -} from "./control-selector.js"; import { SDKValidationError } from "./errors/sdk-validation-error.js"; -import { - EvaluatorSpec, - EvaluatorSpec$inboundSchema, -} from "./evaluator-spec.js"; /** * Where this control executes @@ -48,6 +44,10 @@ export type ControlDefinitionOutput = { * What to do when control matches. */ action: ControlAction; + /** + * Recursive boolean condition tree for control evaluation. + */ + condition: ConditionNodeOutput; /** * Detailed description of the control */ @@ -56,17 +56,6 @@ export type ControlDefinitionOutput = { * Whether this control is active */ enabled: boolean; - /** - * Evaluator specification. See GET /evaluators for available evaluators and schemas. - * - * @remarks - * - * Evaluator reference formats: - * - Built-in: "regex", "list", "json", "sql" - * - External: "galileo.luna2" (requires agent-control-evaluators[galileo]) - * - Agent-scoped: "my-agent:my-evaluator" (validated in endpoint, not here) - */ - evaluator: EvaluatorSpec; /** * Where this control executes */ @@ -75,15 +64,6 @@ export type ControlDefinitionOutput = { * Defines when a control applies to a Step. */ scope?: ControlScope | undefined; - /** - * Selects data from a Step payload. - * - * @remarks - * - * - path: which slice of the Step to feed into the evaluator. Optional, defaults to "*" - * meaning the entire Step object. - */ - selector: ControlSelector; /** * Tags for categorization */ @@ -100,12 +80,11 @@ export const ControlDefinitionOutput$inboundSchema: z.ZodMiniType< unknown > = z.object({ action: ControlAction$inboundSchema, + condition: ConditionNodeOutput$inboundSchema, description: z.optional(z.nullable(types.string())), enabled: z._default(types.boolean(), true), - evaluator: EvaluatorSpec$inboundSchema, execution: Execution$inboundSchema, scope: types.optional(ControlScope$inboundSchema), - selector: ControlSelector$inboundSchema, tags: types.optional(z.array(types.string())), }); diff --git a/sdks/typescript/src/generated/models/index.ts b/sdks/typescript/src/generated/models/index.ts index 53465775..aba5b19f 100644 --- a/sdks/typescript/src/generated/models/index.ts +++ b/sdks/typescript/src/generated/models/index.ts @@ -10,6 +10,8 @@ export * from "./assoc-response.js"; export * from "./auth-mode.js"; export * from "./batch-events-request.js"; export * from "./batch-events-response.js"; +export * from "./condition-node-input.js"; +export * from "./condition-node-output.js"; export * from "./config-response.js"; export * from "./conflict-mode.js"; export * from "./control-action.js"; diff --git a/server/Makefile b/server/Makefile index 3aae2898..1acd50e9 100644 --- a/server/Makefile +++ b/server/Makefile @@ -9,7 +9,9 @@ SHOW ?= head STAMP ?= head TEST_DB ?= agent_control_test -.PHONY: help run start-dependencies test migrate alembic-migrate alembic-revision alembic-upgrade alembic-downgrade alembic-current alembic-history alembic-heads alembic-show alembic-stamp +.PHONY: help run start-dependencies test migrate migrate-control-conditions alembic-migrate alembic-revision alembic-upgrade alembic-downgrade alembic-current alembic-history alembic-heads alembic-show alembic-stamp + +MIGRATE_ARGS ?= --dry-run help: @echo "Available targets:" @@ -17,6 +19,7 @@ help: @echo " start-dependencies - docker compose up -d (start local dependencies)" @echo " test - run server tests (uses DB_DATABASE=$(TEST_DB))" @echo " migrate - run database migrations (alembic upgrade head)" + @echo " migrate-control-conditions - rewrite stored controls to condition trees (default dry-run; set MIGRATE_ARGS=--apply to commit)" @echo " alembic-migrate MSG='message' - autogenerate alembic revision" @echo " alembic-upgrade UP=head - upgrade to revision" @echo " alembic-downgrade DOWN=-1 - downgrade to revision" @@ -30,6 +33,9 @@ migrate: @echo "Running database migrations..." $(ALEMBIC) upgrade head +migrate-control-conditions: + uv run --package agent-control-server agent-control-migrate-controls $(MIGRATE_ARGS) + alembic-migrate: $(ALEMBIC) revision --autogenerate -m "$(MSG)" diff --git a/server/pyproject.toml b/server/pyproject.toml index 40bbe243..77f0cc07 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agent-control-server" -version = "6.7.2" +version = "7.0.0" description = "Server for Agent Control - manage and evaluate controls for AI agents" requires-python = ">=3.12" # Note: agent-control-models and agent-control-engine are bundled at build time @@ -21,7 +21,7 @@ dependencies = [ "jsonschema-rs>=0.22.0", "PyJWT>=2.8.0", "google-re2>=1.1", # For engine (bundled) - "agent-control-evaluators>=3.0.0", # NOT vendored - avoid conflict with galileo + "agent-control-evaluators>=7.0.0", # NOT vendored - avoid conflict with galileo ] authors = [ {name = "Agent Control Team"} @@ -30,7 +30,7 @@ readme = "README.md" license = {text = "Apache-2.0"} [project.optional-dependencies] -galileo = ["agent-control-evaluator-galileo>=3.0.0"] +galileo = ["agent-control-evaluator-galileo>=7.0.0"] [dependency-groups] dev = [ @@ -48,6 +48,7 @@ dev = [ [project.scripts] agent-control-server = "agent_control_server.main:run" +agent-control-migrate-controls = "agent_control_server.scripts.migrate_control_conditions:main" diff --git a/server/src/agent_control_server/endpoints/agents.py b/server/src/agent_control_server/endpoints/agents.py index 23d3a547..4f883596 100644 --- a/server/src/agent_control_server/endpoints/agents.py +++ b/server/src/agent_control_server/endpoints/agents.py @@ -3,6 +3,7 @@ from agent_control_engine import list_evaluators from agent_control_models.agent import Agent as APIAgent from agent_control_models.agent import StepSchema +from agent_control_models.controls import ControlDefinition from agent_control_models.errors import ErrorCode, ValidationErrorItem from agent_control_models.server import ( AgentControlsResponse, @@ -109,43 +110,52 @@ def _validate_controls_for_agent(agent: Agent, controls: list[Control]) -> list[ if not control.data: continue - evaluator_cfg = control.data.get("evaluator", {}) - evaluator_name = evaluator_cfg.get("name", "") - if not evaluator_name: + try: + control_definition = ControlDefinition.model_validate(control.data) + except ValidationError: + errors.append(f"Control '{control.name}' has corrupted data") continue - parsed = parse_evaluator_ref_full(evaluator_name) - if parsed.type != "agent": - continue # Built-in/external evaluator, already validated at control creation + for leaf in control_definition.iter_condition_leaves(): + _, evaluator_cfg = leaf.leaf_parts() or (None, None) + if evaluator_cfg is None: + continue - # Agent-scoped evaluator - check if target matches this agent - if parsed.namespace != agent.name: - errors.append( - f"Control '{control.name}' references evaluator '{evaluator_name}' " - f"which belongs to agent '{parsed.namespace}', not '{agent.name}'" - ) - continue + evaluator_name = evaluator_cfg.name + parsed = parse_evaluator_ref_full(evaluator_name) + if parsed.type != "agent": + continue # Built-in/external evaluator, already validated at control creation - # Check if evaluator exists on this agent - if parsed.local_name not in agent_evaluators: - errors.append( - f"Control '{control.name}' references evaluator '{parsed.local_name}' " - f"which is not registered with agent '{agent.name}'. " - f"Register it via initAgent or use a different evaluator." - ) - continue + # Agent-scoped evaluator - check if target matches this agent + if parsed.namespace != agent.name: + errors.append( + f"Control '{control.name}' references evaluator '{evaluator_name}' " + f"which belongs to agent '{parsed.namespace}', not '{agent.name}'" + ) + continue - # Validate config against schema - registered_ev = agent_evaluators[parsed.local_name] - config = evaluator_cfg.get("config", {}) - if registered_ev.config_schema: - try: - validate_config_against_schema(config, registered_ev.config_schema) - except JSONSchemaValidationError as e: + # Check if evaluator exists on this agent + if parsed.local_name not in agent_evaluators: errors.append( - f"Control '{control.name}' invalid config for " - f"'{parsed.local_name}': {e.message}" + f"Control '{control.name}' references evaluator '{parsed.local_name}' " + f"which is not registered with agent '{agent.name}'. " + f"Register it via initAgent or use a different evaluator." ) + continue + + # Validate config against schema + registered_ev = agent_evaluators[parsed.local_name] + if registered_ev.config_schema: + try: + validate_config_against_schema( + evaluator_cfg.config, + registered_ev.config_schema, + ) + except JSONSchemaValidationError as e: + errors.append( + f"Control '{control.name}' invalid config for " + f"'{parsed.local_name}': {e.message}" + ) return errors @@ -200,15 +210,21 @@ async def _build_overwrite_evaluator_removals( references_by_evaluator: dict[str, list[tuple[int, str]]] = {} for control in controls: - evaluator_ref = control.control.evaluator.name - parsed = parse_evaluator_ref_full(evaluator_ref) - if parsed.type != "agent": - continue - if parsed.namespace != agent.name: - continue - if parsed.local_name not in removed_evaluators: - continue - references_by_evaluator.setdefault(parsed.local_name, []).append((control.id, control.name)) + for leaf in control.control.iter_condition_leaves(): + _, evaluator_spec = leaf.leaf_parts() or (None, None) + if evaluator_spec is None: + continue + evaluator_ref = evaluator_spec.name + parsed = parse_evaluator_ref_full(evaluator_ref) + if parsed.type != "agent": + continue + if parsed.namespace != agent.name: + continue + if parsed.local_name not in removed_evaluators: + continue + references_by_evaluator.setdefault(parsed.local_name, []).append( + (control.id, control.name) + ) removals: list[InitAgentEvaluatorRemoval] = [] for evaluator_name in sorted(removed_evaluators): @@ -1616,11 +1632,14 @@ async def patch_agent( referencing_controls: list[tuple[str, str]] = [] # (control_name, evaluator) for ctrl in controls: - evaluator_ref = ctrl.control.evaluator.name - if ":" in evaluator_ref: + for leaf in ctrl.control.iter_condition_leaves(): + _, evaluator_spec = leaf.leaf_parts() or (None, None) + if evaluator_spec is None: + continue + evaluator_ref = evaluator_spec.name + if ":" not in evaluator_ref: + continue ref_agent, ref_eval = evaluator_ref.split(":", 1) - # Check if this control references an evaluator we're removing - # AND it's scoped to this agent (by name match) if ref_agent == agent.name and ref_eval in remove_evaluator_set: referencing_controls.append((ctrl.name, ref_eval)) diff --git a/server/src/agent_control_server/endpoints/controls.py b/server/src/agent_control_server/endpoints/controls.py index 7957c377..3d6aa7a0 100644 --- a/server/src/agent_control_server/endpoints/controls.py +++ b/server/src/agent_control_server/endpoints/controls.py @@ -1,5 +1,7 @@ +from collections.abc import Iterator + from agent_control_engine import list_evaluators -from agent_control_models import ControlDefinition +from agent_control_models import ConditionNode, ControlDefinition from agent_control_models.errors import ErrorCode, ValidationErrorItem from agent_control_models.server import ( AgentRef, @@ -40,6 +42,7 @@ validate_config_against_schema, ) from ..services.query_utils import escape_like_pattern +from ..services.validation_paths import format_field_path # Pagination constants _DEFAULT_PAGINATION_LIMIT = 20 @@ -53,147 +56,178 @@ _logger = get_logger(__name__) +def _iter_condition_leaves( + node: ConditionNode, + *, + path: str = "data.condition", +) -> Iterator[tuple[str, ConditionNode]]: + """Yield each leaf condition with its dot/bracket field path.""" + if node.is_leaf(): + yield path, node + return + + if node.and_ is not None: + for index, child in enumerate(node.and_): + yield from _iter_condition_leaves(child, path=f"{path}.and[{index}]") + return + + if node.or_ is not None: + for index, child in enumerate(node.or_): + yield from _iter_condition_leaves(child, path=f"{path}.or[{index}]") + return + + if node.not_ is not None: + yield from _iter_condition_leaves(node.not_, path=f"{path}.not") + + async def _validate_control_definition( control_def: ControlDefinition, db: AsyncSession ) -> None: """Validate evaluator config for a control definition.""" - evaluator_ref = control_def.evaluator.name - parsed = parse_evaluator_ref_full(evaluator_ref) + for field_prefix, leaf in _iter_condition_leaves(control_def.condition): + _, evaluator_spec = leaf.leaf_parts() or (None, None) + if evaluator_spec is None: + continue - if parsed.type == "agent": - # Agent-scoped evaluator: validate against agent's registered schema - agent_result = await db.execute( - select(Agent).where(Agent.name == parsed.namespace) - ) - agent = agent_result.scalars().first() - if agent is None: - raise NotFoundError( - error_code=ErrorCode.AGENT_NOT_FOUND, - detail=f"Agent '{parsed.namespace}' not found", - resource="Agent", - resource_id=parsed.namespace, - hint=( - "Ensure the agent exists before creating controls " - "that reference its evaluators." - ), - ) + evaluator_ref = evaluator_spec.name + parsed = parse_evaluator_ref_full(evaluator_ref) - try: - agent_data = AgentData.model_validate(agent.data) - except ValidationError as e: - raise APIValidationError( - error_code=ErrorCode.CORRUPTED_DATA, - detail=f"Agent '{parsed.namespace}' has invalid data", - resource="Agent", - errors=[ - ValidationErrorItem( - resource="Agent", - field=".".join(str(loc) for loc in err.get("loc", [])), - code=err.get("type", "validation_error"), - message=err.get("msg", "Validation failed"), - ) - for err in e.errors() - ], + if parsed.type == "agent": + agent_result = await db.execute( + select(Agent).where(Agent.name == parsed.namespace) ) - - evaluator = next( - (e for e in (agent_data.evaluators or []) if e.name == parsed.local_name), - None, - ) - if evaluator is None: - available = [e.name for e in (agent_data.evaluators or [])] - raise APIValidationError( - error_code=ErrorCode.EVALUATOR_NOT_FOUND, - detail=( - f"Evaluator '{parsed.local_name}' is not registered " - f"with agent '{parsed.namespace}'" - ), - resource="Evaluator", - hint=( - f"Register it via initAgent first. " - f"Available evaluators: {available or 'none'}." - ), - errors=[ - ValidationErrorItem( - resource="Control", - field="data.evaluator.name", - code="evaluator_not_found", - message=( - f"Evaluator '{parsed.local_name}' not found " - f"on agent '{parsed.namespace}'" - ), - value=evaluator_ref, - ) - ], - ) - - # Validate config against evaluator's schema - if evaluator.config_schema: - try: - validate_config_against_schema( - control_def.evaluator.config, evaluator.config_schema - ) - except JSONSchemaValidationError: - raise APIValidationError( - error_code=ErrorCode.INVALID_CONFIG, - detail=f"Config validation failed for evaluator '{evaluator_ref}'", - resource="Control", - hint="Check the evaluator's config schema for required fields and types.", - errors=[ - ValidationErrorItem( - resource="Control", - field="data.evaluator.config", - code="schema_validation_error", - message=_SCHEMA_VALIDATION_FAILED_MESSAGE, - ) - ], + agent = agent_result.scalars().first() + if agent is None: + raise NotFoundError( + error_code=ErrorCode.AGENT_NOT_FOUND, + detail=f"Agent '{parsed.namespace}' not found", + resource="Agent", + resource_id=parsed.namespace, + hint=( + "Ensure the agent exists before creating controls " + "that reference its evaluators." + ), ) - else: - # Built-in or external evaluator: validate if registered - evaluator_cls = list_evaluators().get(parsed.name) - if evaluator_cls is not None: + try: - evaluator_cls.config_model(**control_def.evaluator.config) + agent_data = AgentData.model_validate(agent.data) except ValidationError as e: raise APIValidationError( - error_code=ErrorCode.INVALID_CONFIG, - detail=f"Config validation failed for evaluator '{parsed.name}'", - resource="Control", - hint="Check the evaluator's config schema for required fields and types.", + error_code=ErrorCode.CORRUPTED_DATA, + detail=f"Agent '{parsed.namespace}' has invalid data", + resource="Agent", errors=[ ValidationErrorItem( - resource="Control", - field=( - "data.evaluator.config." - f"{'.'.join(str(loc) for loc in err.get('loc', []))}" - ), + resource="Agent", + field=format_field_path(err.get("loc", ())), code=err.get("type", "validation_error"), message=err.get("msg", "Validation failed"), ) for err in e.errors() ], ) - except TypeError: - _logger.warning( - "Config validation raised TypeError for evaluator '%s'", - parsed.name, - exc_info=True, - ) + + evaluator = next( + (e for e in (agent_data.evaluators or []) if e.name == parsed.local_name), + None, + ) + if evaluator is None: + available = [e.name for e in (agent_data.evaluators or [])] raise APIValidationError( - error_code=ErrorCode.INVALID_CONFIG, - detail=f"Invalid config parameters for evaluator '{parsed.name}'", - resource="Control", - hint="Check the evaluator's config schema for valid parameter names.", + error_code=ErrorCode.EVALUATOR_NOT_FOUND, + detail=( + f"Evaluator '{parsed.local_name}' is not registered " + f"with agent '{parsed.namespace}'" + ), + resource="Evaluator", + hint=( + f"Register it via initAgent first. " + f"Available evaluators: {available or 'none'}." + ), errors=[ ValidationErrorItem( resource="Control", - field="data.evaluator.config", - code="invalid_parameters", - message=_INVALID_PARAMETERS_MESSAGE, + field=f"{field_prefix}.evaluator.name", + code="evaluator_not_found", + message=( + f"Evaluator '{parsed.local_name}' not found " + f"on agent '{parsed.namespace}'" + ), + value=evaluator_ref, ) ], ) - # If evaluator not found, allow it - might be a server-side registered evaluator + + if evaluator.config_schema: + try: + validate_config_against_schema( + evaluator_spec.config, + evaluator.config_schema, + ) + except JSONSchemaValidationError: + raise APIValidationError( + error_code=ErrorCode.INVALID_CONFIG, + detail=f"Config validation failed for evaluator '{evaluator_ref}'", + resource="Control", + hint=( + "Check the evaluator's config schema for required fields and types." + ), + errors=[ + ValidationErrorItem( + resource="Control", + field=f"{field_prefix}.evaluator.config", + code="schema_validation_error", + message=_SCHEMA_VALIDATION_FAILED_MESSAGE, + ) + ], + ) + continue + + evaluator_cls = list_evaluators().get(parsed.name) + if evaluator_cls is None: + continue + + try: + evaluator_cls.config_model(**evaluator_spec.config) + except ValidationError as e: + raise APIValidationError( + error_code=ErrorCode.INVALID_CONFIG, + detail=f"Config validation failed for evaluator '{parsed.name}'", + resource="Control", + hint="Check the evaluator's config schema for required fields and types.", + errors=[ + ValidationErrorItem( + resource="Control", + field=( + f"{field_prefix}.evaluator.config." + f"{format_field_path(err.get('loc', ())) or ''}" + ).rstrip("."), + code=err.get("type", "validation_error"), + message=err.get("msg", "Validation failed"), + ) + for err in e.errors() + ], + ) + except TypeError: + _logger.warning( + "Config validation raised TypeError for evaluator '%s'", + parsed.name, + exc_info=True, + ) + raise APIValidationError( + error_code=ErrorCode.INVALID_CONFIG, + detail=f"Invalid config parameters for evaluator '{parsed.name}'", + resource="Control", + hint="Check the evaluator's config schema for valid parameter names.", + errors=[ + ValidationErrorItem( + resource="Control", + field=f"{field_prefix}.evaluator.config", + code="invalid_parameters", + message=_INVALID_PARAMETERS_MESSAGE, + ) + ], + ) @router.put( @@ -364,7 +398,7 @@ async def get_control_data( errors=[ ValidationErrorItem( resource="Control", - field=".".join(str(loc) for loc in err.get("loc", [])), + field=format_field_path(err.get("loc", ())), code=err.get("type", "validation_error"), message=err.get("msg", "Validation failed"), ) @@ -418,17 +452,12 @@ async def set_control_data( # Validate evaluator config using shared logic await _validate_control_definition(request.data, db) - data_json = request.data.model_dump(mode="json", exclude_none=True, exclude_unset=True) - # Pydantic's exclude_none doesn't propagate into nested model dicts after - # serialization, so we re-dump the selector separately to strip null keys. - try: - selector_json = request.data.selector.model_dump(exclude_none=True, exclude_unset=True) # type: ignore[attr-defined] - selector_json = {k: v for k, v in selector_json.items() if v is not None} - if selector_json: - data_json["selector"] = selector_json - except AttributeError: - # Selector doesn't support model_dump, use original serialization - pass + data_json = request.data.model_dump( + mode="json", + by_alias=True, + exclude_none=True, + exclude_unset=True, + ) # Ensure scope does not store null/None for step_names or other optional fields, # so round-trip (save then load) preserves step selection in the UI. if "scope" in data_json and isinstance(data_json["scope"], dict): diff --git a/server/src/agent_control_server/endpoints/evaluation.py b/server/src/agent_control_server/endpoints/evaluation.py index 34c28c5d..78596b6e 100644 --- a/server/src/agent_control_server/endpoints/evaluation.py +++ b/server/src/agent_control_server/endpoints/evaluation.py @@ -239,6 +239,8 @@ async def _emit_observability_events( if response.matches: for match in response.matches: ctrl = control_lookup.get(match.control_id) + primary_leaf = ctrl.control.primary_leaf() if ctrl else None + primary_parts = primary_leaf.leaf_parts() if primary_leaf else None events.append( ControlExecutionEvent( control_execution_id=match.control_execution_id, @@ -253,8 +255,8 @@ async def _emit_observability_events( matched=True, confidence=match.result.confidence, timestamp=now, - evaluator_name=ctrl.control.evaluator.name if ctrl else None, - selector_path=ctrl.control.selector.path if ctrl else None, + evaluator_name=primary_parts[1].name if primary_parts else None, + selector_path=primary_parts[0].path if primary_parts else None, error_message=match.result.error, metadata=match.result.metadata or {}, ) @@ -264,6 +266,8 @@ async def _emit_observability_events( if response.errors: for error in response.errors: ctrl = control_lookup.get(error.control_id) + primary_leaf = ctrl.control.primary_leaf() if ctrl else None + primary_parts = primary_leaf.leaf_parts() if primary_leaf else None events.append( ControlExecutionEvent( control_execution_id=error.control_execution_id, @@ -278,8 +282,8 @@ async def _emit_observability_events( matched=False, confidence=error.result.confidence, timestamp=now, - evaluator_name=ctrl.control.evaluator.name if ctrl else None, - selector_path=ctrl.control.selector.path if ctrl else None, + evaluator_name=primary_parts[1].name if primary_parts else None, + selector_path=primary_parts[0].path if primary_parts else None, error_message=error.result.error, metadata=error.result.metadata or {}, ) @@ -289,6 +293,8 @@ async def _emit_observability_events( if response.non_matches: for non_match in response.non_matches: ctrl = control_lookup.get(non_match.control_id) + primary_leaf = ctrl.control.primary_leaf() if ctrl else None + primary_parts = primary_leaf.leaf_parts() if primary_leaf else None events.append( ControlExecutionEvent( control_execution_id=non_match.control_execution_id, @@ -303,8 +309,8 @@ async def _emit_observability_events( matched=False, confidence=non_match.result.confidence, timestamp=now, - evaluator_name=ctrl.control.evaluator.name if ctrl else None, - selector_path=ctrl.control.selector.path if ctrl else None, + evaluator_name=primary_parts[1].name if primary_parts else None, + selector_path=primary_parts[0].path if primary_parts else None, error_message=None, metadata=non_match.result.metadata or {}, ) diff --git a/server/src/agent_control_server/errors.py b/server/src/agent_control_server/errors.py index 94af5890..1066a7cb 100644 --- a/server/src/agent_control_server/errors.py +++ b/server/src/agent_control_server/errors.py @@ -47,6 +47,8 @@ from fastapi import HTTPException, Request from fastapi.responses import JSONResponse +from .services.validation_paths import format_field_path + _logger = logging.getLogger(__name__) _MAX_PUBLIC_TEXT_LENGTH = 500 @@ -575,8 +577,8 @@ async def validation_exception_handler( # Build field path from location loc = error.get("loc", ()) # Skip 'body' prefix in location - field_parts = [str(p) for p in loc if p != "body"] - field = ".".join(field_parts) if field_parts else None + field_parts = [p for p in loc if p != "body"] + field = format_field_path(field_parts) # Determine resource from first path component resource = "Request" @@ -589,7 +591,7 @@ async def validation_exception_handler( "data": "Control", "policy": "Policy", } - first_part = field_parts[0].lower() + first_part = str(field_parts[0]).lower() resource = prefix_map.get(first_part, resource) errors.append( diff --git a/server/src/agent_control_server/main.py b/server/src/agent_control_server/main.py index 27509762..8f00adc9 100644 --- a/server/src/agent_control_server/main.py +++ b/server/src/agent_control_server/main.py @@ -14,6 +14,7 @@ from fastapi.openapi.utils import get_openapi from starlette_exporter import PrometheusMiddleware, handle_metrics +from . import __version__ as server_version from .auth import require_api_key from .config import observability_settings, settings from .db import AsyncSessionLocal @@ -141,7 +142,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 4. Assign the policy to your agent 5. Query agent's active controls with `/api/v1/agents/{agent_name}/controls` """, - version="0.1.0", + version=server_version, lifespan=lifespan, ) @@ -161,6 +162,14 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: configure_logging(level=log_level) +@app.middleware("http") +async def attach_version_header(request, call_next): # type: ignore[no-untyped-def] + """Attach server version metadata to every response.""" + response = await call_next(request) + response.headers["X-Agent-Control-Server-Version"] = server_version + return response + + # ============================================================================= # Exception Handlers (RFC 7807 / Kubernetes / GitHub-style) # ============================================================================= @@ -264,7 +273,7 @@ async def health_check() -> HealthResponse: Returns: HealthResponse with status and version """ - return HealthResponse(status="healthy", version="0.1.0") + return HealthResponse(status="healthy", version=server_version) configure_ui_routes(app) diff --git a/server/src/agent_control_server/scripts/__init__.py b/server/src/agent_control_server/scripts/__init__.py new file mode 100644 index 00000000..e8e00625 --- /dev/null +++ b/server/src/agent_control_server/scripts/__init__.py @@ -0,0 +1 @@ +"""Operational scripts for the Agent Control server package.""" diff --git a/server/src/agent_control_server/scripts/migrate_control_conditions.py b/server/src/agent_control_server/scripts/migrate_control_conditions.py new file mode 100644 index 00000000..a25fbae4 --- /dev/null +++ b/server/src/agent_control_server/scripts/migrate_control_conditions.py @@ -0,0 +1,114 @@ +"""Rewrite stored control payloads into canonical condition-tree form.""" + +from __future__ import annotations + +import argparse + +from sqlalchemy import create_engine, select +from sqlalchemy.orm import Session + +from agent_control_server.config import db_config +from agent_control_server.models import Control +from agent_control_server.services.control_migration import ( + ControlMigrationResult, + migrate_control_payload, +) + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Migrate stored controls from legacy selector/evaluator fields " + "to canonical condition trees." + ), + ) + mode_group = parser.add_mutually_exclusive_group() + mode_group.add_argument( + "--dry-run", + action="store_true", + help="Analyze stored controls without writing changes (default).", + ) + mode_group.add_argument( + "--apply", + action="store_true", + help="Apply the migration after a clean analysis run.", + ) + return parser.parse_args() + + +def _print_summary( + *, + total: int, + unchanged: int, + migrated: list[tuple[Control, ControlMigrationResult]], + invalid: list[tuple[Control, ControlMigrationResult]], + apply: bool, +) -> None: + mode = "apply" if apply else "dry-run" + print(f"Control condition migration summary ({mode})") + print(f"Total controls: {total}") + print(f"Already canonical: {unchanged}") + print(f"Ready to migrate: {len(migrated)}") + print(f"Invalid/corrupted: {len(invalid)}") + + if invalid: + print("") + print("Invalid controls:") + for control, result in invalid: + reason = result.reason or "Unknown validation error." + print(f"- id={control.id} name={control.name}: {reason}") + + +def main() -> int: + args = _parse_args() + apply = bool(args.apply) + + engine = create_engine(db_config.get_url(), future=True) + + try: + with Session(engine) as session: + controls = list(session.execute(select(Control).order_by(Control.id)).scalars().all()) + migrated: list[tuple[Control, ControlMigrationResult]] = [] + invalid: list[tuple[Control, ControlMigrationResult]] = [] + unchanged = 0 + + for control in controls: + result = migrate_control_payload(control.data) + if result.status == "unchanged": + unchanged += 1 + elif result.status == "migrated": + migrated.append((control, result)) + else: + invalid.append((control, result)) + + _print_summary( + total=len(controls), + unchanged=unchanged, + migrated=migrated, + invalid=invalid, + apply=apply, + ) + + if invalid: + if apply: + print("") + print("Aborting apply because invalid controls must be fixed first.") + return 1 + + if not apply: + return 0 + + for control, result in migrated: + assert result.payload is not None + control.data = result.payload + + session.commit() + print("") + print(f"Applied migration to {len(migrated)} controls.") + return 0 + finally: + engine.dispose() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/server/src/agent_control_server/services/control_migration.py b/server/src/agent_control_server/services/control_migration.py new file mode 100644 index 00000000..bf608395 --- /dev/null +++ b/server/src/agent_control_server/services/control_migration.py @@ -0,0 +1,93 @@ +"""Helpers for migrating stored controls to condition trees.""" + +from __future__ import annotations + +from copy import deepcopy +from dataclasses import dataclass +from typing import Any, Literal + +from agent_control_models import ControlDefinition +from pydantic import ValidationError + +type MigrationStatus = Literal["unchanged", "migrated", "invalid"] + + +@dataclass(frozen=True) +class ControlMigrationResult: + """Outcome of migrating a single stored control payload.""" + + status: MigrationStatus + payload: dict[str, Any] | None = None + reason: str | None = None + + +def _validation_message(error: ValidationError) -> str: + first_error = error.errors()[0] + location = ".".join(str(part) for part in first_error.get("loc", ())) + message = first_error.get("msg", "Validation failed.") + if location: + return f"{location}: {message}" + return message + + +def migrate_control_payload(data: object) -> ControlMigrationResult: + """Migrate a stored control payload to canonical condition-tree shape.""" + if not isinstance(data, dict): + return ControlMigrationResult( + status="invalid", + reason="Stored control data must be a JSON object.", + ) + + has_condition = "condition" in data + has_selector = "selector" in data + has_evaluator = "evaluator" in data + + if has_condition and (has_selector or has_evaluator): + return ControlMigrationResult( + status="invalid", + reason=( + "Stored control data mixes canonical condition fields " + "with legacy selector/evaluator fields." + ), + ) + + candidate = deepcopy(data) + status: MigrationStatus = "unchanged" + + if not has_condition: + if has_selector != has_evaluator: + return ControlMigrationResult( + status="invalid", + reason="Legacy control data must include both selector and evaluator.", + ) + if not has_selector: + return ControlMigrationResult( + status="invalid", + reason="Stored control data is missing the condition definition.", + ) + + selector = candidate.pop("selector") + evaluator = candidate.pop("evaluator") + candidate["condition"] = { + "selector": selector, + "evaluator": evaluator, + } + status = "migrated" + + try: + validated = ControlDefinition.model_validate(candidate) + except ValidationError as error: + return ControlMigrationResult( + status="invalid", + reason=_validation_message(error), + ) + + return ControlMigrationResult( + status=status, + payload=validated.model_dump( + mode="json", + by_alias=True, + exclude_none=True, + exclude_unset=True, + ), + ) diff --git a/server/src/agent_control_server/services/controls.py b/server/src/agent_control_server/services/controls.py index ea334006..11ba5529 100644 --- a/server/src/agent_control_server/services/controls.py +++ b/server/src/agent_control_server/services/controls.py @@ -12,6 +12,7 @@ from ..errors import APIValidationError from ..models import Control, agent_controls, agent_policies, policy_controls +from .validation_paths import format_field_path _logger = logging.getLogger(__name__) @@ -79,7 +80,7 @@ async def list_controls_for_agent( error_items = [] for err in e.errors(): loc: Sequence[str | int] = err.get("loc", []) - field_suffix = ".".join(str(part) for part in loc) if loc else "" + field_suffix = format_field_path(loc) or "" error_items.append( ValidationErrorItem( resource="Control", diff --git a/server/src/agent_control_server/services/validation_paths.py b/server/src/agent_control_server/services/validation_paths.py new file mode 100644 index 00000000..9df1052e --- /dev/null +++ b/server/src/agent_control_server/services/validation_paths.py @@ -0,0 +1,18 @@ +"""Helpers for formatting nested validation field paths.""" + +from collections.abc import Sequence + + +def format_field_path(parts: Sequence[str | int]) -> str | None: + """Format nested field parts using dot/bracket notation.""" + field = "" + for part in parts: + if isinstance(part, int): + field += f"[{part}]" + continue + + if field: + field += "." + field += part + + return field or None diff --git a/server/tests/test_agents_additional.py b/server/tests/test_agents_additional.py index 8c8085f8..8c69f79f 100644 --- a/server/tests/test_agents_additional.py +++ b/server/tests/test_agents_additional.py @@ -7,7 +7,7 @@ from fastapi.testclient import TestClient from sqlalchemy import text -from .utils import VALID_CONTROL_PAYLOAD +from .utils import VALID_CONTROL_PAYLOAD, canonicalize_control_payload from .conftest import engine @@ -39,7 +39,10 @@ def _create_control_with_data(client: TestClient, data: dict) -> int: resp = client.put("/api/v1/controls", json={"name": f"control-{uuid.uuid4()}"}) assert resp.status_code == 200 control_id = resp.json()["control_id"] - set_resp = client.put(f"/api/v1/controls/{control_id}/data", json={"data": data}) + set_resp = client.put( + f"/api/v1/controls/{control_id}/data", + json={"data": canonicalize_control_payload(data)}, + ) assert set_resp.status_code == 200, set_resp.text return control_id @@ -220,7 +223,7 @@ def test_patch_agent_remove_evaluator_in_use_conflict(client: TestClient) -> Non agent_name, agent_name = _init_agent(client, evaluators=evaluators) control_payload = deepcopy(VALID_CONTROL_PAYLOAD) - control_payload["evaluator"] = { + control_payload["condition"]["evaluator"] = { "name": f"{agent_name}:custom", "config": {"pattern": "x"}, } @@ -255,7 +258,7 @@ def test_set_agent_policy_incompatible_controls(client: TestClient) -> None: agent_a_id, agent_a_name = _init_agent(client, evaluators=evaluators) control_payload = deepcopy(VALID_CONTROL_PAYLOAD) - control_payload["evaluator"] = { + control_payload["condition"]["evaluator"] = { "name": f"{agent_a_name}:custom", "config": {}, } @@ -347,7 +350,7 @@ def test_list_agent_controls_corrupted_control_data_returns_422( # Given: an agent with a policy that includes a control agent_name, _ = _init_agent(client) control_payload = deepcopy(VALID_CONTROL_PAYLOAD) - control_payload["evaluator"] = {"name": "regex", "config": {"pattern": "x"}} + control_payload["condition"]["evaluator"] = {"name": "regex", "config": {"pattern": "x"}} control_id = _create_control_with_data(client, control_payload) policy_id = _create_policy(client) assoc = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") @@ -442,12 +445,15 @@ def test_set_agent_policy_rejects_missing_agent_evaluator(client: TestClient) -> assert assoc.status_code == 200 with engine.begin() as conn: + corrupted_payload = deepcopy(VALID_CONTROL_PAYLOAD) + corrupted_payload["condition"]["evaluator"] = { + "name": f"{agent_name}:missing", + "config": {}, + } conn.execute( text("UPDATE controls SET data = CAST(:data AS JSONB) WHERE id = :id"), { - "data": json.dumps( - {"evaluator": {"name": f"{agent_name}:missing", "config": {}}} - ), + "data": json.dumps(corrupted_payload), "id": control_id, }, ) @@ -484,12 +490,15 @@ def test_set_agent_policy_rejects_invalid_agent_evaluator_config(client: TestCli assert assoc.status_code == 200 with engine.begin() as conn: + corrupted_payload = deepcopy(VALID_CONTROL_PAYLOAD) + corrupted_payload["condition"]["evaluator"] = { + "name": f"{agent_name}:custom", + "config": {}, + } conn.execute( text("UPDATE controls SET data = CAST(:data AS JSONB) WHERE id = :id"), { - "data": json.dumps( - {"evaluator": {"name": f"{agent_name}:custom", "config": {}}} - ), + "data": json.dumps(corrupted_payload), "id": control_id, }, ) @@ -666,8 +675,8 @@ def test_set_agent_policy_skips_controls_without_data(client: TestClient) -> Non assert resp.json()["success"] is True -def test_set_agent_policy_skips_controls_without_evaluator_name(client: TestClient) -> None: - # Given: an agent and a policy with a control missing evaluator name +def test_set_agent_policy_rejects_controls_without_evaluator_name(client: TestClient) -> None: + # Given: an agent and a policy with a stored control whose leaf is missing evaluator name agent_name, _ = _init_agent(client) policy_id = _create_policy(client) control_id = _create_control_with_data(client, VALID_CONTROL_PAYLOAD) @@ -675,17 +684,21 @@ def test_set_agent_policy_skips_controls_without_evaluator_name(client: TestClie assert assoc.status_code == 200 with engine.begin() as conn: + corrupted_payload = deepcopy(VALID_CONTROL_PAYLOAD) + corrupted_payload["condition"]["evaluator"] = {"config": {}} conn.execute( text("UPDATE controls SET data = CAST(:data AS JSONB) WHERE id = :id"), - {"data": json.dumps({"evaluator": {}}), "id": control_id}, + {"data": json.dumps(corrupted_payload), "id": control_id}, ) # When: assigning the policy to the agent resp = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") - # Then: assignment succeeds because evaluator name is missing - assert resp.status_code == 200 - assert resp.json()["success"] is True + # Then: assignment is rejected because the stored control data is corrupted + assert resp.status_code == 400 + body = resp.json() + assert body["error_code"] == "POLICY_CONTROL_INCOMPATIBLE" + assert any("corrupted data" in err.get("message", "").lower() for err in body.get("errors", [])) def test_list_agents_includes_active_controls_count(client: TestClient) -> None: diff --git a/server/tests/test_auth.py b/server/tests/test_auth.py index 57c2bba1..20a1c5be 100644 --- a/server/tests/test_auth.py +++ b/server/tests/test_auth.py @@ -145,10 +145,12 @@ def test_evaluators_accessible_when_disabled( "enabled": True, "execution": "server", "scope": {"step_types": ["llm"], "stages": ["pre"]}, - "selector": {"path": "input"}, - "evaluator": { - "name": "regex", - "config": {"pattern": "test", "flags": []}, + "condition": { + "selector": {"path": "input"}, + "evaluator": { + "name": "regex", + "config": {"pattern": "test", "flags": []}, + }, }, "action": {"decision": "deny"}, "tags": ["test"], diff --git a/server/tests/test_control_migration.py b/server/tests/test_control_migration.py new file mode 100644 index 00000000..cb73b2a9 --- /dev/null +++ b/server/tests/test_control_migration.py @@ -0,0 +1,60 @@ +"""Tests for stored control condition migration.""" + +from __future__ import annotations + +from copy import deepcopy + +from agent_control_server.services.control_migration import migrate_control_payload + +from .utils import VALID_CONTROL_PAYLOAD + + +def test_migrate_control_payload_rewrites_legacy_leaf() -> None: + legacy_payload = deepcopy(VALID_CONTROL_PAYLOAD) + legacy_payload["selector"] = legacy_payload["condition"]["selector"] + legacy_payload["evaluator"] = legacy_payload["condition"]["evaluator"] + legacy_payload.pop("condition") + + result = migrate_control_payload(legacy_payload) + + assert result.status == "migrated" + assert result.payload is not None + assert "selector" not in result.payload + assert "evaluator" not in result.payload + assert result.payload["condition"]["selector"]["path"] == "input" + + +def test_migrate_control_payload_leaves_canonical_rows_unchanged() -> None: + result = migrate_control_payload(deepcopy(VALID_CONTROL_PAYLOAD)) + + assert result.status == "unchanged" + assert result.payload == VALID_CONTROL_PAYLOAD + + +def test_migrate_control_payload_rejects_mixed_rows() -> None: + mixed_payload = deepcopy(VALID_CONTROL_PAYLOAD) + mixed_payload["selector"] = {"path": "input"} + + result = migrate_control_payload(mixed_payload) + + assert result.status == "invalid" + assert result.reason is not None + assert "mixes canonical condition fields" in result.reason + + +def test_migrate_control_payload_rejects_partial_legacy_rows() -> None: + partial_payload = deepcopy(VALID_CONTROL_PAYLOAD) + partial_payload.pop("condition") + partial_payload["selector"] = {"path": "input"} + + result = migrate_control_payload(partial_payload) + + assert result.status == "invalid" + assert result.reason == "Legacy control data must include both selector and evaluator." + + +def test_migrate_control_payload_rejects_non_object_rows() -> None: + result = migrate_control_payload(["not", "an", "object"]) + + assert result.status == "invalid" + assert result.reason == "Stored control data must be a JSON object." diff --git a/server/tests/test_controls.py b/server/tests/test_controls.py index c901b884..119c1085 100644 --- a/server/tests/test_controls.py +++ b/server/tests/test_controls.py @@ -38,10 +38,12 @@ def test_get_control_data_initially_unconfigured(client: TestClient) -> None: "enabled": True, "execution": "server", "scope": {"step_types": ["llm"], "stages": ["pre"]}, - "selector": {"path": "input"}, - "evaluator": { - "name": "regex", - "config": {"pattern": "test", "flags": []} + "condition": { + "selector": {"path": "input"}, + "evaluator": { + "name": "regex", + "config": {"pattern": "test", "flags": []} + }, }, "action": {"decision": "deny"}, "tags": ["test"] @@ -67,9 +69,9 @@ def test_set_control_data_replaces_existing(client: TestClient) -> None: assert data["enabled"] == payload["enabled"] assert data["execution"] == payload["execution"] assert data["scope"] == payload["scope"] - assert data["evaluator"] == payload["evaluator"] + assert data["condition"]["evaluator"] == payload["condition"]["evaluator"] assert data["action"] == payload["action"] - assert data["selector"]["path"] == payload["selector"]["path"] + assert data["condition"]["selector"]["path"] == payload["condition"]["selector"]["path"] def test_set_control_data_with_empty_dict_fails(client: TestClient) -> None: diff --git a/server/tests/test_controls_additional.py b/server/tests/test_controls_additional.py index 21a58f5f..cef4c84e 100644 --- a/server/tests/test_controls_additional.py +++ b/server/tests/test_controls_additional.py @@ -14,6 +14,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session +from agent_control_models import ConditionNode from agent_control_server.db import get_async_db from agent_control_server.models import Control @@ -584,7 +585,7 @@ def test_set_control_data_agent_scoped_agent_not_found(client: TestClient) -> No # When: setting data with a missing agent in evaluator ref payload = deepcopy(VALID_CONTROL_PAYLOAD) - payload["evaluator"] = {"name": "missing-agent:custom", "config": {"pattern": "x"}} + payload["condition"]["evaluator"] = {"name": "missing-agent:custom", "config": {"pattern": "x"}} resp = client.put(f"/api/v1/controls/{control_id}/data", json={"data": payload}) # Then: not found @@ -608,7 +609,7 @@ def test_set_control_data_agent_scoped_evaluator_missing(client: TestClient) -> control_id, _ = _create_control(client) payload = deepcopy(VALID_CONTROL_PAYLOAD) - payload["evaluator"] = {"name": f"{agent_name}:missing", "config": {"pattern": "x"}} + payload["condition"]["evaluator"] = {"name": f"{agent_name}:missing", "config": {"pattern": "x"}} # When: setting data with evaluator not registered on agent resp = client.put(f"/api/v1/controls/{control_id}/data", json={"data": payload}) @@ -617,7 +618,7 @@ def test_set_control_data_agent_scoped_evaluator_missing(client: TestClient) -> assert resp.status_code == 422 body = resp.json() assert body["error_code"] == "EVALUATOR_NOT_FOUND" - assert any(err.get("field") == "data.evaluator.name" for err in body.get("errors", [])) + assert any(err.get("field") == "data.condition.evaluator.name" for err in body.get("errors", [])) def test_set_control_data_agent_scoped_invalid_schema(client: TestClient) -> None: @@ -646,7 +647,7 @@ def test_set_control_data_agent_scoped_invalid_schema(client: TestClient) -> Non control_id, _ = _create_control(client) payload = deepcopy(VALID_CONTROL_PAYLOAD) - payload["evaluator"] = {"name": f"{agent_name}:custom", "config": {}} + payload["condition"]["evaluator"] = {"name": f"{agent_name}:custom", "config": {}} # When: setting data with config missing required fields resp = client.put(f"/api/v1/controls/{control_id}/data", json={"data": payload}) @@ -655,7 +656,7 @@ def test_set_control_data_agent_scoped_invalid_schema(client: TestClient) -> Non assert resp.status_code == 422 body = resp.json() assert body["error_code"] == "INVALID_CONFIG" - assert any(err.get("field") == "data.evaluator.config" for err in body.get("errors", [])) + assert any(err.get("field") == "data.condition.evaluator.config" for err in body.get("errors", [])) def test_patch_control_updates_name_and_enabled(client: TestClient) -> None: @@ -732,7 +733,7 @@ def test_set_control_data_agent_scoped_corrupted_agent_data_returns_422( control_id, _ = _create_control(client) payload = deepcopy(VALID_CONTROL_PAYLOAD) - payload["evaluator"] = {"name": f"{agent_name}:custom", "config": {}} + payload["condition"]["evaluator"] = {"name": f"{agent_name}:custom", "config": {}} # When: setting control data referencing the corrupted agent's evaluator resp = client.put(f"/api/v1/controls/{control_id}/data", json={"data": payload}) @@ -746,7 +747,7 @@ def test_set_control_data_unknown_evaluator_allowed(client: TestClient) -> None: # Given: a control with a non-registered evaluator name control_id, _ = _create_control(client) payload = deepcopy(VALID_CONTROL_PAYLOAD) - payload["evaluator"] = {"name": "unknown-eval", "config": {}} + payload["condition"]["evaluator"] = {"name": "unknown-eval", "config": {}} # When: setting the control data resp = client.put(f"/api/v1/controls/{control_id}/data", json={"data": payload}) @@ -772,7 +773,7 @@ class DummyEvaluator: ) payload = deepcopy(VALID_CONTROL_PAYLOAD) - payload["evaluator"] = {"name": "dummy", "config": {}} + payload["condition"]["evaluator"] = {"name": "dummy", "config": {}} # When: setting control data with invalid config resp = client.put(f"/api/v1/controls/{control_id}/data", json={"data": payload}) @@ -781,7 +782,10 @@ class DummyEvaluator: assert resp.status_code == 422 body = resp.json() assert body["error_code"] == "INVALID_CONFIG" - assert any("data.evaluator.config" in err.get("field", "") for err in body.get("errors", [])) + assert any( + "data.condition.evaluator.config" in err.get("field", "") + for err in body.get("errors", []) + ) def test_set_control_data_builtin_evaluator_invalid_parameters( @@ -802,7 +806,7 @@ def config_model(**_kwargs): # type: ignore[no-untyped-def] ) payload = deepcopy(VALID_CONTROL_PAYLOAD) - payload["evaluator"] = {"name": "dummy", "config": {"unexpected": "value"}} + payload["condition"]["evaluator"] = {"name": "dummy", "config": {"unexpected": "value"}} # When: setting control data with invalid parameters resp = client.put(f"/api/v1/controls/{control_id}/data", json={"data": payload}) @@ -833,11 +837,7 @@ async def test_set_control_data_selector_without_model_dump_uses_original_serial class DummyData: def __init__(self, data: dict[str, object]) -> None: self._data = data - self.selector = data["selector"] - self.evaluator = SimpleNamespace( - name=data["evaluator"]["name"], - config=data["evaluator"]["config"], - ) + self.condition = ConditionNode.model_validate(data["condition"]) def model_dump(self, *args: object, **kwargs: object) -> dict[str, object]: return self._data @@ -850,7 +850,7 @@ def model_dump(self, *args: object, **kwargs: object) -> dict[str, object]: # Then: the update succeeds and uses the original selector serialization assert response.success is True await async_db.refresh(control) - assert control.data["selector"] == payload["selector"] + assert control.data["condition"] == payload["condition"] def test_patch_control_rename_preserves_enabled(client: TestClient) -> None: diff --git a/server/tests/test_controls_validation.py b/server/tests/test_controls_validation.py index cd5084ae..6f5acb6d 100644 --- a/server/tests/test_controls_validation.py +++ b/server/tests/test_controls_validation.py @@ -1,4 +1,5 @@ """Tests for control validation and schema enforcement.""" +from copy import deepcopy import uuid from fastapi.testclient import TestClient from .utils import VALID_CONTROL_PAYLOAD @@ -13,8 +14,8 @@ def test_validation_invalid_logic_enum(client: TestClient): """Test that invalid enum values in config are rejected.""" # Given: a control and a payload with invalid 'logic' value control_id = create_control(client) - payload = VALID_CONTROL_PAYLOAD.copy() - payload["evaluator"] = { + payload = deepcopy(VALID_CONTROL_PAYLOAD) + payload["condition"]["evaluator"] = { "name": "list", "config": { "values": ["a", "b"], @@ -40,8 +41,8 @@ def test_validation_discriminator_mismatch(client: TestClient): """Test that config must match the evaluator type.""" # Given: a control and type='list' but config has 'pattern' (RegexEvaluatorConfig) control_id = create_control(client) - payload = VALID_CONTROL_PAYLOAD.copy() - payload["evaluator"] = { + payload = deepcopy(VALID_CONTROL_PAYLOAD) + payload["condition"]["evaluator"] = { "name": "list", "config": { "pattern": "some_regex", # Invalid for ListEvaluatorConfig @@ -67,8 +68,8 @@ def test_validation_regex_flags_list(client: TestClient): """Test validation of regex flags list.""" # Given: a control and regex config with invalid flags type (string instead of list) control_id = create_control(client) - payload = VALID_CONTROL_PAYLOAD.copy() - payload["evaluator"] = { + payload = deepcopy(VALID_CONTROL_PAYLOAD) + payload["condition"]["evaluator"] = { "name": "regex", "config": { "pattern": "abc", @@ -90,8 +91,8 @@ def test_validation_invalid_regex_pattern(client: TestClient): """Test validation of regex pattern syntax.""" # Given: a control and regex config with invalid pattern (unclosed bracket) control_id = create_control(client) - payload = VALID_CONTROL_PAYLOAD.copy() - payload["evaluator"] = { + payload = deepcopy(VALID_CONTROL_PAYLOAD) + payload["condition"]["evaluator"] = { "name": "regex", "config": { "pattern": "[", # Invalid regex @@ -116,8 +117,8 @@ def test_validation_empty_string_path_rejected(client: TestClient): """Test that empty string path is rejected.""" # Given: a control and payload with empty string path control_id = create_control(client) - payload = VALID_CONTROL_PAYLOAD.copy() - payload["selector"] = {"path": ""} + payload = deepcopy(VALID_CONTROL_PAYLOAD) + payload["condition"]["selector"] = {"path": ""} # When: setting control data resp = client.put(f"/api/v1/controls/{control_id}/data", json={"data": payload}) @@ -136,8 +137,8 @@ def test_validation_none_path_defaults_to_star(client: TestClient): """Test that None/missing path defaults to '*'.""" # Given: a control and payload without path in selector (None) control_id = create_control(client) - payload = VALID_CONTROL_PAYLOAD.copy() - payload["selector"] = {} # No path specified + payload = deepcopy(VALID_CONTROL_PAYLOAD) + payload["condition"]["selector"] = {} # No path specified # When: setting control data resp = client.put(f"/api/v1/controls/{control_id}/data", json={"data": payload}) @@ -151,7 +152,7 @@ def test_validation_none_path_defaults_to_star(client: TestClient): # Then: path should default to '*' data = get_resp.json()["data"] - assert data["selector"]["path"] == "*" + assert data["condition"]["selector"]["path"] == "*" def test_get_control_data_returns_typed_response(client: TestClient): @@ -171,9 +172,8 @@ def test_get_control_data_returns_typed_response(client: TestClient): data = resp_get.json()["data"] # Should have required ControlDefinition fields - assert "evaluator" in data + assert "condition" in data assert "action" in data - assert "selector" in data assert "execution" in data assert "scope" in data @@ -182,7 +182,7 @@ def test_validation_empty_step_names_rejected(client: TestClient): """Test that empty step_names list is rejected.""" # Given: a control and payload with empty step_names list control_id = create_control(client) - payload = VALID_CONTROL_PAYLOAD.copy() + payload = deepcopy(VALID_CONTROL_PAYLOAD) payload["scope"] = {"step_names": []} # When: setting control data diff --git a/server/tests/test_error_handling.py b/server/tests/test_error_handling.py index 5f139431..f9689851 100644 --- a/server/tests/test_error_handling.py +++ b/server/tests/test_error_handling.py @@ -474,8 +474,10 @@ async def mock_db_returns_control() -> AsyncGenerator[AsyncSession, None]: "enabled": True, "execution": "server", "scope": {"step_types": ["llm"], "stages": ["pre"]}, - "selector": {"path": "input"}, - "evaluator": {"name": "regex", "config": {"pattern": "x"}}, + "condition": { + "selector": {"path": "input"}, + "evaluator": {"name": "regex", "config": {"pattern": "x"}}, + }, "action": {"decision": "deny"} } resp = client.put( diff --git a/server/tests/test_evaluation_e2e.py b/server/tests/test_evaluation_e2e.py index 8d3f6ad1..7ebb03b9 100644 --- a/server/tests/test_evaluation_e2e.py +++ b/server/tests/test_evaluation_e2e.py @@ -1,8 +1,10 @@ """End-to-end tests for evaluation flow.""" import uuid + from fastapi.testclient import TestClient from agent_control_models import EvaluationRequest, Step -from .utils import create_and_assign_policy + +from .utils import canonicalize_control_payload, create_and_assign_policy def test_evaluation_flow_deny(client: TestClient): @@ -236,7 +238,10 @@ def test_evaluation_deny_precedence(client: TestClient): } resp = client.put("/api/v1/controls", json={"name": f"deny-control-{uuid.uuid4()}"}) deny_control_id = resp.json()["control_id"] - client.put(f"/api/v1/controls/{deny_control_id}/data", json={"data": control_deny}) + client.put( + f"/api/v1/controls/{deny_control_id}/data", + json={"data": canonicalize_control_payload(control_deny)}, + ) # Add Control to Agent's Policy client.post(f"/api/v1/policies/{policy_id}/controls/{deny_control_id}") diff --git a/server/tests/test_init_agent_conflict_mode.py b/server/tests/test_init_agent_conflict_mode.py index e39f1c02..98af20e0 100644 --- a/server/tests/test_init_agent_conflict_mode.py +++ b/server/tests/test_init_agent_conflict_mode.py @@ -47,7 +47,7 @@ def _create_policy_with_agent_evaluator_control( control_id = create_control_resp.json()["control_id"] control_data = deepcopy(VALID_CONTROL_PAYLOAD) - control_data["evaluator"] = { + control_data["condition"]["evaluator"] = { "name": f"{agent_name}:{evaluator_name}", "config": {}, } diff --git a/server/tests/test_new_features.py b/server/tests/test_new_features.py index ad5a91c8..f79682a2 100644 --- a/server/tests/test_new_features.py +++ b/server/tests/test_new_features.py @@ -4,6 +4,8 @@ from fastapi.testclient import TestClient +from .utils import canonicalize_control_payload + def make_agent_payload( agent_name: str | None = None, @@ -269,7 +271,7 @@ def _create_policy_with_control( # Set control data data_resp = client.put( f"/api/v1/controls/{control_id}/data", - json={"data": control_data}, + json={"data": canonicalize_control_payload(control_data)}, ) assert data_resp.status_code == 200 @@ -357,8 +359,10 @@ def test_control_creation_with_unregistered_evaluator_fails(client: TestClient) "data": { "execution": "server", "scope": {"step_types": ["llm"], "stages": ["pre"]}, - "selector": {"path": "input"}, - "evaluator": {"name": f"{agent_name}:nonexistent-eval", "config": {}}, + "condition": { + "selector": {"path": "input"}, + "evaluator": {"name": f"{agent_name}:nonexistent-eval", "config": {}}, + }, "action": {"decision": "deny"}, } }, diff --git a/server/tests/utils.py b/server/tests/utils.py index a2a32098..3cb552d2 100644 --- a/server/tests/utils.py +++ b/server/tests/utils.py @@ -1,6 +1,8 @@ """Test utilities for server tests.""" +from copy import deepcopy import uuid from typing import Any + from fastapi.testclient import TestClient @@ -9,12 +11,34 @@ "enabled": True, "execution": "server", "scope": {"step_types": ["llm"], "stages": ["pre"]}, - "selector": {"path": "input"}, - "evaluator": {"name": "regex", "config": {"pattern": "x"}}, + "condition": { + "selector": {"path": "input"}, + "evaluator": {"name": "regex", "config": {"pattern": "x"}}, + }, "action": {"decision": "deny"} } +def canonicalize_control_payload(payload: dict[str, Any]) -> dict[str, Any]: + """Convert legacy flat test payloads into canonical condition trees.""" + canonical = deepcopy(payload) + if "condition" in canonical: + return canonical + + selector = canonical.pop("selector", None) + evaluator = canonical.pop("evaluator", None) + if selector is None and evaluator is None: + return canonical + if selector is None or evaluator is None: + raise ValueError("Legacy control payloads must include both selector and evaluator.") + + canonical["condition"] = { + "selector": selector, + "evaluator": evaluator, + } + return canonical + + def create_and_assign_policy( client: TestClient, control_config: dict[str, Any] | None = None, @@ -31,7 +55,9 @@ def create_and_assign_policy( tuple: (agent_name, control_name) """ if control_config is None: - control_config = VALID_CONTROL_PAYLOAD.copy() + control_config = deepcopy(VALID_CONTROL_PAYLOAD) + else: + control_config = canonicalize_control_payload(control_config) # 1. Create Control control_name = f"control-{uuid.uuid4()}" diff --git a/ui/src/core/api/generated/api-types.ts b/ui/src/core/api/generated/api-types.ts index d54bbae1..6d385ea2 100644 --- a/ui/src/core/api/generated/api-types.ts +++ b/ui/src/core/api/generated/api-types.ts @@ -4,4453 +4,4745 @@ */ export interface paths { - '/api/v1/agents': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/api/config": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * UI configuration + * @description Return configuration flags that drive UI behavior. + * + * If authentication is enabled, this also reports whether the current + * request has an active session (via header or cookie), allowing the UI + * to skip the login prompt on refresh when a valid cookie is present. + */ + get: operations["get_config_api_config_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Login with API key + * @description Validate an API key and issue a signed JWT session cookie. + * + * The raw API key is transmitted only in this single request and is never + * stored in the cookie. Subsequent requests authenticate via the JWT. + */ + post: operations["login_api_login_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Logout (clear session cookie) + * @description Clear the session cookie. + */ + post: operations["logout_api_logout_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List all agents + * @description List all registered agents with cursor-based pagination. + * + * Returns a summary of each agent including identifier, policy associations, + * and counts of registered steps and evaluators. + * + * Args: + * cursor: Optional cursor for pagination (last agent name from previous page) + * limit: Pagination limit (default 20, max 100) + * name: Optional name filter (case-insensitive partial match) + * db: Database session (injected) + * + * Returns: + * ListAgentsResponse with agent summaries and pagination info + */ + get: operations["list_agents_api_v1_agents_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/initAgent": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Initialize or update an agent + * @description Register a new agent or update an existing agent's steps and metadata. + * + * This endpoint is idempotent: + * - If the agent name doesn't exist, creates a new agent + * - If the agent name exists, updates registration data in place + * + * conflict_mode controls registration conflict handling: + * - strict (default): preserve compatibility checks and conflict errors + * - overwrite: latest init payload replaces steps/evaluators and returns change summary + * + * Args: + * request: Agent metadata and step schemas + * db: Database session (injected) + * + * Returns: + * InitAgentResponse with created flag and active controls (policy-derived + direct) + */ + post: operations["init_agent_api_v1_agents_initAgent_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_name}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get agent details + * @description Retrieve agent metadata and all registered steps. + * + * Returns the latest version of each step (deduplicated by type+name). + * + * Args: + * agent_name: Agent identifier + * db: Database session (injected) + * + * Returns: + * GetAgentResponse with agent metadata and step list + * + * Raises: + * HTTPException 404: Agent not found + * HTTPException 422: Agent data is corrupted + */ + get: operations["get_agent_api_v1_agents__agent_name__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Modify agent (remove steps/evaluators) + * @description Remove steps and/or evaluators from an agent. + * + * This is the complement to initAgent which only adds items. + * Removals are idempotent - attempting to remove non-existent items is not an error. + * + * Args: + * agent_name: Agent identifier + * request: Lists of step/evaluator identifiers to remove + * db: Database session (injected) + * + * Returns: + * PatchAgentResponse with lists of actually removed items + * + * Raises: + * HTTPException 404: Agent not found + * HTTPException 500: Database error during update + */ + patch: operations["patch_agent_api_v1_agents__agent_name__patch"]; + trace?: never; + }; + "/api/v1/agents/{agent_name}/controls": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List agent's active controls + * @description List all protection controls active for an agent. + * + * Controls include the union of policy-derived and directly associated controls. + * + * Args: + * agent_name: Agent identifier + * db: Database session (injected) + * + * Returns: + * AgentControlsResponse with list of active controls + * + * Raises: + * HTTPException 404: Agent not found + */ + get: operations["list_agent_controls_api_v1_agents__agent_name__controls_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_name}/controls/{control_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Associate control directly with agent + * @description Associate a control directly with an agent (idempotent). + */ + post: operations["add_agent_control_api_v1_agents__agent_name__controls__control_id__post"]; + /** + * Remove direct control association from agent + * @description Remove a direct control association from an agent (idempotent). + */ + delete: operations["remove_agent_control_api_v1_agents__agent_name__controls__control_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_name}/evaluators": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List agent's registered evaluator schemas + * @description List all evaluator schemas registered with an agent. + * + * Evaluator schemas are registered via initAgent and used for: + * - Config validation when creating Controls + * - UI to display available config options + * + * Args: + * agent_name: Agent identifier + * cursor: Optional cursor for pagination (name of last evaluator from previous page) + * limit: Pagination limit (default 20, max 100) + * db: Database session (injected) + * + * Returns: + * ListEvaluatorsResponse with evaluator schemas and pagination + * + * Raises: + * HTTPException 404: Agent not found + */ + get: operations["list_agent_evaluators_api_v1_agents__agent_name__evaluators_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_name}/evaluators/{evaluator_name}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get specific evaluator schema + * @description Get a specific evaluator schema registered with an agent. + * + * Args: + * agent_name: Agent identifier + * evaluator_name: Name of the evaluator + * db: Database session (injected) + * + * Returns: + * EvaluatorSchemaItem with schema details + * + * Raises: + * HTTPException 404: Agent or evaluator not found + */ + get: operations["get_agent_evaluator_api_v1_agents__agent_name__evaluators__evaluator_name__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_name}/policies": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List policies associated with agent + * @description List policy IDs associated with an agent. + */ + get: operations["get_agent_policies_api_v1_agents__agent_name__policies_get"]; + put?: never; + post?: never; + /** + * Remove all policy associations from agent + * @description Remove all policy associations from an agent. + */ + delete: operations["remove_all_agent_policies_api_v1_agents__agent_name__policies_delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_name}/policies/{policy_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Associate policy with agent + * @description Associate a policy with an agent (idempotent). + */ + post: operations["add_agent_policy_api_v1_agents__agent_name__policies__policy_id__post"]; + /** + * Remove policy association from agent + * @description Remove a policy association from an agent. + * + * Idempotent for existing resources: removing a non-associated link is a no-op. + * Missing agent/policy resources still return 404. + */ + delete: operations["remove_agent_policy_api_v1_agents__agent_name__policies__policy_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_name}/policy": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get agent's assigned policy (compatibility) + * @description Compatibility endpoint that returns the first associated policy. + */ + get: operations["get_agent_policy_api_v1_agents__agent_name__policy_get"]; + put?: never; + post?: never; + /** + * Remove agent's policy assignment (compatibility) + * @description Compatibility endpoint that removes all policy associations. + */ + delete: operations["delete_agent_policy_api_v1_agents__agent_name__policy_delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_name}/policy/{policy_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Assign policy to agent (compatibility) + * @description Compatibility endpoint that replaces all policy associations with one policy. + */ + post: operations["set_agent_policy_api_v1_agents__agent_name__policy__policy_id__post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/controls": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List all controls + * @description List all controls with optional filtering and cursor-based pagination. + * + * Controls are returned ordered by ID descending (newest first). + * + * Args: + * cursor: ID of the last control from the previous page (for pagination) + * limit: Maximum number of controls to return (default 20, max 100) + * name: Optional filter by name (partial, case-insensitive match) + * enabled: Optional filter by enabled status + * step_type: Optional filter by step type (built-ins: 'tool', 'llm') + * stage: Optional filter by stage ('pre' or 'post') + * execution: Optional filter by execution ('server' or 'sdk') + * tag: Optional filter by tag + * db: Database session (injected) + * + * Returns: + * ListControlsResponse with control summaries and pagination info + * + * Example: + * GET /controls?limit=10&enabled=true&step_type=tool + */ + get: operations["list_controls_api_v1_controls_get"]; + /** + * Create a new control + * @description Create a new control with a unique name and empty data. + * + * Controls define protection logic and can be added to policies. + * Use the PUT /{control_id}/data endpoint to set control configuration. + * + * Args: + * request: Control creation request with unique name + * db: Database session (injected) + * + * Returns: + * CreateControlResponse with the new control's ID + * + * Raises: + * HTTPException 409: Control with this name already exists + * HTTPException 500: Database error during creation + */ + put: operations["create_control_api_v1_controls_put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/controls/validate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Validate control configuration + * @description Validate control configuration data without saving it. + * + * Args: + * request: Control configuration data to validate + * db: Database session (injected) + * + * Returns: + * ValidateControlDataResponse with success=True if valid + */ + post: operations["validate_control_data_api_v1_controls_validate_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/controls/{control_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get control details + * @description Retrieve a control by ID including its name and configuration data. + * + * Args: + * control_id: ID of the control + * db: Database session (injected) + * + * Returns: + * GetControlResponse with control id, name, and data + * + * Raises: + * HTTPException 404: Control not found + */ + get: operations["get_control_api_v1_controls__control_id__get"]; + put?: never; + post?: never; + /** + * Delete a control + * @description Delete a control by ID. + * + * By default, deletion fails if the control is associated with any policy or agent. + * Use force=true to automatically dissociate and delete. + * + * Args: + * control_id: ID of the control to delete + * force: If true, remove associations before deleting + * db: Database session (injected) + * + * Returns: + * DeleteControlResponse with success flag and dissociation details + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 409: Control is in use (and force=false) + * HTTPException 500: Database error during deletion + */ + delete: operations["delete_control_api_v1_controls__control_id__delete"]; + options?: never; + head?: never; + /** + * Update control metadata + * @description Update control metadata (name and/or enabled status). + * + * This endpoint allows partial updates: + * - To rename: provide 'name' field + * - To enable/disable: provide 'enabled' field (updates the control's data) + * + * Args: + * control_id: ID of the control to update + * request: Fields to update (name, enabled) + * db: Database session (injected) + * + * Returns: + * PatchControlResponse with current control state + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 409: New name conflicts with existing control + * HTTPException 422: Cannot update enabled status (control has no data configured) + * HTTPException 500: Database error during update + */ + patch: operations["patch_control_api_v1_controls__control_id__patch"]; + trace?: never; + }; + "/api/v1/controls/{control_id}/data": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get control configuration data + * @description Retrieve the configuration data for a control. + * + * Control data is a JSONB field that must follow the ControlDefinition schema. + * + * Args: + * control_id: ID of the control + * db: Database session (injected) + * + * Returns: + * GetControlDataResponse with validated ControlDefinition + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 422: Control data is corrupted + */ + get: operations["get_control_data_api_v1_controls__control_id__data_get"]; + /** + * Update control configuration data + * @description Update the configuration data for a control. + * + * This replaces the entire data payload. The data is validated against + * the ControlDefinition schema. + * + * Args: + * control_id: ID of the control + * request: New control data (replaces existing) + * db: Database session (injected) + * + * Returns: + * SetControlDataResponse with success flag + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 500: Database error during update + */ + put: operations["set_control_data_api_v1_controls__control_id__data_put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/evaluation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Analyze content safety + * @description Analyze content for safety and control violations. + * + * Runs all controls assigned to the agent via policy through the + * evaluation engine. Controls are evaluated in parallel with + * cancel-on-deny for efficiency. + * + * Custom evaluators must be deployed as Evaluator classes + * with the engine. Their schemas are registered via initAgent. + * + * Optionally accepts X-Trace-Id and X-Span-Id headers for + * OpenTelemetry-compatible distributed tracing. + */ + post: operations["evaluate_api_v1_evaluation_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/evaluator-configs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List evaluator configs */ + get: operations["list_evaluator_configs_api_v1_evaluator_configs_get"]; + put?: never; + /** Create evaluator config */ + post: operations["create_evaluator_config_api_v1_evaluator_configs_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/evaluator-configs/{config_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get evaluator config */ + get: operations["get_evaluator_config_api_v1_evaluator_configs__config_id__get"]; + /** Update evaluator config */ + put: operations["update_evaluator_config_api_v1_evaluator_configs__config_id__put"]; + post?: never; + /** Delete evaluator config */ + delete: operations["delete_evaluator_config_api_v1_evaluator_configs__config_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/evaluators": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List available evaluators + * @description List all available evaluators. + * + * Returns metadata and JSON Schema for each built-in evaluator. + * + * Built-in evaluators: + * - **regex**: Regular expression pattern matching + * - **list**: List-based value matching with flexible logic + * - **json**: JSON validation with schema, types, constraints + * - **sql**: SQL query validation + * + * Custom evaluators are registered per-agent via initAgent. + * Use GET /agents/{agent_name}/evaluators to list agent-specific schemas. + */ + get: operations["get_evaluators_api_v1_evaluators_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/observability/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Ingest Events + * @description Ingest batched control execution events. + * + * Events are stored directly to the database with ~5-20ms latency. + * + * Args: + * request: Batch of events to ingest + * ingestor: Event ingestor (injected) + * + * Returns: + * BatchEventsResponse with counts of received/processed/dropped + */ + post: operations["ingest_events_api_v1_observability_events_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/observability/events/query": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Query Events + * @description Query raw control execution events. + * + * Supports filtering by: + * - trace_id: Get all events for a request + * - span_id: Get all events for a function call + * - control_execution_id: Get a specific event + * - agent_name: Filter by agent + * - control_ids: Filter by controls + * - actions: Filter by actions (allow, deny, warn, log) + * - matched: Filter by matched status + * - check_stages: Filter by check stage (pre, post) + * - applies_to: Filter by call type (llm_call, tool_call) + * - start_time/end_time: Filter by time range + * + * Results are paginated with limit/offset. + * + * Args: + * request: Query parameters + * store: Event store (injected) + * + * Returns: + * EventQueryResponse with matching events and pagination info + */ + post: operations["query_events_api_v1_observability_events_query_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/observability/stats": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Stats + * @description Get agent-level aggregated statistics. + * + * Returns totals across all controls plus per-control breakdown. + * Use /stats/controls/{control_id} for single control stats. + * + * Args: + * agent_name: Agent to get stats for + * time_range: Time range (1m, 5m, 15m, 1h, 24h, 7d, 30d, 180d, 365d) + * include_timeseries: Include time-series data points for trend visualization + * store: Event store (injected) + * + * Returns: + * StatsResponse with agent-level totals and per-control breakdown + */ + get: operations["get_stats_api_v1_observability_stats_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/observability/stats/controls/{control_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Control Stats + * @description Get statistics for a single control. + * + * Returns stats for the specified control with optional time-series. + * + * Args: + * control_id: Control ID to get stats for + * agent_name: Agent to get stats for + * time_range: Time range (1m, 5m, 15m, 1h, 24h, 7d, 30d, 180d, 365d) + * include_timeseries: Include time-series data points for trend visualization + * store: Event store (injected) + * + * Returns: + * ControlStatsResponse with control stats and optional timeseries + */ + get: operations["get_control_stats_api_v1_observability_stats_controls__control_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/observability/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Status + * @description Get observability system status. + * + * Returns basic health information. + */ + get: operations["get_status_api_v1_observability_status_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/policies": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Create a new policy + * @description Create a new empty policy with a unique name. + * + * Policies contain controls and can be assigned to agents. + * A newly created policy has no controls until they are explicitly added. + * + * Args: + * request: Policy creation request with unique name + * db: Database session (injected) + * + * Returns: + * CreatePolicyResponse with the new policy's ID + * + * Raises: + * HTTPException 409: Policy with this name already exists + * HTTPException 500: Database error during creation + */ + put: operations["create_policy_api_v1_policies_put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/policies/{policy_id}/controls": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List policy's controls + * @description List all controls associated with a policy. + * + * Args: + * policy_id: ID of the policy + * db: Database session (injected) + * + * Returns: + * GetPolicyControlsResponse with list of control IDs + * + * Raises: + * HTTPException 404: Policy not found + */ + get: operations["list_policy_controls_api_v1_policies__policy_id__controls_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/policies/{policy_id}/controls/{control_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Add control to policy + * @description Associate a control with a policy. + * + * This operation is idempotent - adding the same control multiple times has no effect. + * Agents with this policy will immediately see the added control. + * + * Args: + * policy_id: ID of the policy + * control_id: ID of the control to add + * db: Database session (injected) + * + * Returns: + * AssocResponse with success flag + * + * Raises: + * HTTPException 404: Policy or control not found + * HTTPException 500: Database error + */ + post: operations["add_control_to_policy_api_v1_policies__policy_id__controls__control_id__post"]; + /** + * Remove control from policy + * @description Remove a control from a policy. + * + * This operation is idempotent - removing a non-associated control has no effect. + * Agents with this policy will immediately lose the removed control. + * + * Args: + * policy_id: ID of the policy + * control_id: ID of the control to remove + * db: Database session (injected) + * + * Returns: + * AssocResponse with success flag + * + * Raises: + * HTTPException 404: Policy or control not found + * HTTPException 500: Database error + */ + delete: operations["remove_control_from_policy_api_v1_policies__policy_id__controls__control_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Health check + * @description Check if the server is running and responsive. + * + * This endpoint does not check database connectivity. + * + * Returns: + * HealthResponse with status and version + */ + get: operations["health_check_health_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** - * List all agents - * @description List all registered agents with cursor-based pagination. - * - * Returns a summary of each agent including identifier, policy associations, - * and counts of registered steps and evaluators. - * - * Args: - * cursor: Optional cursor for pagination (last agent name from previous page) - * limit: Pagination limit (default 20, max 100) - * name: Optional name filter (case-insensitive partial match) - * db: Database session (injected) - * - * Returns: - * ListAgentsResponse with agent summaries and pagination info - */ - get: operations['list_agents_api_v1_agents_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/initAgent': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Initialize or update an agent - * @description Register a new agent or update an existing agent's steps and metadata. - * - * This endpoint is idempotent: - * - If the agent name doesn't exist, creates a new agent - * - If the agent name exists, updates registration data in place - * - * conflict_mode controls registration conflict handling: - * - strict (default): preserve compatibility checks and conflict errors - * - overwrite: latest init payload replaces steps/evaluators and returns change summary - * - * Args: - * request: Agent metadata and step schemas - * db: Database session (injected) - * - * Returns: - * InitAgentResponse with created flag and active controls (if policy assigned) - */ - post: operations['init_agent_api_v1_agents_initAgent_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_name}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get agent details - * @description Retrieve agent metadata and all registered steps. - * - * Returns the latest version of each step (deduplicated by type+name). - * - * Args: - * agent_name: Agent identifier - * db: Database session (injected) - * - * Returns: - * GetAgentResponse with agent metadata and step list - * - * Raises: - * HTTPException 404: Agent not found - * HTTPException 422: Agent data is corrupted - */ - get: operations['get_agent_api_v1_agents__agent_name__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Modify agent (remove steps/evaluators) - * @description Remove steps and/or evaluators from an agent. - * - * This is the complement to initAgent which only adds items. - * Removals are idempotent - attempting to remove non-existent items is not an error. - * - * Args: - * agent_name: Agent identifier - * request: Lists of step/evaluator identifiers to remove - * db: Database session (injected) - * - * Returns: - * PatchAgentResponse with lists of actually removed items - * - * Raises: - * HTTPException 404: Agent not found - * HTTPException 500: Database error during update - */ - patch: operations['patch_agent_api_v1_agents__agent_name__patch']; - trace?: never; - }; - '/api/v1/agents/{agent_name}/controls': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List agent's active controls - * @description List all protection controls active for an agent. - * - * Controls include the union of policy-derived and directly associated controls. - * - * Args: - * agent_name: Agent identifier - * db: Database session (injected) - * - * Returns: - * AgentControlsResponse with list of active controls - * - * Raises: - * HTTPException 404: Agent not found - */ - get: operations['list_agent_controls_api_v1_agents__agent_name__controls_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_name}/controls/{control_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Associate control directly with agent - * @description Associate a control directly with an agent (idempotent). - */ - post: operations['add_agent_control_api_v1_agents__agent_name__controls__control_id__post']; - /** - * Remove direct control association from agent - * @description Remove a direct control association from an agent (idempotent). - */ - delete: operations['remove_agent_control_api_v1_agents__agent_name__controls__control_id__delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_name}/evaluators': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List agent's registered evaluator schemas - * @description List all evaluator schemas registered with an agent. - * - * Evaluator schemas are registered via initAgent and used for: - * - Config validation when creating Controls - * - UI to display available config options - * - * Args: - * agent_name: Agent identifier - * cursor: Optional cursor for pagination (name of last evaluator from previous page) - * limit: Pagination limit (default 20, max 100) - * db: Database session (injected) - * - * Returns: - * ListEvaluatorsResponse with evaluator schemas and pagination - * - * Raises: - * HTTPException 404: Agent not found - */ - get: operations['list_agent_evaluators_api_v1_agents__agent_name__evaluators_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_name}/evaluators/{evaluator_name}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get specific evaluator schema - * @description Get a specific evaluator schema registered with an agent. - * - * Args: - * agent_name: Agent identifier - * evaluator_name: Name of the evaluator - * db: Database session (injected) - * - * Returns: - * EvaluatorSchemaItem with schema details - * - * Raises: - * HTTPException 404: Agent or evaluator not found - */ - get: operations['get_agent_evaluator_api_v1_agents__agent_name__evaluators__evaluator_name__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_name}/policies': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List policies associated with agent - * @description List policy IDs associated with an agent. - */ - get: operations['get_agent_policies_api_v1_agents__agent_name__policies_get']; - put?: never; - post?: never; - /** - * Remove all policy associations from agent - * @description Remove all policy associations from an agent. - */ - delete: operations['remove_all_agent_policies_api_v1_agents__agent_name__policies_delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_name}/policies/{policy_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Associate policy with agent - * @description Associate a policy with an agent (idempotent). - */ - post: operations['add_agent_policy_api_v1_agents__agent_name__policies__policy_id__post']; - /** - * Remove policy association from agent - * @description Remove a policy association from an agent. - * - * Idempotent for existing resources: removing a non-associated link is a no-op. - * Missing agent/policy resources still return 404. - */ - delete: operations['remove_agent_policy_api_v1_agents__agent_name__policies__policy_id__delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_name}/policy': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get agent's assigned policy (compatibility) - * @description Compatibility endpoint that returns the first associated policy. - */ - get: operations['get_agent_policy_api_v1_agents__agent_name__policy_get']; - put?: never; - post?: never; - /** - * Remove agent's policy assignment (compatibility) - * @description Compatibility endpoint that removes all policy associations. - */ - delete: operations['delete_agent_policy_api_v1_agents__agent_name__policy_delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_name}/policy/{policy_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Assign policy to agent (compatibility) - * @description Compatibility endpoint that replaces all policy associations with one policy. - */ - post: operations['set_agent_policy_api_v1_agents__agent_name__policy__policy_id__post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/controls': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List all controls - * @description List all controls with optional filtering and cursor-based pagination. - * - * Controls are returned ordered by ID descending (newest first). - * - * Args: - * cursor: ID of the last control from the previous page (for pagination) - * limit: Maximum number of controls to return (default 20, max 100) - * name: Optional filter by name (partial, case-insensitive match) - * enabled: Optional filter by enabled status - * step_type: Optional filter by step type (built-ins: 'tool', 'llm') - * stage: Optional filter by stage ('pre' or 'post') - * execution: Optional filter by execution ('server' or 'sdk') - * tag: Optional filter by tag - * db: Database session (injected) - * - * Returns: - * ListControlsResponse with control summaries and pagination info - * - * Example: - * GET /controls?limit=10&enabled=true&step_type=tool - */ - get: operations['list_controls_api_v1_controls_get']; - /** - * Create a new control - * @description Create a new control with a unique name and empty data. - * - * Controls define protection logic and can be added to policies. - * Use the PUT /{control_id}/data endpoint to set control configuration. - * - * Args: - * request: Control creation request with unique name - * db: Database session (injected) - * - * Returns: - * CreateControlResponse with the new control's ID - * - * Raises: - * HTTPException 409: Control with this name already exists - * HTTPException 500: Database error during creation - */ - put: operations['create_control_api_v1_controls_put']; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/controls/validate': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Validate control configuration - * @description Validate control configuration data without saving it. - * - * Args: - * request: Control configuration data to validate - * db: Database session (injected) - * - * Returns: - * ValidateControlDataResponse with success=True if valid - */ - post: operations['validate_control_data_api_v1_controls_validate_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/controls/{control_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get control details - * @description Retrieve a control by ID including its name and configuration data. - * - * Args: - * control_id: ID of the control - * db: Database session (injected) - * - * Returns: - * GetControlResponse with control id, name, and data - * - * Raises: - * HTTPException 404: Control not found - */ - get: operations['get_control_api_v1_controls__control_id__get']; - put?: never; - post?: never; - /** - * Delete a control - * @description Delete a control by ID. - * - * By default, deletion fails if the control is associated with any policy or agent. - * Use force=true to automatically dissociate and delete. - * - * Args: - * control_id: ID of the control to delete - * force: If true, remove associations before deleting - * db: Database session (injected) - * - * Returns: - * DeleteControlResponse with success flag and dissociation details - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 409: Control is in use (and force=false) - * HTTPException 500: Database error during deletion - */ - delete: operations['delete_control_api_v1_controls__control_id__delete']; - options?: never; - head?: never; - /** - * Update control metadata - * @description Update control metadata (name and/or enabled status). - * - * This endpoint allows partial updates: - * - To rename: provide 'name' field - * - To enable/disable: provide 'enabled' field (updates the control's data) - * - * Args: - * control_id: ID of the control to update - * request: Fields to update (name, enabled) - * db: Database session (injected) - * - * Returns: - * PatchControlResponse with current control state - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 409: New name conflicts with existing control - * HTTPException 422: Cannot update enabled status (control has no data configured) - * HTTPException 500: Database error during update - */ - patch: operations['patch_control_api_v1_controls__control_id__patch']; - trace?: never; - }; - '/api/v1/controls/{control_id}/data': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get control configuration data - * @description Retrieve the configuration data for a control. - * - * Control data is a JSONB field that must follow the ControlDefinition schema. - * - * Args: - * control_id: ID of the control - * db: Database session (injected) - * - * Returns: - * GetControlDataResponse with validated ControlDefinition - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 422: Control data is corrupted - */ - get: operations['get_control_data_api_v1_controls__control_id__data_get']; - /** - * Update control configuration data - * @description Update the configuration data for a control. - * - * This replaces the entire data payload. The data is validated against - * the ControlDefinition schema. - * - * Args: - * control_id: ID of the control - * request: New control data (replaces existing) - * db: Database session (injected) - * - * Returns: - * SetControlDataResponse with success flag - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 500: Database error during update - */ - put: operations['set_control_data_api_v1_controls__control_id__data_put']; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/evaluation': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Analyze content safety - * @description Analyze content for safety and control violations. - * - * Runs all controls assigned to the agent via policy through the - * evaluation engine. Controls are evaluated in parallel with - * cancel-on-deny for efficiency. - * - * Custom evaluators must be deployed as Evaluator classes - * with the engine. Their schemas are registered via initAgent. - * - * Optionally accepts X-Trace-Id and X-Span-Id headers for - * OpenTelemetry-compatible distributed tracing. - */ - post: operations['evaluate_api_v1_evaluation_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/evaluator-configs': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List evaluator configs */ - get: operations['list_evaluator_configs_api_v1_evaluator_configs_get']; - put?: never; - /** Create evaluator config */ - post: operations['create_evaluator_config_api_v1_evaluator_configs_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/evaluator-configs/{config_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get evaluator config */ - get: operations['get_evaluator_config_api_v1_evaluator_configs__config_id__get']; - /** Update evaluator config */ - put: operations['update_evaluator_config_api_v1_evaluator_configs__config_id__put']; - post?: never; - /** Delete evaluator config */ - delete: operations['delete_evaluator_config_api_v1_evaluator_configs__config_id__delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/evaluators': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List available evaluators - * @description List all available evaluators. - * - * Returns metadata and JSON Schema for each built-in evaluator. - * - * Built-in evaluators: - * - **regex**: Regular expression pattern matching - * - **list**: List-based value matching with flexible logic - * - **json**: JSON validation with schema, types, constraints - * - **sql**: SQL query validation - * - * Custom evaluators are registered per-agent via initAgent. - * Use GET /agents/{agent_name}/evaluators to list agent-specific schemas. - */ - get: operations['get_evaluators_api_v1_evaluators_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/observability/events': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Ingest Events - * @description Ingest batched control execution events. - * - * Events are stored directly to the database with ~5-20ms latency. - * - * Args: - * request: Batch of events to ingest - * ingestor: Event ingestor (injected) - * - * Returns: - * BatchEventsResponse with counts of received/processed/dropped - */ - post: operations['ingest_events_api_v1_observability_events_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/observability/events/query': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Query Events - * @description Query raw control execution events. - * - * Supports filtering by: - * - trace_id: Get all events for a request - * - span_id: Get all events for a function call - * - control_execution_id: Get a specific event - * - agent_name: Filter by agent - * - control_ids: Filter by controls - * - actions: Filter by actions (allow, deny, warn, log) - * - matched: Filter by matched status - * - check_stages: Filter by check stage (pre, post) - * - applies_to: Filter by call type (llm_call, tool_call) - * - start_time/end_time: Filter by time range - * - * Results are paginated with limit/offset. - * - * Args: - * request: Query parameters - * store: Event store (injected) - * - * Returns: - * EventQueryResponse with matching events and pagination info - */ - post: operations['query_events_api_v1_observability_events_query_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/observability/stats': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Stats - * @description Get agent-level aggregated statistics. - * - * Returns totals across all controls plus per-control breakdown. - * Use /stats/controls/{control_id} for single control stats. - * - * Args: - * agent_name: Agent to get stats for - * time_range: Time range (1m, 5m, 15m, 1h, 24h, 7d, 30d, 180d, 365d) - * include_timeseries: Include time-series data points for trend visualization - * store: Event store (injected) - * - * Returns: - * StatsResponse with agent-level totals and per-control breakdown - */ - get: operations['get_stats_api_v1_observability_stats_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/observability/stats/controls/{control_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Control Stats - * @description Get statistics for a single control. - * - * Returns stats for the specified control with optional time-series. - * - * Args: - * control_id: Control ID to get stats for - * agent_name: Agent to get stats for - * time_range: Time range (1m, 5m, 15m, 1h, 24h, 7d, 30d, 180d, 365d) - * include_timeseries: Include time-series data points for trend visualization - * store: Event store (injected) - * - * Returns: - * ControlStatsResponse with control stats and optional timeseries - */ - get: operations['get_control_stats_api_v1_observability_stats_controls__control_id__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/observability/status': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Status - * @description Get observability system status. - * - * Returns basic health information. - */ - get: operations['get_status_api_v1_observability_status_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/policies': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** - * Create a new policy - * @description Create a new empty policy with a unique name. - * - * Policies contain controls and can be assigned to agents. - * A newly created policy has no controls until they are explicitly added. - * - * Args: - * request: Policy creation request with unique name - * db: Database session (injected) - * - * Returns: - * CreatePolicyResponse with the new policy's ID - * - * Raises: - * HTTPException 409: Policy with this name already exists - * HTTPException 500: Database error during creation - */ - put: operations['create_policy_api_v1_policies_put']; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/policies/{policy_id}/controls': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List policy's controls - * @description List all controls associated with a policy. - * - * Args: - * policy_id: ID of the policy - * db: Database session (injected) - * - * Returns: - * GetPolicyControlsResponse with list of control IDs - * - * Raises: - * HTTPException 404: Policy not found - */ - get: operations['list_policy_controls_api_v1_policies__policy_id__controls_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/policies/{policy_id}/controls/{control_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Add control to policy - * @description Associate a control with a policy. - * - * This operation is idempotent - adding the same control multiple times has no effect. - * Agents with this policy will immediately see the added control. - * - * Args: - * policy_id: ID of the policy - * control_id: ID of the control to add - * db: Database session (injected) - * - * Returns: - * AssocResponse with success flag - * - * Raises: - * HTTPException 404: Policy or control not found - * HTTPException 500: Database error - */ - post: operations['add_control_to_policy_api_v1_policies__policy_id__controls__control_id__post']; - /** - * Remove control from policy - * @description Remove a control from a policy. - * - * This operation is idempotent - removing a non-associated control has no effect. - * Agents with this policy will immediately lose the removed control. - * - * Args: - * policy_id: ID of the policy - * control_id: ID of the control to remove - * db: Database session (injected) - * - * Returns: - * AssocResponse with success flag - * - * Raises: - * HTTPException 404: Policy or control not found - * HTTPException 500: Database error - */ - delete: operations['remove_control_from_policy_api_v1_policies__policy_id__controls__control_id__delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/health': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Health check - * @description Check if the server is running and responsive. - * - * This endpoint does not check database connectivity. - * - * Returns: - * HealthResponse with status and version - */ - get: operations['health_check_health_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; } export type webhooks = Record; export interface components { - schemas: { - /** - * Agent - * @description Agent metadata for registration and tracking. - * - * An agent represents an AI system that can be protected and monitored. - * Each agent has a unique immutable name and can have multiple steps registered with it. - * @example { - * "agent_description": "Handles customer inquiries and support tickets", - * "agent_metadata": { - * "environment": "production", - * "team": "support" - * }, - * "agent_name": "customer-service-bot", - * "agent_version": "1.0.0" - * } - */ - Agent: { - /** - * Agent Created At - * @description ISO 8601 timestamp when agent was created - */ - agent_created_at?: string | null; - /** - * Agent Description - * @description Optional description of the agent's purpose - */ - agent_description?: string | null; - /** - * Agent Metadata - * @description Free-form metadata dictionary for custom properties - */ - agent_metadata?: { - [key: string]: unknown; - } | null; - /** - * Agent Name - * @description Unique immutable identifier for the agent - */ - agent_name: string; - /** - * Agent Updated At - * @description ISO 8601 timestamp when agent was last updated - */ - agent_updated_at?: string | null; - /** - * Agent Version - * @description Semantic version string (e.g. '1.0.0') - */ - agent_version?: string | null; - }; - /** AgentControlsResponse */ - AgentControlsResponse: { - /** - * Controls - * @description List of active controls associated with the agent - */ - controls: components['schemas']['Control'][]; - }; - /** - * AgentRef - * @description Reference to an agent (for listing which agents use a control). - */ - AgentRef: { - /** - * Agent Name - * @description Agent name - */ - agent_name: string; - }; - /** - * AgentSummary - * @description Summary of an agent for list responses. - */ - AgentSummary: { - /** - * Active Controls Count - * @description Number of active controls for this agent - * @default 0 - */ - active_controls_count: number; - /** - * Agent Name - * @description Unique identifier of the agent - */ - agent_name: string; - /** - * Created At - * @description ISO 8601 timestamp when agent was created - */ - created_at?: string | null; - /** - * Evaluator Count - * @description Number of evaluators registered with the agent - * @default 0 - */ - evaluator_count: number; - /** - * Policy Ids - * @description IDs of policies associated with the agent - */ - policy_ids?: number[]; - /** - * Step Count - * @description Number of steps registered with the agent - * @default 0 - */ - step_count: number; - }; - /** AssocResponse */ - AssocResponse: { - /** - * Success - * @description Whether the association change succeeded - */ - success: boolean; - }; - /** - * BatchEventsRequest - * @description Request model for batch event ingestion. - * - * SDKs batch events and send them to the server periodically. - * This reduces HTTP overhead significantly (100x reduction). - * - * Attributes: - * events: List of control execution events to ingest - * @example { - * "events": [ - * { - * "action": "deny", - * "agent_name": "my-agent", - * "applies_to": "llm_call", - * "check_stage": "pre", - * "confidence": 0.95, - * "control_id": 123, - * "control_name": "sql-injection-check", - * "matched": true, - * "span_id": "00f067aa0ba902b7", - * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" - * } - * ] - * } - */ - BatchEventsRequest: { - /** - * Events - * @description List of events to ingest - */ - events: components['schemas']['ControlExecutionEvent'][]; - }; - /** - * BatchEventsResponse - * @description Response model for batch event ingestion. - * - * Attributes: - * received: Number of events received - * enqueued: Number of events successfully enqueued - * dropped: Number of events dropped (queue full) - * status: Overall status ('queued', 'partial', 'failed') - */ - BatchEventsResponse: { - /** - * Dropped - * @description Number of events dropped - */ - dropped: number; - /** - * Enqueued - * @description Number of events enqueued - */ - enqueued: number; - /** - * Received - * @description Number of events received - */ - received: number; - /** - * Status - * @description Overall ingestion status - * @enum {string} - */ - status: 'queued' | 'partial' | 'failed'; - }; - /** - * ConflictMode - * @description Conflict handling mode for initAgent registration updates. - * - * STRICT preserves compatibility checks and raises conflicts on incompatible changes. - * OVERWRITE applies latest-init-wins replacement for steps and evaluators. - * @enum {string} - */ - ConflictMode: 'strict' | 'overwrite'; - /** - * Control - * @description A control with identity and configuration. - * - * Note: Only fully-configured controls (with valid ControlDefinition) - * are returned from API endpoints. Unconfigured controls are filtered out. - */ - Control: { - control: components['schemas']['ControlDefinition-Output']; - /** Id */ - id: number; - /** Name */ - name: string; - }; - /** - * ControlAction - * @description What to do when control matches. - */ - ControlAction: { - /** - * Decision - * @description Action to take when control is triggered - * @enum {string} - */ - decision: 'allow' | 'deny' | 'steer' | 'warn' | 'log'; - /** @description Steering context object for steer actions. Strongly recommended when decision='steer' to provide correction suggestions. If not provided, the evaluator result message will be used as fallback. */ - steering_context?: components['schemas']['SteeringContext'] | null; - }; - /** - * ControlDefinition - * @description A control definition to evaluate agent interactions. - * - * This model contains only the logic and configuration. - * Identity fields (id, name) are managed by the database. - * @example { - * "action": { - * "decision": "deny" - * }, - * "description": "Block outputs containing US Social Security Numbers", - * "enabled": true, - * "evaluator": { - * "config": { - * "pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b" - * }, - * "name": "regex" - * }, - * "execution": "server", - * "scope": { - * "stages": [ - * "post" - * ], - * "step_types": [ - * "llm" - * ] - * }, - * "selector": { - * "path": "output" - * }, - * "tags": [ - * "pii", - * "compliance" - * ] - * } - */ - 'ControlDefinition-Input': { - /** @description What action to take when control matches */ - action: components['schemas']['ControlAction']; - /** - * Description - * @description Detailed description of the control - */ - description?: string | null; - /** - * Enabled - * @description Whether this control is active - * @default true - */ - enabled: boolean; - /** @description How to evaluate the selected data */ - evaluator: components['schemas']['EvaluatorSpec']; - /** - * Execution - * @description Where this control executes - * @enum {string} - */ - execution: 'server' | 'sdk'; - /** @description Which steps and stages this control applies to */ - scope?: components['schemas']['ControlScope']; - /** @description What data to select from the payload */ - selector: components['schemas']['ControlSelector']; - /** - * Tags - * @description Tags for categorization - */ - tags?: string[]; - }; - /** - * ControlDefinition - * @description A control definition to evaluate agent interactions. - * - * This model contains only the logic and configuration. - * Identity fields (id, name) are managed by the database. - * @example { - * "action": { - * "decision": "deny" - * }, - * "description": "Block outputs containing US Social Security Numbers", - * "enabled": true, - * "evaluator": { - * "config": { - * "pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b" - * }, - * "name": "regex" - * }, - * "execution": "server", - * "scope": { - * "stages": [ - * "post" - * ], - * "step_types": [ - * "llm" - * ] - * }, - * "selector": { - * "path": "output" - * }, - * "tags": [ - * "pii", - * "compliance" - * ] - * } - */ - 'ControlDefinition-Output': { - /** @description What action to take when control matches */ - action: components['schemas']['ControlAction']; - /** - * Description - * @description Detailed description of the control - */ - description?: string | null; - /** - * Enabled - * @description Whether this control is active - * @default true - */ - enabled: boolean; - /** @description How to evaluate the selected data */ - evaluator: components['schemas']['EvaluatorSpec']; - /** - * Execution - * @description Where this control executes - * @enum {string} - */ - execution: 'server' | 'sdk'; - /** @description Which steps and stages this control applies to */ - scope?: components['schemas']['ControlScope']; - /** @description What data to select from the payload */ - selector: components['schemas']['ControlSelector']; - /** - * Tags - * @description Tags for categorization - */ - tags?: string[]; - }; - /** - * ControlExecutionEvent - * @description Represents a single control execution event. - * - * This is the core observability data model, capturing: - * - Identity: control_execution_id, trace_id, span_id (OpenTelemetry-compatible) - * - Context: agent, control, check stage, applies to - * - Result: action taken, whether matched, confidence score - * - Timing: when it happened, how long it took - * - Optional details: evaluator name, selector path, errors, metadata - * - * Attributes: - * control_execution_id: Unique ID for this specific control execution - * trace_id: OpenTelemetry-compatible trace ID (128-bit hex, 32 chars) - * span_id: OpenTelemetry-compatible span ID (64-bit hex, 16 chars) - * agent_name: Identifier of the agent that executed the control - * control_id: Database ID of the control - * control_name: Name of the control (denormalized for queries) - * check_stage: "pre" (before execution) or "post" (after execution) - * applies_to: "llm_call" or "tool_call" - * action: The action taken (allow, deny, warn, log) - * matched: Whether the control evaluator matched - * confidence: Confidence score from the evaluator (0.0-1.0) - * timestamp: When the control was executed (UTC) - * execution_duration_ms: How long the control evaluation took - * evaluator_name: Name of the evaluator used - * selector_path: The selector path used to extract data - * error_message: Error message if evaluation failed - * metadata: Additional metadata for extensibility - * @example { - * "action": "deny", - * "agent_name": "my-agent", - * "applies_to": "llm_call", - * "check_stage": "pre", - * "confidence": 0.95, - * "control_execution_id": "550e8400-e29b-41d4-a716-446655440000", - * "control_id": 123, - * "control_name": "sql-injection-check", - * "evaluator_name": "regex", - * "execution_duration_ms": 15.3, - * "matched": true, - * "selector_path": "input", - * "span_id": "00f067aa0ba902b7", - * "timestamp": "2025-01-09T10:30:00Z", - * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" - * } - */ - ControlExecutionEvent: { - /** - * Action - * @description Action taken by the control - * @enum {string} - */ - action: 'allow' | 'deny' | 'steer' | 'warn' | 'log'; - /** - * Agent Name - * @description Identifier of the agent - */ - agent_name: string; - /** - * Applies To - * @description Type of call: 'llm_call' or 'tool_call' - * @enum {string} - */ - applies_to: 'llm_call' | 'tool_call'; - /** - * Check Stage - * @description Check stage: 'pre' or 'post' - * @enum {string} - */ - check_stage: 'pre' | 'post'; - /** - * Confidence - * @description Confidence score (0.0 to 1.0) - */ - confidence: number; - /** - * Control Execution Id - * @description Unique ID for this control execution - */ - control_execution_id?: string; - /** - * Control Id - * @description Database ID of the control - */ - control_id: number; - /** - * Control Name - * @description Name of the control (denormalized) - */ - control_name: string; - /** - * Error Message - * @description Error message if evaluation failed - */ - error_message?: string | null; - /** - * Evaluator Name - * @description Name of the evaluator used - */ - evaluator_name?: string | null; - /** - * Execution Duration Ms - * @description Execution duration in milliseconds - */ - execution_duration_ms?: number | null; - /** - * Matched - * @description Whether the evaluator matched (True) or not (False) - */ - matched: boolean; - /** - * Metadata - * @description Additional metadata - */ - metadata?: { - [key: string]: unknown; - }; - /** - * Selector Path - * @description Selector path used to extract data - */ - selector_path?: string | null; - /** - * Span Id - * @description Span ID for distributed tracing (SDK generates OTEL-compatible 16-char hex) - */ - span_id: string; - /** - * Timestamp - * Format: date-time - * @description When the control was executed (UTC) - */ - timestamp?: string; - /** - * Trace Id - * @description Trace ID for distributed tracing (SDK generates OTEL-compatible 32-char hex) - */ - trace_id: string; - }; - /** - * ControlMatch - * @description Represents a control evaluation result (match, non-match, or error). - */ - ControlMatch: { - /** - * Action - * @description Action configured for this control - * @enum {string} - */ - action: 'allow' | 'deny' | 'steer' | 'warn' | 'log'; - /** - * Control Execution Id - * @description Unique ID for this control execution (generated by engine) - */ - control_execution_id?: string; - /** - * Control Id - * @description Database ID of the control - */ - control_id: number; - /** - * Control Name - * @description Name of the control - */ - control_name: string; - /** @description Evaluator result (confidence, message, metadata) */ - result: components['schemas']['EvaluatorResult']; - /** @description Steering context for steer actions if configured */ - steering_context?: components['schemas']['SteeringContext'] | null; - }; - /** - * ControlScope - * @description Defines when a control applies to a Step. - * @example { - * "stages": [ - * "pre" - * ], - * "step_types": [ - * "tool" - * ] - * } - * @example { - * "step_names": [ - * "search_db", - * "fetch_user" - * ] - * } - * @example { - * "step_name_regex": "^db_.*" - * } - * @example { - * "stages": [ - * "post" - * ], - * "step_types": [ - * "llm" - * ] - * } - */ - ControlScope: { - /** - * Stages - * @description Evaluation stages this control applies to - */ - stages?: ('pre' | 'post')[] | null; - /** - * Step Name Regex - * @description RE2 pattern matched with search() against step name - */ - step_name_regex?: string | null; - /** - * Step Names - * @description Exact step names this control applies to - */ - step_names?: string[] | null; - /** - * Step Types - * @description Step types this control applies to (omit to apply to all types). Built-in types are 'tool' and 'llm'. - */ - step_types?: string[] | null; - }; - /** - * ControlSelector - * @description Selects data from a Step payload. - * - * - path: which slice of the Step to feed into the evaluator. Optional, defaults to "*" - * meaning the entire Step object. - * @example { - * "path": "output" - * } - * @example { - * "path": "context.user_id" - * } - * @example { - * "path": "input" - * } - * @example { - * "path": "*" - * } - * @example { - * "path": "name" - * } - * @example { - * "path": "output" - * } - */ - ControlSelector: { - /** - * Path - * @description Path to data using dot notation. Examples: 'input', 'output', 'context.user_id', 'name', 'type', '*' - * @default * - */ - path: string | null; - }; - /** - * ControlStats - * @description Aggregated statistics for a single control. - * - * Attributes: - * control_id: Database ID of the control - * control_name: Name of the control - * execution_count: Total number of executions - * match_count: Number of times the control matched - * non_match_count: Number of times the control did not match - * allow_count: Number of allow actions - * deny_count: Number of deny actions - * steer_count: Number of steer actions - * warn_count: Number of warn actions - * log_count: Number of log actions - * error_count: Number of errors during evaluation - * avg_confidence: Average confidence score - * avg_duration_ms: Average execution duration in milliseconds - */ - ControlStats: { - /** - * Allow Count - * @description Allow actions - */ - allow_count: number; - /** - * Avg Confidence - * @description Average confidence - */ - avg_confidence: number; - /** - * Avg Duration Ms - * @description Average duration (ms) - */ - avg_duration_ms?: number | null; - /** - * Control Id - * @description Control ID - */ - control_id: number; - /** - * Control Name - * @description Control name - */ - control_name: string; - /** - * Deny Count - * @description Deny actions - */ - deny_count: number; - /** - * Error Count - * @description Evaluation errors - */ - error_count: number; - /** - * Execution Count - * @description Total executions - */ - execution_count: number; - /** - * Log Count - * @description Log actions - */ - log_count: number; - /** - * Match Count - * @description Total matches - */ - match_count: number; - /** - * Non Match Count - * @description Total non-matches - */ - non_match_count: number; - /** - * Steer Count - * @description Steer actions - */ - steer_count: number; - /** - * Warn Count - * @description Warn actions - */ - warn_count: number; - }; - /** - * ControlStatsResponse - * @description Response model for control-level statistics. - * - * Contains stats for a single control (with optional timeseries). - * - * Attributes: - * agent_name: Agent identifier - * time_range: Time range used - * control_id: Control ID - * control_name: Control name - * stats: Control statistics (includes timeseries when requested) - */ - ControlStatsResponse: { - /** - * Agent Name - * @description Agent identifier - */ - agent_name: string; - /** - * Control Id - * @description Control ID - */ - control_id: number; - /** - * Control Name - * @description Control name - */ - control_name: string; - /** @description Control statistics */ - stats: components['schemas']['StatsTotals']; - /** - * Time Range - * @description Time range used - */ - time_range: string; - }; - /** - * ControlSummary - * @description Summary of a control for list responses. - */ - ControlSummary: { - /** - * Description - * @description Control description - */ - description?: string | null; - /** - * Enabled - * @description Whether control is enabled - * @default true - */ - enabled: boolean; - /** - * Execution - * @description 'server' or 'sdk' - */ - execution?: string | null; - /** - * Id - * @description Control ID - */ - id: number; - /** - * Name - * @description Control name - */ - name: string; - /** - * Stages - * @description Evaluation stages in scope - */ - stages?: string[] | null; - /** - * Step Types - * @description Step types in scope - */ - step_types?: string[] | null; - /** - * Tags - * @description Control tags - */ - tags?: string[]; - /** @description Agent using this control */ - used_by_agent?: components['schemas']['AgentRef'] | null; - /** - * Used By Agents Count - * @description Number of unique agents using this control - * @default 0 - */ - used_by_agents_count: number; - }; - /** CreateControlRequest */ - CreateControlRequest: { - /** - * Name - * @description Unique control name (letters, numbers, hyphens, underscores) - */ - name: string; - }; - /** CreateControlResponse */ - CreateControlResponse: { - /** - * Control Id - * @description Identifier of the created control - */ - control_id: number; - }; - /** - * CreateEvaluatorConfigRequest - * @description Request to create an evaluator config template. - */ - CreateEvaluatorConfigRequest: { - /** - * Config - * @description Evaluator-specific configuration - */ - config: { - [key: string]: unknown; - }; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Evaluator - * @description Evaluator name (built-in or custom) - */ - evaluator: string; - /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) - */ - name: string; - }; - /** CreatePolicyRequest */ - CreatePolicyRequest: { - /** - * Name - * @description Unique policy name (letters, numbers, hyphens, underscores) - */ - name: string; - }; - /** CreatePolicyResponse */ - CreatePolicyResponse: { - /** - * Policy Id - * @description Identifier of the created policy - */ - policy_id: number; - }; - /** - * DeleteControlResponse - * @description Response for deleting a control. - */ - DeleteControlResponse: { - /** - * Dissociated From - * @description Deprecated: policy IDs the control was removed from before deletion - */ - dissociated_from?: number[]; - /** - * Dissociated From Agents - * @description Agent names the control was removed from before deletion - */ - dissociated_from_agents?: string[]; - /** - * Dissociated From Policies - * @description Policy IDs the control was removed from before deletion - */ - dissociated_from_policies?: number[]; - /** - * Success - * @description Whether the control was deleted - */ - success: boolean; - }; - /** - * DeleteEvaluatorConfigResponse - * @description Response for deleting an evaluator config. - */ - DeleteEvaluatorConfigResponse: { - /** - * Success - * @description Whether the evaluator config was deleted - */ - success: boolean; - }; - /** - * DeletePolicyResponse - * @description Compatibility response for singular policy deletion endpoint. - */ - DeletePolicyResponse: { - /** - * Success - * @description Whether the request succeeded - */ - success: boolean; - }; - /** - * EvaluationRequest - * @description Request model for evaluation analysis. - * - * Used to analyze agent interactions for safety violations, - * policy compliance, and control rules. - * - * Attributes: - * agent_name: Unique identifier of the agent making the request - * step: Step payload for evaluation - * stage: 'pre' (before execution) or 'post' (after execution) - * @example { - * "agent_name": "customer-service-bot", - * "stage": "pre", - * "step": { - * "context": { - * "session_id": "abc123", - * "user_id": "user123" - * }, - * "input": "What is the customer's credit card number?", - * "name": "support-answer", - * "type": "llm" - * } - * } - * @example { - * "agent_name": "customer-service-bot", - * "stage": "post", - * "step": { - * "context": { - * "session_id": "abc123", - * "user_id": "user123" - * }, - * "input": "What is the customer's credit card number?", - * "name": "support-answer", - * "output": "I cannot share sensitive payment information.", - * "type": "llm" - * } - * } - * @example { - * "agent_name": "customer-service-bot", - * "stage": "pre", - * "step": { - * "context": { - * "user_id": "user123" - * }, - * "input": { - * "query": "SELECT * FROM users" - * }, - * "name": "search_database", - * "type": "tool" - * } - * } - * @example { - * "agent_name": "customer-service-bot", - * "stage": "post", - * "step": { - * "context": { - * "user_id": "user123" - * }, - * "input": { - * "query": "SELECT * FROM users" - * }, - * "name": "search_database", - * "output": { - * "results": [] - * }, - * "type": "tool" - * } - * } - */ - EvaluationRequest: { - /** - * Agent Name - * @description Identifier of the agent making the evaluation request - */ - agent_name: string; - /** - * Stage - * @description Evaluation stage: 'pre' or 'post' - * @enum {string} - */ - stage: 'pre' | 'post'; - /** @description Agent step payload to evaluate */ - step: components['schemas']['Step']; - }; - /** - * EvaluationResponse - * @description Response model from evaluation analysis (server-side). - * - * This is what the server returns. The SDK may transform this - * into an EvaluationResult for client convenience. - * - * Attributes: - * is_safe: Whether the content is considered safe - * confidence: Confidence score between 0.0 and 1.0 - * reason: Optional explanation for the decision - * matches: List of controls that matched/triggered (if any) - * errors: List of controls that failed during evaluation (if any) - * non_matches: List of controls that were evaluated but did not match (if any) - */ - EvaluationResponse: { - /** - * Confidence - * @description Confidence score (0.0 to 1.0) - */ - confidence: number; - /** - * Errors - * @description List of controls that failed during evaluation (if any) - */ - errors?: components['schemas']['ControlMatch'][] | null; - /** - * Is Safe - * @description Whether content is safe - */ - is_safe: boolean; - /** - * Matches - * @description List of controls that matched/triggered (if any) - */ - matches?: components['schemas']['ControlMatch'][] | null; - /** - * Non Matches - * @description List of controls that were evaluated but did not match (if any) - */ - non_matches?: components['schemas']['ControlMatch'][] | null; - /** - * Reason - * @description Explanation for the decision - */ - reason?: string | null; - }; - /** - * EvaluatorConfigItem - * @description Evaluator config template stored in the server. - */ - EvaluatorConfigItem: { - /** - * Config - * @description Evaluator-specific configuration - */ - config: { - [key: string]: unknown; - }; - /** - * Created At - * @description ISO 8601 created timestamp - */ - created_at?: string | null; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Evaluator - * @description Evaluator name (built-in or custom) - */ - evaluator: string; - /** - * Id - * @description Evaluator config ID - */ - id: number; - /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) - */ - name: string; - /** - * Updated At - * @description ISO 8601 updated timestamp - */ - updated_at?: string | null; - }; - /** - * EvaluatorInfo - * @description Information about a registered evaluator. - */ - EvaluatorInfo: { - /** - * Config Schema - * @description JSON Schema for config - */ - config_schema: { - [key: string]: unknown; - }; - /** - * Description - * @description Evaluator description - */ - description: string; - /** - * Name - * @description Evaluator name - */ - name: string; - /** - * Requires Api Key - * @description Whether evaluator requires API key - */ - requires_api_key: boolean; - /** - * Timeout Ms - * @description Default timeout in milliseconds - */ - timeout_ms: number; - /** - * Version - * @description Evaluator version - */ - version: string; - }; - /** - * EvaluatorResult - * @description Result from a control evaluator. - * - * The `error` field indicates evaluator failures, NOT validation failures: - * - Set `error` for: evaluator crashes, timeouts, missing dependencies, external service errors - * - Do NOT set `error` for: invalid input, syntax errors, schema violations, constraint failures - * - * When `error` is set, `matched` must be False (fail-open on evaluator errors). - * When `error` is None, `matched` reflects the actual validation result. - * - * This distinction allows: - * - Clients to distinguish "data violated rules" from "evaluator is broken" - * - Observability systems to monitor evaluator health separately from validation outcomes - */ - EvaluatorResult: { - /** - * Confidence - * @description Confidence in the evaluation - */ - confidence: number; - /** - * Error - * @description Error message if evaluation failed internally. When set, matched=False is due to error, not actual evaluation. - */ - error?: string | null; - /** - * Matched - * @description Whether the pattern matched - */ - matched: boolean; - /** - * Message - * @description Explanation of the result - */ - message?: string | null; - /** - * Metadata - * @description Additional result metadata - */ - metadata?: { - [key: string]: unknown; - } | null; - }; - /** - * EvaluatorSchema - * @description Schema for a custom evaluator registered with an agent. - * - * Custom evaluators are Evaluator classes deployed with the engine. - * This schema is registered via initAgent for validation and UI purposes. - */ - EvaluatorSchema: { - /** - * Config Schema - * @description JSON Schema for evaluator config validation - */ - config_schema?: { - [key: string]: unknown; - }; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Name - * @description Unique evaluator name - */ - name: string; - }; - /** - * EvaluatorSchemaItem - * @description Evaluator schema summary for list response. - */ - EvaluatorSchemaItem: { - /** Config Schema */ - config_schema: { - [key: string]: unknown; - }; - /** Description */ - description: string | null; - /** Name */ - name: string; - }; - /** - * EvaluatorSpec - * @description Evaluator specification. See GET /evaluators for available evaluators and schemas. - * - * Evaluator reference formats: - * - Built-in: "regex", "list", "json", "sql" - * - External: "galileo.luna2" (requires agent-control-evaluators[galileo]) - * - Agent-scoped: "my-agent:my-evaluator" (validated in endpoint, not here) - */ - EvaluatorSpec: { - /** - * Config - * @description Evaluator-specific configuration - * @example { - * "pattern": "\\d{3}-\\d{2}-\\d{4}" - * } - * @example { - * "logic": "any", - * "values": [ - * "admin" - * ] - * } - */ - config: { - [key: string]: unknown; - }; - /** - * Name - * @description Evaluator name or agent-scoped reference (agent:evaluator) - * @example regex - * @example list - * @example my-agent:pii-detector - */ - name: string; - }; - /** - * EventQueryRequest - * @description Request model for querying raw events. - * - * Supports filtering by various criteria and pagination. - * - * Attributes: - * trace_id: Filter by trace ID (get all events for a request) - * span_id: Filter by span ID (get all events for a function call) - * control_execution_id: Filter by specific event ID - * agent_name: Filter by agent identifier - * control_ids: Filter by control IDs - * actions: Filter by actions (allow, deny, steer, warn, log) - * matched: Filter by matched status - * check_stages: Filter by check stages (pre, post) - * applies_to: Filter by call type (llm_call, tool_call) - * start_time: Filter events after this time - * end_time: Filter events before this time - * limit: Maximum number of events to return - * offset: Offset for pagination - * @example { - * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" - * } - * @example { - * "actions": [ - * "deny", - * "warn" - * ], - * "agent_name": "my-agent", - * "limit": 50, - * "start_time": "2025-01-09T00:00:00Z" - * } - */ - EventQueryRequest: { - /** - * Actions - * @description Filter by actions - */ - actions?: ('allow' | 'deny' | 'steer' | 'warn' | 'log')[] | null; - /** - * Agent Name - * @description Filter by agent identifier - */ - agent_name?: string | null; - /** - * Applies To - * @description Filter by call types - */ - applies_to?: ('llm_call' | 'tool_call')[] | null; - /** - * Check Stages - * @description Filter by check stages - */ - check_stages?: ('pre' | 'post')[] | null; - /** - * Control Execution Id - * @description Filter by specific event ID - */ - control_execution_id?: string | null; - /** - * Control Ids - * @description Filter by control IDs - */ - control_ids?: number[] | null; - /** - * End Time - * @description Filter events before this time - */ - end_time?: string | null; - /** - * Limit - * @description Maximum events - * @default 100 - */ - limit: number; - /** - * Matched - * @description Filter by matched status - */ - matched?: boolean | null; - /** - * Offset - * @description Pagination offset - * @default 0 - */ - offset: number; - /** - * Span Id - * @description Filter by span ID (all events for a function) - */ - span_id?: string | null; - /** - * Start Time - * @description Filter events after this time - */ - start_time?: string | null; - /** - * Trace Id - * @description Filter by trace ID (all events for a request) - */ - trace_id?: string | null; - }; - /** - * EventQueryResponse - * @description Response model for event queries. - * - * Attributes: - * events: List of matching events - * total: Total number of matching events (for pagination) - * limit: Limit used in query - * offset: Offset used in query - */ - EventQueryResponse: { - /** - * Events - * @description Matching events - */ - events: components['schemas']['ControlExecutionEvent'][]; - /** - * Limit - * @description Limit used in query - */ - limit: number; - /** - * Offset - * @description Offset used in query - */ - offset: number; - /** - * Total - * @description Total matching events - */ - total: number; - }; - /** GetAgentPoliciesResponse */ - GetAgentPoliciesResponse: { - /** - * Policy Ids - * @description IDs of policies associated with the agent - */ - policy_ids?: number[]; - }; - /** - * GetAgentResponse - * @description Response containing agent details and registered steps. - */ - GetAgentResponse: { - /** @description Agent metadata */ - agent: components['schemas']['Agent']; - /** - * Evaluators - * @description Custom evaluators registered with this agent - */ - evaluators?: components['schemas']['EvaluatorSchema'][]; - /** - * Steps - * @description Steps registered with this agent - */ - steps: components['schemas']['StepSchema'][]; - }; - /** GetControlDataResponse */ - GetControlDataResponse: { - /** @description Control data payload */ - data: components['schemas']['ControlDefinition-Output']; - }; - /** - * GetControlResponse - * @description Response containing control details. - */ - GetControlResponse: { - /** @description Control configuration data (None if not yet configured) */ - data?: components['schemas']['ControlDefinition-Output'] | null; - /** - * Id - * @description Control ID - */ - id: number; - /** - * Name - * @description Control name - */ - name: string; - }; - /** - * GetPolicyControlsResponse - * @description Response containing control IDs associated with a policy. - */ - GetPolicyControlsResponse: { - /** - * Control Ids - * @description List of control IDs associated with the policy - */ - control_ids: number[]; - }; - /** - * GetPolicyResponse - * @description Compatibility response for singular policy retrieval endpoint. - */ - GetPolicyResponse: { - /** - * Policy Id - * @description Associated policy ID - */ - policy_id: number; - }; - /** HTTPValidationError */ - HTTPValidationError: { - /** Detail */ - detail?: components['schemas']['ValidationError'][]; - }; - /** - * HealthResponse - * @description Health check response model. - * - * Attributes: - * status: Current health status (e.g., "healthy", "degraded", "unhealthy") - * version: Application version - */ - HealthResponse: { - /** Status */ - status: string; - /** Version */ - version: string; - }; - /** - * InitAgentEvaluatorRemoval - * @description Details for an evaluator removed during overwrite mode. - */ - InitAgentEvaluatorRemoval: { - /** - * Control Ids - * @description IDs of active controls referencing this evaluator - */ - control_ids?: number[]; - /** - * Control Names - * @description Names of active controls referencing this evaluator - */ - control_names?: string[]; - /** - * Name - * @description Evaluator name removed by overwrite - */ - name: string; - /** - * Referenced By Active Controls - * @description Whether this evaluator is still referenced by active controls - * @default false - */ - referenced_by_active_controls: boolean; - }; - /** - * InitAgentOverwriteChanges - * @description Detailed change summary for initAgent overwrite mode. - */ - InitAgentOverwriteChanges: { - /** - * Evaluator Removals - * @description Per-evaluator removal details, including active control references - */ - evaluator_removals?: components['schemas']['InitAgentEvaluatorRemoval'][]; - /** - * Evaluators Added - * @description Evaluator names added by overwrite - */ - evaluators_added?: string[]; - /** - * Evaluators Removed - * @description Evaluator names removed by overwrite - */ - evaluators_removed?: string[]; - /** - * Evaluators Updated - * @description Existing evaluator names updated by overwrite - */ - evaluators_updated?: string[]; - /** - * Metadata Changed - * @description Whether agent metadata changed - * @default false - */ - metadata_changed: boolean; - /** - * Steps Added - * @description Steps added by overwrite - */ - steps_added?: components['schemas']['StepKey'][]; - /** - * Steps Removed - * @description Steps removed by overwrite - */ - steps_removed?: components['schemas']['StepKey'][]; - /** - * Steps Updated - * @description Existing steps updated by overwrite - */ - steps_updated?: components['schemas']['StepKey'][]; - }; - /** - * InitAgentRequest - * @description Request to initialize or update an agent registration. - * @example { - * "agent": { - * "agent_description": "Handles customer inquiries", - * "agent_name": "customer-service-bot", - * "agent_version": "1.0.0" - * }, - * "evaluators": [ - * { - * "config_schema": { - * "properties": { - * "sensitivity": { - * "type": "string" - * } - * }, - * "type": "object" - * }, - * "description": "Detects PII in text", - * "name": "pii-detector" - * } - * ], - * "steps": [ - * { - * "input_schema": { - * "query": { - * "type": "string" - * } - * }, - * "name": "search_kb", - * "output_schema": { - * "results": { - * "type": "array" - * } - * }, - * "type": "tool" - * } - * ] - * } - */ - InitAgentRequest: { - /** @description Agent metadata including ID, name, and version */ - agent: components['schemas']['Agent']; - /** - * @description Conflict handling mode for init registration updates. 'strict' preserves existing compatibility checks. 'overwrite' applies latest-init-wins replacement for steps and evaluators. - * @default strict - */ - conflict_mode: components['schemas']['ConflictMode']; - /** - * Evaluators - * @description Custom evaluator schemas for config validation - */ - evaluators?: components['schemas']['EvaluatorSchema'][]; - /** - * Force Replace - * @description If true, replace corrupted agent data instead of failing. Use only when agent data is corrupted and cannot be parsed. - * @default false - */ - force_replace: boolean; - /** - * Steps - * @description List of steps available to the agent - */ - steps?: components['schemas']['StepSchema'][]; - }; - /** - * InitAgentResponse - * @description Response from agent initialization. - */ - InitAgentResponse: { - /** - * Controls - * @description Active protection controls for the agent - */ - controls?: components['schemas']['Control'][]; - /** - * Created - * @description True if agent was newly created, False if updated - */ - created: boolean; - /** - * Overwrite Applied - * @description True if overwrite mode changed registration data on an existing agent - * @default false - */ - overwrite_applied: boolean; - /** @description Detailed list of changes applied in overwrite mode */ - overwrite_changes?: components['schemas']['InitAgentOverwriteChanges']; - }; - JSONObject: { - [key: string]: components['schemas']['JSONValue']; - }; - /** @description Any JSON value */ - JSONValue: unknown; - /** - * ListAgentsResponse - * @description Response for listing agents. - */ - ListAgentsResponse: { - /** - * Agents - * @description List of agent summaries - */ - agents: components['schemas']['AgentSummary'][]; - /** @description Pagination metadata */ - pagination: components['schemas']['PaginationInfo']; - }; - /** - * ListControlsResponse - * @description Response for listing controls. - */ - ListControlsResponse: { - /** - * Controls - * @description List of control summaries - */ - controls: components['schemas']['ControlSummary'][]; - /** @description Pagination metadata */ - pagination: components['schemas']['PaginationInfo']; - }; - /** - * ListEvaluatorConfigsResponse - * @description Response for listing evaluator configs. - */ - ListEvaluatorConfigsResponse: { - /** - * Evaluator Configs - * @description List of evaluator configs - */ - evaluator_configs: components['schemas']['EvaluatorConfigItem'][]; - /** @description Pagination metadata */ - pagination: components['schemas']['PaginationInfo']; - }; - /** - * ListEvaluatorsResponse - * @description Response for listing agent's evaluator schemas. - */ - ListEvaluatorsResponse: { - /** Evaluators */ - evaluators: components['schemas']['EvaluatorSchemaItem'][]; - pagination: components['schemas']['PaginationInfo']; - }; - /** - * PaginationInfo - * @description Pagination metadata for cursor-based pagination. - */ - PaginationInfo: { - /** - * Has More - * @description Whether there are more pages available - */ - has_more: boolean; - /** - * Limit - * @description Number of items per page - */ - limit: number; - /** - * Next Cursor - * @description Cursor for fetching the next page (null if no more pages) - */ - next_cursor?: string | null; - /** - * Total - * @description Total number of items - */ - total: number; - }; - /** - * PatchAgentRequest - * @description Request to modify an agent (remove steps/evaluators). - */ - PatchAgentRequest: { - /** - * Remove Evaluators - * @description Evaluator names to remove from the agent - */ - remove_evaluators?: string[]; - /** - * Remove Steps - * @description Step identifiers to remove from the agent - */ - remove_steps?: components['schemas']['StepKey'][]; - }; - /** - * PatchAgentResponse - * @description Response from agent modification. - */ - PatchAgentResponse: { - /** - * Evaluators Removed - * @description Evaluator names that were removed - */ - evaluators_removed?: string[]; - /** - * Steps Removed - * @description Step identifiers that were removed - */ - steps_removed?: components['schemas']['StepKey'][]; - }; - /** - * PatchControlRequest - * @description Request to update control metadata (name, enabled status). - */ - PatchControlRequest: { - /** - * Enabled - * @description Enable or disable the control - */ - enabled?: boolean | null; - /** - * Name - * @description New name for the control - */ - name?: string | null; - }; - /** - * PatchControlResponse - * @description Response from control metadata update. - */ - PatchControlResponse: { - /** - * Enabled - * @description Current enabled status (if control has data configured) - */ - enabled?: boolean | null; - /** - * Name - * @description Current control name (may have changed) - */ - name: string; - /** - * Success - * @description Whether the update succeeded - */ - success: boolean; - }; - /** - * RemoveAgentControlResponse - * @description Response for removing a direct agent-control association. - */ - RemoveAgentControlResponse: { - /** - * Control Still Active - * @description True if the control remains active via policy association(s) - */ - control_still_active: boolean; - /** - * Removed Direct Association - * @description True if a direct agent-control link was removed - */ - removed_direct_association: boolean; - /** - * Success - * @description Whether the request succeeded - */ - success: boolean; - }; - /** - * SetControlDataRequest - * @description Request to update control configuration data. - */ - SetControlDataRequest: { - /** @description Control configuration data (replaces existing) */ - data: components['schemas']['ControlDefinition-Input']; - }; - /** SetControlDataResponse */ - SetControlDataResponse: { - /** - * Success - * @description Whether the control data was updated - */ - success: boolean; - }; - /** - * SetPolicyResponse - * @description Compatibility response for singular policy assignment endpoint. - */ - SetPolicyResponse: { - /** - * Old Policy Id - * @description Previously associated policy ID, if any - */ - old_policy_id?: number | null; - /** - * Success - * @description Whether the request succeeded - */ - success: boolean; - }; - /** - * StatsResponse - * @description Response model for agent-level aggregated statistics. - * - * Contains agent-level totals (with optional timeseries) and per-control breakdown. - * - * Attributes: - * agent_name: Agent identifier - * time_range: Time range used - * totals: Agent-level aggregate statistics (includes timeseries) - * controls: Per-control breakdown for discovery and detail - */ - StatsResponse: { - /** - * Agent Name - * @description Agent identifier - */ - agent_name: string; - /** - * Controls - * @description Per-control breakdown - */ - controls: components['schemas']['ControlStats'][]; - /** - * Time Range - * @description Time range used - */ - time_range: string; - /** @description Agent-level aggregate statistics */ - totals: components['schemas']['StatsTotals']; - }; - /** - * StatsTotals - * @description Agent-level aggregate statistics. - * - * Invariant: execution_count = match_count + non_match_count + error_count - * - * Matches have actions (allow, deny, steer, warn, log) tracked in action_counts. - * sum(action_counts.values()) == match_count - * - * Attributes: - * execution_count: Total executions across all controls - * match_count: Total matches across all controls (evaluator matched) - * non_match_count: Total non-matches across all controls (evaluator didn't match) - * error_count: Total errors across all controls (evaluation failed) - * action_counts: Breakdown of actions for matched executions - * timeseries: Time-series data points (only when include_timeseries=true) - */ - StatsTotals: { - /** - * Action Counts - * @description Action breakdown for matches: {allow, deny, steer, warn, log} - */ - action_counts?: { - [key: string]: number; - }; - /** - * Error Count - * @description Total errors - * @default 0 - */ - error_count: number; - /** - * Execution Count - * @description Total executions - */ - execution_count: number; - /** - * Match Count - * @description Total matches - * @default 0 - */ - match_count: number; - /** - * Non Match Count - * @description Total non-matches - * @default 0 - */ - non_match_count: number; - /** - * Timeseries - * @description Time-series data points (only when include_timeseries=true) - */ - timeseries?: components['schemas']['TimeseriesBucket'][] | null; - }; - /** - * SteeringContext - * @description Steering context for steer actions. - * - * This model provides an extensible structure for steering guidance. - * Future fields could include severity, categories, suggested_actions, etc. - * @example { - * "message": "This large transfer requires user verification. Request 2FA code from user, verify it, then retry the transaction with verified_2fa=True." - * } - * @example { - * "message": "Transfer exceeds daily limit. Steps: 1) Ask user for business justification, 2) Request manager approval with amount and justification, 3) If approved, retry with manager_approved=True and justification filled in." - * } - */ - SteeringContext: { - /** - * Message - * @description Guidance message explaining what needs to be corrected and how - */ - message: string; - }; - /** - * Step - * @description Runtime payload for an agent step invocation. - */ - Step: { - /** @description Optional context (conversation history, metadata, etc.) */ - context?: components['schemas']['JSONObject'] | null; - /** @description Input content for this step */ - input: components['schemas']['JSONValue']; - /** - * Name - * @description Step name (tool name or model/chain id) - */ - name: string; - /** @description Output content for this step (None for pre-checks) */ - output?: components['schemas']['JSONValue'] | null; - /** - * Type - * @description Step type (e.g., 'tool', 'llm') - */ - type: string; - }; - /** - * StepKey - * @description Identifies a registered step schema by type and name. - */ - StepKey: { - /** - * Name - * @description Registered step name - */ - name: string; - /** - * Type - * @description Step type - */ - type: string; - }; - /** - * StepSchema - * @description Schema for a registered agent step. - * @example { - * "description": "Search the internal knowledge base", - * "input_schema": { - * "query": { - * "description": "Search query", - * "type": "string" - * } - * }, - * "name": "search_knowledge_base", - * "output_schema": { - * "results": { - * "items": { - * "type": "object" - * }, - * "type": "array" - * } - * }, - * "type": "tool" - * } - * @example { - * "description": "Customer support response generation", - * "input_schema": { - * "messages": { - * "items": { - * "type": "object" - * }, - * "type": "array" - * } - * }, - * "name": "support-answer", - * "output_schema": { - * "text": { - * "type": "string" - * } - * }, - * "type": "llm" - * } - */ - StepSchema: { - /** - * Description - * @description Optional description of the step - */ - description?: string | null; - /** - * Input Schema - * @description JSON schema describing step input - */ - input_schema?: { - [key: string]: unknown; - } | null; - /** - * Metadata - * @description Additional metadata for the step - */ - metadata?: { - [key: string]: unknown; - } | null; - /** - * Name - * @description Unique name for the step - */ - name: string; - /** - * Output Schema - * @description JSON schema describing step output - */ - output_schema?: { - [key: string]: unknown; - } | null; - /** - * Type - * @description Step type for this schema (e.g., 'tool', 'llm') - */ - type: string; - }; - /** - * TimeseriesBucket - * @description Single data point in a time-series. - * - * Represents aggregated metrics for a single time bucket. - * - * Attributes: - * timestamp: Start time of the bucket (UTC, always timezone-aware) - * execution_count: Total executions in this bucket - * match_count: Number of matches in this bucket - * non_match_count: Number of non-matches in this bucket - * error_count: Number of errors in this bucket - * action_counts: Breakdown of actions for matched executions - * avg_confidence: Average confidence score (None if no executions) - * avg_duration_ms: Average execution duration in milliseconds (None if no data) - */ - TimeseriesBucket: { - /** - * Action Counts - * @description Action breakdown: {allow, deny, steer, warn, log} - */ - action_counts?: { - [key: string]: number; - }; - /** - * Avg Confidence - * @description Average confidence score - */ - avg_confidence?: number | null; - /** - * Avg Duration Ms - * @description Average duration (ms) - */ - avg_duration_ms?: number | null; - /** - * Error Count - * @description Errors in bucket - */ - error_count: number; - /** - * Execution Count - * @description Total executions in bucket - */ - execution_count: number; - /** - * Match Count - * @description Matches in bucket - */ - match_count: number; - /** - * Non Match Count - * @description Non-matches in bucket - */ - non_match_count: number; - /** - * Timestamp - * Format: date-time - * @description Start time of the bucket (UTC) - */ - timestamp: string; - }; - /** - * UpdateEvaluatorConfigRequest - * @description Request to replace an evaluator config template. - */ - UpdateEvaluatorConfigRequest: { - /** - * Config - * @description Evaluator-specific configuration - */ - config: { - [key: string]: unknown; - }; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Evaluator - * @description Evaluator name (built-in or custom) - */ - evaluator: string; - /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) - */ - name: string; - }; - /** - * ValidateControlDataRequest - * @description Request to validate control configuration data without saving. - */ - ValidateControlDataRequest: { - /** @description Control configuration data to validate */ - data: components['schemas']['ControlDefinition-Input']; - }; - /** ValidateControlDataResponse */ - ValidateControlDataResponse: { - /** - * Success - * @description Whether the control data is valid - */ - success: boolean; - }; - /** ValidationError */ - ValidationError: { - /** Context */ - ctx?: Record; - /** Input */ - input?: unknown; - /** Location */ - loc: (string | number)[]; - /** Message */ - msg: string; - /** Error Type */ - type: string; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + schemas: { + /** + * Agent + * @description Agent metadata for registration and tracking. + * + * An agent represents an AI system that can be protected and monitored. + * Each agent has a unique immutable name and can have multiple steps registered with it. + * @example { + * "agent_description": "Handles customer inquiries and support tickets", + * "agent_metadata": { + * "environment": "production", + * "team": "support" + * }, + * "agent_name": "customer-service-bot", + * "agent_version": "1.0.0" + * } + */ + Agent: { + /** + * Agent Created At + * @description ISO 8601 timestamp when agent was created + */ + agent_created_at?: string | null; + /** + * Agent Description + * @description Optional description of the agent's purpose + */ + agent_description?: string | null; + /** + * Agent Metadata + * @description Free-form metadata dictionary for custom properties + */ + agent_metadata?: { + [key: string]: unknown; + } | null; + /** + * Agent Name + * @description Unique immutable identifier for the agent + */ + agent_name: string; + /** + * Agent Updated At + * @description ISO 8601 timestamp when agent was last updated + */ + agent_updated_at?: string | null; + /** + * Agent Version + * @description Semantic version string (e.g. '1.0.0') + */ + agent_version?: string | null; + }; + /** AgentControlsResponse */ + AgentControlsResponse: { + /** + * Controls + * @description List of active controls associated with the agent + */ + controls: components["schemas"]["Control"][]; + }; + /** + * AgentRef + * @description Reference to an agent (for listing which agents use a control). + */ + AgentRef: { + /** + * Agent Name + * @description Agent name + */ + agent_name: string; + }; + /** + * AgentSummary + * @description Summary of an agent for list responses. + */ + AgentSummary: { + /** + * Active Controls Count + * @description Number of active controls for this agent + * @default 0 + */ + active_controls_count: number; + /** + * Agent Name + * @description Unique identifier of the agent + */ + agent_name: string; + /** + * Created At + * @description ISO 8601 timestamp when agent was created + */ + created_at?: string | null; + /** + * Evaluator Count + * @description Number of evaluators registered with the agent + * @default 0 + */ + evaluator_count: number; + /** + * Policy Ids + * @description IDs of policies associated with the agent + */ + policy_ids?: number[]; + /** + * Step Count + * @description Number of steps registered with the agent + * @default 0 + */ + step_count: number; + }; + /** AssocResponse */ + AssocResponse: { + /** + * Success + * @description Whether the association change succeeded + */ + success: boolean; + }; + /** + * AuthMode + * @description Authentication mode advertised to the UI. + * @enum {string} + */ + AuthMode: "none" | "api-key"; + /** + * BatchEventsRequest + * @description Request model for batch event ingestion. + * + * SDKs batch events and send them to the server periodically. + * This reduces HTTP overhead significantly (100x reduction). + * + * Attributes: + * events: List of control execution events to ingest + * @example { + * "events": [ + * { + * "action": "deny", + * "agent_name": "my-agent", + * "applies_to": "llm_call", + * "check_stage": "pre", + * "confidence": 0.95, + * "control_id": 123, + * "control_name": "sql-injection-check", + * "matched": true, + * "span_id": "00f067aa0ba902b7", + * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" + * } + * ] + * } + */ + BatchEventsRequest: { + /** + * Events + * @description List of events to ingest + */ + events: components["schemas"]["ControlExecutionEvent"][]; + }; + /** + * BatchEventsResponse + * @description Response model for batch event ingestion. + * + * Attributes: + * received: Number of events received + * enqueued: Number of events successfully enqueued + * dropped: Number of events dropped (queue full) + * status: Overall status ('queued', 'partial', 'failed') + */ + BatchEventsResponse: { + /** + * Dropped + * @description Number of events dropped + */ + dropped: number; + /** + * Enqueued + * @description Number of events enqueued + */ + enqueued: number; + /** + * Received + * @description Number of events received + */ + received: number; + /** + * Status + * @description Overall ingestion status + * @enum {string} + */ + status: "queued" | "partial" | "failed"; + }; + /** + * ConditionNode + * @description Recursive boolean condition tree for control evaluation. + * @example { + * "evaluator": { + * "config": { + * "pattern": "\\d{3}-\\d{2}-\\d{4}" + * }, + * "name": "regex" + * }, + * "selector": { + * "path": "output" + * } + * } + * @example { + * "and": [ + * { + * "evaluator": { + * "config": { + * "values": [ + * "high", + * "critical" + * ] + * }, + * "name": "list" + * }, + * "selector": { + * "path": "context.risk_level" + * } + * }, + * { + * "not": { + * "evaluator": { + * "config": { + * "values": [ + * "admin", + * "security" + * ] + * }, + * "name": "list" + * }, + * "selector": { + * "path": "context.user_role" + * } + * } + * } + * ] + * } + */ + "ConditionNode-Input": { + /** + * And + * @description Logical AND over child conditions. + */ + and?: components["schemas"]["ConditionNode-Input"][] | null; + /** @description Leaf evaluator. Must be provided together with selector. */ + evaluator?: components["schemas"]["EvaluatorSpec"] | null; + /** @description Logical NOT over a single child condition. */ + not?: components["schemas"]["ConditionNode-Input"] | null; + /** + * Or + * @description Logical OR over child conditions. + */ + or?: components["schemas"]["ConditionNode-Input"][] | null; + /** @description Leaf selector. Must be provided together with evaluator. */ + selector?: components["schemas"]["ControlSelector"] | null; + }; + /** + * ConditionNode + * @description Recursive boolean condition tree for control evaluation. + * @example { + * "evaluator": { + * "config": { + * "pattern": "\\d{3}-\\d{2}-\\d{4}" + * }, + * "name": "regex" + * }, + * "selector": { + * "path": "output" + * } + * } + * @example { + * "and": [ + * { + * "evaluator": { + * "config": { + * "values": [ + * "high", + * "critical" + * ] + * }, + * "name": "list" + * }, + * "selector": { + * "path": "context.risk_level" + * } + * }, + * { + * "not": { + * "evaluator": { + * "config": { + * "values": [ + * "admin", + * "security" + * ] + * }, + * "name": "list" + * }, + * "selector": { + * "path": "context.user_role" + * } + * } + * } + * ] + * } + */ + "ConditionNode-Output": { + /** + * And + * @description Logical AND over child conditions. + */ + and?: components["schemas"]["ConditionNode-Output"][] | null; + /** @description Leaf evaluator. Must be provided together with selector. */ + evaluator?: components["schemas"]["EvaluatorSpec"] | null; + /** @description Logical NOT over a single child condition. */ + not?: components["schemas"]["ConditionNode-Output"] | null; + /** + * Or + * @description Logical OR over child conditions. + */ + or?: components["schemas"]["ConditionNode-Output"][] | null; + /** @description Leaf selector. Must be provided together with evaluator. */ + selector?: components["schemas"]["ControlSelector"] | null; + }; + /** + * ConfigResponse + * @description Configuration surface exposed to the UI. + */ + ConfigResponse: { + auth_mode: components["schemas"]["AuthMode"]; + /** + * Has Active Session + * @default false + */ + has_active_session: boolean; + /** Requires Api Key */ + requires_api_key: boolean; + }; + /** + * ConflictMode + * @description Conflict handling mode for initAgent registration updates. + * + * STRICT preserves compatibility checks and raises conflicts on incompatible changes. + * OVERWRITE applies latest-init-wins replacement for steps and evaluators. + * @enum {string} + */ + ConflictMode: "strict" | "overwrite"; + /** + * Control + * @description A control with identity and configuration. + * + * Note: Only fully-configured controls (with valid ControlDefinition) + * are returned from API endpoints. Unconfigured controls are filtered out. + */ + Control: { + control: components["schemas"]["ControlDefinition-Output"]; + /** Id */ + id: number; + /** Name */ + name: string; + }; + /** + * ControlAction + * @description What to do when control matches. + */ + ControlAction: { + /** + * Decision + * @description Action to take when control is triggered + * @enum {string} + */ + decision: "allow" | "deny" | "steer" | "warn" | "log"; + /** @description Steering context object for steer actions. Strongly recommended when decision='steer' to provide correction suggestions. If not provided, the evaluator result message will be used as fallback. */ + steering_context?: components["schemas"]["SteeringContext"] | null; + }; + /** + * ControlDefinition + * @description A control definition to evaluate agent interactions. + * + * This model contains only the logic and configuration. + * Identity fields (id, name) are managed by the database. + * @example { + * "action": { + * "decision": "deny" + * }, + * "condition": { + * "evaluator": { + * "config": { + * "pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b" + * }, + * "name": "regex" + * }, + * "selector": { + * "path": "output" + * } + * }, + * "description": "Block outputs containing US Social Security Numbers", + * "enabled": true, + * "execution": "server", + * "scope": { + * "stages": [ + * "post" + * ], + * "step_types": [ + * "llm" + * ] + * }, + * "tags": [ + * "pii", + * "compliance" + * ] + * } + */ + "ControlDefinition-Input": { + /** @description What action to take when control matches */ + action: components["schemas"]["ControlAction"]; + /** @description Recursive boolean condition tree. Leaf nodes contain selector + evaluator; composite nodes contain and/or/not. */ + condition: components["schemas"]["ConditionNode-Input"]; + /** + * Description + * @description Detailed description of the control + */ + description?: string | null; + /** + * Enabled + * @description Whether this control is active + * @default true + */ + enabled: boolean; + /** + * Execution + * @description Where this control executes + * @enum {string} + */ + execution: "server" | "sdk"; + /** @description Which steps and stages this control applies to */ + scope?: components["schemas"]["ControlScope"]; + /** + * Tags + * @description Tags for categorization + */ + tags?: string[]; + }; + /** + * ControlDefinition + * @description A control definition to evaluate agent interactions. + * + * This model contains only the logic and configuration. + * Identity fields (id, name) are managed by the database. + * @example { + * "action": { + * "decision": "deny" + * }, + * "condition": { + * "evaluator": { + * "config": { + * "pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b" + * }, + * "name": "regex" + * }, + * "selector": { + * "path": "output" + * } + * }, + * "description": "Block outputs containing US Social Security Numbers", + * "enabled": true, + * "execution": "server", + * "scope": { + * "stages": [ + * "post" + * ], + * "step_types": [ + * "llm" + * ] + * }, + * "tags": [ + * "pii", + * "compliance" + * ] + * } + */ + "ControlDefinition-Output": { + /** @description What action to take when control matches */ + action: components["schemas"]["ControlAction"]; + /** @description Recursive boolean condition tree. Leaf nodes contain selector + evaluator; composite nodes contain and/or/not. */ + condition: components["schemas"]["ConditionNode-Output"]; + /** + * Description + * @description Detailed description of the control + */ + description?: string | null; + /** + * Enabled + * @description Whether this control is active + * @default true + */ + enabled: boolean; + /** + * Execution + * @description Where this control executes + * @enum {string} + */ + execution: "server" | "sdk"; + /** @description Which steps and stages this control applies to */ + scope?: components["schemas"]["ControlScope"]; + /** + * Tags + * @description Tags for categorization + */ + tags?: string[]; + }; + /** + * ControlExecutionEvent + * @description Represents a single control execution event. + * + * This is the core observability data model, capturing: + * - Identity: control_execution_id, trace_id, span_id (OpenTelemetry-compatible) + * - Context: agent, control, check stage, applies to + * - Result: action taken, whether matched, confidence score + * - Timing: when it happened, how long it took + * - Optional details: evaluator name, selector path, errors, metadata + * + * Attributes: + * control_execution_id: Unique ID for this specific control execution + * trace_id: OpenTelemetry-compatible trace ID (128-bit hex, 32 chars) + * span_id: OpenTelemetry-compatible span ID (64-bit hex, 16 chars) + * agent_name: Identifier of the agent that executed the control + * control_id: Database ID of the control + * control_name: Name of the control (denormalized for queries) + * check_stage: "pre" (before execution) or "post" (after execution) + * applies_to: "llm_call" or "tool_call" + * action: The action taken (allow, deny, warn, log) + * matched: Whether the control evaluator matched + * confidence: Confidence score from the evaluator (0.0-1.0) + * timestamp: When the control was executed (UTC) + * execution_duration_ms: How long the control evaluation took + * evaluator_name: Name of the evaluator used + * selector_path: The selector path used to extract data + * error_message: Error message if evaluation failed + * metadata: Additional metadata for extensibility + * @example { + * "action": "deny", + * "agent_name": "my-agent", + * "applies_to": "llm_call", + * "check_stage": "pre", + * "confidence": 0.95, + * "control_execution_id": "550e8400-e29b-41d4-a716-446655440000", + * "control_id": 123, + * "control_name": "sql-injection-check", + * "evaluator_name": "regex", + * "execution_duration_ms": 15.3, + * "matched": true, + * "selector_path": "input", + * "span_id": "00f067aa0ba902b7", + * "timestamp": "2025-01-09T10:30:00Z", + * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" + * } + */ + ControlExecutionEvent: { + /** + * Action + * @description Action taken by the control + * @enum {string} + */ + action: "allow" | "deny" | "steer" | "warn" | "log"; + /** + * Agent Name + * @description Identifier of the agent + */ + agent_name: string; + /** + * Applies To + * @description Type of call: 'llm_call' or 'tool_call' + * @enum {string} + */ + applies_to: "llm_call" | "tool_call"; + /** + * Check Stage + * @description Check stage: 'pre' or 'post' + * @enum {string} + */ + check_stage: "pre" | "post"; + /** + * Confidence + * @description Confidence score (0.0 to 1.0) + */ + confidence: number; + /** + * Control Execution Id + * @description Unique ID for this control execution + */ + control_execution_id?: string; + /** + * Control Id + * @description Database ID of the control + */ + control_id: number; + /** + * Control Name + * @description Name of the control (denormalized) + */ + control_name: string; + /** + * Error Message + * @description Error message if evaluation failed + */ + error_message?: string | null; + /** + * Evaluator Name + * @description Name of the evaluator used + */ + evaluator_name?: string | null; + /** + * Execution Duration Ms + * @description Execution duration in milliseconds + */ + execution_duration_ms?: number | null; + /** + * Matched + * @description Whether the evaluator matched (True) or not (False) + */ + matched: boolean; + /** + * Metadata + * @description Additional metadata + */ + metadata?: { + [key: string]: unknown; + }; + /** + * Selector Path + * @description Selector path used to extract data + */ + selector_path?: string | null; + /** + * Span Id + * @description Span ID for distributed tracing (SDK generates OTEL-compatible 16-char hex) + */ + span_id: string; + /** + * Timestamp + * Format: date-time + * @description When the control was executed (UTC) + */ + timestamp?: string; + /** + * Trace Id + * @description Trace ID for distributed tracing (SDK generates OTEL-compatible 32-char hex) + */ + trace_id: string; + }; + /** + * ControlMatch + * @description Represents a control evaluation result (match, non-match, or error). + */ + ControlMatch: { + /** + * Action + * @description Action configured for this control + * @enum {string} + */ + action: "allow" | "deny" | "steer" | "warn" | "log"; + /** + * Control Execution Id + * @description Unique ID for this control execution (generated by engine) + */ + control_execution_id?: string; + /** + * Control Id + * @description Database ID of the control + */ + control_id: number; + /** + * Control Name + * @description Name of the control + */ + control_name: string; + /** @description Evaluator result (confidence, message, metadata) */ + result: components["schemas"]["EvaluatorResult"]; + /** @description Steering context for steer actions if configured */ + steering_context?: components["schemas"]["SteeringContext"] | null; + }; + /** + * ControlScope + * @description Defines when a control applies to a Step. + * @example { + * "stages": [ + * "pre" + * ], + * "step_types": [ + * "tool" + * ] + * } + * @example { + * "step_names": [ + * "search_db", + * "fetch_user" + * ] + * } + * @example { + * "step_name_regex": "^db_.*" + * } + * @example { + * "stages": [ + * "post" + * ], + * "step_types": [ + * "llm" + * ] + * } + */ + ControlScope: { + /** + * Stages + * @description Evaluation stages this control applies to + */ + stages?: ("pre" | "post")[] | null; + /** + * Step Name Regex + * @description RE2 pattern matched with search() against step name + */ + step_name_regex?: string | null; + /** + * Step Names + * @description Exact step names this control applies to + */ + step_names?: string[] | null; + /** + * Step Types + * @description Step types this control applies to (omit to apply to all types). Built-in types are 'tool' and 'llm'. + */ + step_types?: string[] | null; + }; + /** + * ControlSelector + * @description Selects data from a Step payload. + * + * - path: which slice of the Step to feed into the evaluator. Optional, defaults to "*" + * meaning the entire Step object. + * @example { + * "path": "output" + * } + * @example { + * "path": "context.user_id" + * } + * @example { + * "path": "input" + * } + * @example { + * "path": "*" + * } + * @example { + * "path": "name" + * } + * @example { + * "path": "output" + * } + */ + ControlSelector: { + /** + * Path + * @description Path to data using dot notation. Examples: 'input', 'output', 'context.user_id', 'name', 'type', '*' + * @default * + */ + path: string | null; + }; + /** + * ControlStats + * @description Aggregated statistics for a single control. + * + * Attributes: + * control_id: Database ID of the control + * control_name: Name of the control + * execution_count: Total number of executions + * match_count: Number of times the control matched + * non_match_count: Number of times the control did not match + * allow_count: Number of allow actions + * deny_count: Number of deny actions + * steer_count: Number of steer actions + * warn_count: Number of warn actions + * log_count: Number of log actions + * error_count: Number of errors during evaluation + * avg_confidence: Average confidence score + * avg_duration_ms: Average execution duration in milliseconds + */ + ControlStats: { + /** + * Allow Count + * @description Allow actions + */ + allow_count: number; + /** + * Avg Confidence + * @description Average confidence + */ + avg_confidence: number; + /** + * Avg Duration Ms + * @description Average duration (ms) + */ + avg_duration_ms?: number | null; + /** + * Control Id + * @description Control ID + */ + control_id: number; + /** + * Control Name + * @description Control name + */ + control_name: string; + /** + * Deny Count + * @description Deny actions + */ + deny_count: number; + /** + * Error Count + * @description Evaluation errors + */ + error_count: number; + /** + * Execution Count + * @description Total executions + */ + execution_count: number; + /** + * Log Count + * @description Log actions + */ + log_count: number; + /** + * Match Count + * @description Total matches + */ + match_count: number; + /** + * Non Match Count + * @description Total non-matches + */ + non_match_count: number; + /** + * Steer Count + * @description Steer actions + */ + steer_count: number; + /** + * Warn Count + * @description Warn actions + */ + warn_count: number; + }; + /** + * ControlStatsResponse + * @description Response model for control-level statistics. + * + * Contains stats for a single control (with optional timeseries). + * + * Attributes: + * agent_name: Agent identifier + * time_range: Time range used + * control_id: Control ID + * control_name: Control name + * stats: Control statistics (includes timeseries when requested) + */ + ControlStatsResponse: { + /** + * Agent Name + * @description Agent identifier + */ + agent_name: string; + /** + * Control Id + * @description Control ID + */ + control_id: number; + /** + * Control Name + * @description Control name + */ + control_name: string; + /** @description Control statistics */ + stats: components["schemas"]["StatsTotals"]; + /** + * Time Range + * @description Time range used + */ + time_range: string; + }; + /** + * ControlSummary + * @description Summary of a control for list responses. + */ + ControlSummary: { + /** + * Description + * @description Control description + */ + description?: string | null; + /** + * Enabled + * @description Whether control is enabled + * @default true + */ + enabled: boolean; + /** + * Execution + * @description 'server' or 'sdk' + */ + execution?: string | null; + /** + * Id + * @description Control ID + */ + id: number; + /** + * Name + * @description Control name + */ + name: string; + /** + * Stages + * @description Evaluation stages in scope + */ + stages?: string[] | null; + /** + * Step Types + * @description Step types in scope + */ + step_types?: string[] | null; + /** + * Tags + * @description Control tags + */ + tags?: string[]; + /** @description Agent using this control */ + used_by_agent?: components["schemas"]["AgentRef"] | null; + /** + * Used By Agents Count + * @description Number of unique agents using this control + * @default 0 + */ + used_by_agents_count: number; + }; + /** CreateControlRequest */ + CreateControlRequest: { + /** + * Name + * @description Unique control name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** CreateControlResponse */ + CreateControlResponse: { + /** + * Control Id + * @description Identifier of the created control + */ + control_id: number; + }; + /** + * CreateEvaluatorConfigRequest + * @description Request to create an evaluator config template. + */ + CreateEvaluatorConfigRequest: { + /** + * Config + * @description Evaluator-specific configuration + */ + config: { + [key: string]: unknown; + }; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Evaluator + * @description Evaluator name (built-in or custom) + */ + evaluator: string; + /** + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** CreatePolicyRequest */ + CreatePolicyRequest: { + /** + * Name + * @description Unique policy name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** CreatePolicyResponse */ + CreatePolicyResponse: { + /** + * Policy Id + * @description Identifier of the created policy + */ + policy_id: number; + }; + /** + * DeleteControlResponse + * @description Response for deleting a control. + */ + DeleteControlResponse: { + /** + * Dissociated From + * @description Deprecated: policy IDs the control was removed from before deletion + */ + dissociated_from?: number[]; + /** + * Dissociated From Agents + * @description Agent names the control was removed from before deletion + */ + dissociated_from_agents?: string[]; + /** + * Dissociated From Policies + * @description Policy IDs the control was removed from before deletion + */ + dissociated_from_policies?: number[]; + /** + * Success + * @description Whether the control was deleted + */ + success: boolean; + }; + /** + * DeleteEvaluatorConfigResponse + * @description Response for deleting an evaluator config. + */ + DeleteEvaluatorConfigResponse: { + /** + * Success + * @description Whether the evaluator config was deleted + */ + success: boolean; + }; + /** + * DeletePolicyResponse + * @description Compatibility response for singular policy deletion endpoint. + */ + DeletePolicyResponse: { + /** + * Success + * @description Whether the request succeeded + */ + success: boolean; + }; + /** + * EvaluationRequest + * @description Request model for evaluation analysis. + * + * Used to analyze agent interactions for safety violations, + * policy compliance, and control rules. + * + * Attributes: + * agent_name: Unique identifier of the agent making the request + * step: Step payload for evaluation + * stage: 'pre' (before execution) or 'post' (after execution) + * @example { + * "agent_name": "customer-service-bot", + * "stage": "pre", + * "step": { + * "context": { + * "session_id": "abc123", + * "user_id": "user123" + * }, + * "input": "What is the customer's credit card number?", + * "name": "support-answer", + * "type": "llm" + * } + * } + * @example { + * "agent_name": "customer-service-bot", + * "stage": "post", + * "step": { + * "context": { + * "session_id": "abc123", + * "user_id": "user123" + * }, + * "input": "What is the customer's credit card number?", + * "name": "support-answer", + * "output": "I cannot share sensitive payment information.", + * "type": "llm" + * } + * } + * @example { + * "agent_name": "customer-service-bot", + * "stage": "pre", + * "step": { + * "context": { + * "user_id": "user123" + * }, + * "input": { + * "query": "SELECT * FROM users" + * }, + * "name": "search_database", + * "type": "tool" + * } + * } + * @example { + * "agent_name": "customer-service-bot", + * "stage": "post", + * "step": { + * "context": { + * "user_id": "user123" + * }, + * "input": { + * "query": "SELECT * FROM users" + * }, + * "name": "search_database", + * "output": { + * "results": [] + * }, + * "type": "tool" + * } + * } + */ + EvaluationRequest: { + /** + * Agent Name + * @description Identifier of the agent making the evaluation request + */ + agent_name: string; + /** + * Stage + * @description Evaluation stage: 'pre' or 'post' + * @enum {string} + */ + stage: "pre" | "post"; + /** @description Agent step payload to evaluate */ + step: components["schemas"]["Step"]; + }; + /** + * EvaluationResponse + * @description Response model from evaluation analysis (server-side). + * + * This is what the server returns. The SDK may transform this + * into an EvaluationResult for client convenience. + * + * Attributes: + * is_safe: Whether the content is considered safe + * confidence: Confidence score between 0.0 and 1.0 + * reason: Optional explanation for the decision + * matches: List of controls that matched/triggered (if any) + * errors: List of controls that failed during evaluation (if any) + * non_matches: List of controls that were evaluated but did not match (if any) + */ + EvaluationResponse: { + /** + * Confidence + * @description Confidence score (0.0 to 1.0) + */ + confidence: number; + /** + * Errors + * @description List of controls that failed during evaluation (if any) + */ + errors?: components["schemas"]["ControlMatch"][] | null; + /** + * Is Safe + * @description Whether content is safe + */ + is_safe: boolean; + /** + * Matches + * @description List of controls that matched/triggered (if any) + */ + matches?: components["schemas"]["ControlMatch"][] | null; + /** + * Non Matches + * @description List of controls that were evaluated but did not match (if any) + */ + non_matches?: components["schemas"]["ControlMatch"][] | null; + /** + * Reason + * @description Explanation for the decision + */ + reason?: string | null; + }; + /** + * EvaluatorConfigItem + * @description Evaluator config template stored in the server. + */ + EvaluatorConfigItem: { + /** + * Config + * @description Evaluator-specific configuration + */ + config: { + [key: string]: unknown; + }; + /** + * Created At + * @description ISO 8601 created timestamp + */ + created_at?: string | null; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Evaluator + * @description Evaluator name (built-in or custom) + */ + evaluator: string; + /** + * Id + * @description Evaluator config ID + */ + id: number; + /** + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + */ + name: string; + /** + * Updated At + * @description ISO 8601 updated timestamp + */ + updated_at?: string | null; + }; + /** + * EvaluatorInfo + * @description Information about a registered evaluator. + */ + EvaluatorInfo: { + /** + * Config Schema + * @description JSON Schema for config + */ + config_schema: { + [key: string]: unknown; + }; + /** + * Description + * @description Evaluator description + */ + description: string; + /** + * Name + * @description Evaluator name + */ + name: string; + /** + * Requires Api Key + * @description Whether evaluator requires API key + */ + requires_api_key: boolean; + /** + * Timeout Ms + * @description Default timeout in milliseconds + */ + timeout_ms: number; + /** + * Version + * @description Evaluator version + */ + version: string; + }; + /** + * EvaluatorResult + * @description Result from a control evaluator. + * + * The `error` field indicates evaluator failures, NOT validation failures: + * - Set `error` for: evaluator crashes, timeouts, missing dependencies, external service errors + * - Do NOT set `error` for: invalid input, syntax errors, schema violations, constraint failures + * + * When `error` is set, `matched` must be False (fail-open on evaluator errors). + * When `error` is None, `matched` reflects the actual validation result. + * + * This distinction allows: + * - Clients to distinguish "data violated rules" from "evaluator is broken" + * - Observability systems to monitor evaluator health separately from validation outcomes + */ + EvaluatorResult: { + /** + * Confidence + * @description Confidence in the evaluation + */ + confidence: number; + /** + * Error + * @description Error message if evaluation failed internally. When set, matched=False is due to error, not actual evaluation. + */ + error?: string | null; + /** + * Matched + * @description Whether the pattern matched + */ + matched: boolean; + /** + * Message + * @description Explanation of the result + */ + message?: string | null; + /** + * Metadata + * @description Additional result metadata + */ + metadata?: { + [key: string]: unknown; + } | null; + }; + /** + * EvaluatorSchema + * @description Schema for a custom evaluator registered with an agent. + * + * Custom evaluators are Evaluator classes deployed with the engine. + * This schema is registered via initAgent for validation and UI purposes. + */ + EvaluatorSchema: { + /** + * Config Schema + * @description JSON Schema for evaluator config validation + */ + config_schema?: { + [key: string]: unknown; + }; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Name + * @description Unique evaluator name + */ + name: string; + }; + /** + * EvaluatorSchemaItem + * @description Evaluator schema summary for list response. + */ + EvaluatorSchemaItem: { + /** Config Schema */ + config_schema: { + [key: string]: unknown; + }; + /** Description */ + description: string | null; + /** Name */ + name: string; + }; + /** + * EvaluatorSpec + * @description Evaluator specification. See GET /evaluators for available evaluators and schemas. + * + * Evaluator reference formats: + * - Built-in: "regex", "list", "json", "sql" + * - External: "galileo.luna2" (requires agent-control-evaluators[galileo]) + * - Agent-scoped: "my-agent:my-evaluator" (validated in endpoint, not here) + */ + EvaluatorSpec: { + /** + * Config + * @description Evaluator-specific configuration + * @example { + * "pattern": "\\d{3}-\\d{2}-\\d{4}" + * } + * @example { + * "logic": "any", + * "values": [ + * "admin" + * ] + * } + */ + config: { + [key: string]: unknown; + }; + /** + * Name + * @description Evaluator name or agent-scoped reference (agent:evaluator) + * @example regex + * @example list + * @example my-agent:pii-detector + */ + name: string; + }; + /** + * EventQueryRequest + * @description Request model for querying raw events. + * + * Supports filtering by various criteria and pagination. + * + * Attributes: + * trace_id: Filter by trace ID (get all events for a request) + * span_id: Filter by span ID (get all events for a function call) + * control_execution_id: Filter by specific event ID + * agent_name: Filter by agent identifier + * control_ids: Filter by control IDs + * actions: Filter by actions (allow, deny, steer, warn, log) + * matched: Filter by matched status + * check_stages: Filter by check stages (pre, post) + * applies_to: Filter by call type (llm_call, tool_call) + * start_time: Filter events after this time + * end_time: Filter events before this time + * limit: Maximum number of events to return + * offset: Offset for pagination + * @example { + * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" + * } + * @example { + * "actions": [ + * "deny", + * "warn" + * ], + * "agent_name": "my-agent", + * "limit": 50, + * "start_time": "2025-01-09T00:00:00Z" + * } + */ + EventQueryRequest: { + /** + * Actions + * @description Filter by actions + */ + actions?: ("allow" | "deny" | "steer" | "warn" | "log")[] | null; + /** + * Agent Name + * @description Filter by agent identifier + */ + agent_name?: string | null; + /** + * Applies To + * @description Filter by call types + */ + applies_to?: ("llm_call" | "tool_call")[] | null; + /** + * Check Stages + * @description Filter by check stages + */ + check_stages?: ("pre" | "post")[] | null; + /** + * Control Execution Id + * @description Filter by specific event ID + */ + control_execution_id?: string | null; + /** + * Control Ids + * @description Filter by control IDs + */ + control_ids?: number[] | null; + /** + * End Time + * @description Filter events before this time + */ + end_time?: string | null; + /** + * Limit + * @description Maximum events + * @default 100 + */ + limit: number; + /** + * Matched + * @description Filter by matched status + */ + matched?: boolean | null; + /** + * Offset + * @description Pagination offset + * @default 0 + */ + offset: number; + /** + * Span Id + * @description Filter by span ID (all events for a function) + */ + span_id?: string | null; + /** + * Start Time + * @description Filter events after this time + */ + start_time?: string | null; + /** + * Trace Id + * @description Filter by trace ID (all events for a request) + */ + trace_id?: string | null; + }; + /** + * EventQueryResponse + * @description Response model for event queries. + * + * Attributes: + * events: List of matching events + * total: Total number of matching events (for pagination) + * limit: Limit used in query + * offset: Offset used in query + */ + EventQueryResponse: { + /** + * Events + * @description Matching events + */ + events: components["schemas"]["ControlExecutionEvent"][]; + /** + * Limit + * @description Limit used in query + */ + limit: number; + /** + * Offset + * @description Offset used in query + */ + offset: number; + /** + * Total + * @description Total matching events + */ + total: number; + }; + /** GetAgentPoliciesResponse */ + GetAgentPoliciesResponse: { + /** + * Policy Ids + * @description IDs of policies associated with the agent + */ + policy_ids?: number[]; + }; + /** + * GetAgentResponse + * @description Response containing agent details and registered steps. + */ + GetAgentResponse: { + /** @description Agent metadata */ + agent: components["schemas"]["Agent"]; + /** + * Evaluators + * @description Custom evaluators registered with this agent + */ + evaluators?: components["schemas"]["EvaluatorSchema"][]; + /** + * Steps + * @description Steps registered with this agent + */ + steps: components["schemas"]["StepSchema"][]; + }; + /** GetControlDataResponse */ + GetControlDataResponse: { + /** @description Control data payload */ + data: components["schemas"]["ControlDefinition-Output"]; + }; + /** + * GetControlResponse + * @description Response containing control details. + */ + GetControlResponse: { + /** @description Control configuration data (None if not yet configured) */ + data?: components["schemas"]["ControlDefinition-Output"] | null; + /** + * Id + * @description Control ID + */ + id: number; + /** + * Name + * @description Control name + */ + name: string; + }; + /** + * GetPolicyControlsResponse + * @description Response containing control IDs associated with a policy. + */ + GetPolicyControlsResponse: { + /** + * Control Ids + * @description List of control IDs associated with the policy + */ + control_ids: number[]; + }; + /** + * GetPolicyResponse + * @description Compatibility response for singular policy retrieval endpoint. + */ + GetPolicyResponse: { + /** + * Policy Id + * @description Associated policy ID + */ + policy_id: number; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + /** + * HealthResponse + * @description Health check response model. + * + * Attributes: + * status: Current health status (e.g., "healthy", "degraded", "unhealthy") + * version: Application version + */ + HealthResponse: { + /** Status */ + status: string; + /** Version */ + version: string; + }; + /** + * InitAgentEvaluatorRemoval + * @description Details for an evaluator removed during overwrite mode. + */ + InitAgentEvaluatorRemoval: { + /** + * Control Ids + * @description IDs of active controls referencing this evaluator + */ + control_ids?: number[]; + /** + * Control Names + * @description Names of active controls referencing this evaluator + */ + control_names?: string[]; + /** + * Name + * @description Evaluator name removed by overwrite + */ + name: string; + /** + * Referenced By Active Controls + * @description Whether this evaluator is still referenced by active controls + * @default false + */ + referenced_by_active_controls: boolean; + }; + /** + * InitAgentOverwriteChanges + * @description Detailed change summary for initAgent overwrite mode. + */ + InitAgentOverwriteChanges: { + /** + * Evaluator Removals + * @description Per-evaluator removal details, including active control references + */ + evaluator_removals?: components["schemas"]["InitAgentEvaluatorRemoval"][]; + /** + * Evaluators Added + * @description Evaluator names added by overwrite + */ + evaluators_added?: string[]; + /** + * Evaluators Removed + * @description Evaluator names removed by overwrite + */ + evaluators_removed?: string[]; + /** + * Evaluators Updated + * @description Existing evaluator names updated by overwrite + */ + evaluators_updated?: string[]; + /** + * Metadata Changed + * @description Whether agent metadata changed + * @default false + */ + metadata_changed: boolean; + /** + * Steps Added + * @description Steps added by overwrite + */ + steps_added?: components["schemas"]["StepKey"][]; + /** + * Steps Removed + * @description Steps removed by overwrite + */ + steps_removed?: components["schemas"]["StepKey"][]; + /** + * Steps Updated + * @description Existing steps updated by overwrite + */ + steps_updated?: components["schemas"]["StepKey"][]; + }; + /** + * InitAgentRequest + * @description Request to initialize or update an agent registration. + * @example { + * "agent": { + * "agent_description": "Handles customer inquiries", + * "agent_name": "customer-service-bot", + * "agent_version": "1.0.0" + * }, + * "evaluators": [ + * { + * "config_schema": { + * "properties": { + * "sensitivity": { + * "type": "string" + * } + * }, + * "type": "object" + * }, + * "description": "Detects PII in text", + * "name": "pii-detector" + * } + * ], + * "steps": [ + * { + * "input_schema": { + * "query": { + * "type": "string" + * } + * }, + * "name": "search_kb", + * "output_schema": { + * "results": { + * "type": "array" + * } + * }, + * "type": "tool" + * } + * ] + * } + */ + InitAgentRequest: { + /** @description Agent metadata including ID, name, and version */ + agent: components["schemas"]["Agent"]; + /** + * @description Conflict handling mode for init registration updates. 'strict' preserves existing compatibility checks. 'overwrite' applies latest-init-wins replacement for steps and evaluators. + * @default strict + */ + conflict_mode: components["schemas"]["ConflictMode"]; + /** + * Evaluators + * @description Custom evaluator schemas for config validation + */ + evaluators?: components["schemas"]["EvaluatorSchema"][]; + /** + * Force Replace + * @description If true, replace corrupted agent data instead of failing. Use only when agent data is corrupted and cannot be parsed. + * @default false + */ + force_replace: boolean; + /** + * Steps + * @description List of steps available to the agent + */ + steps?: components["schemas"]["StepSchema"][]; + }; + /** + * InitAgentResponse + * @description Response from agent initialization. + */ + InitAgentResponse: { + /** + * Controls + * @description Active protection controls for the agent + */ + controls?: components["schemas"]["Control"][]; + /** + * Created + * @description True if agent was newly created, False if updated + */ + created: boolean; + /** + * Overwrite Applied + * @description True if overwrite mode changed registration data on an existing agent + * @default false + */ + overwrite_applied: boolean; + /** @description Detailed list of changes applied in overwrite mode */ + overwrite_changes?: components["schemas"]["InitAgentOverwriteChanges"]; + }; + JSONObject: { + [key: string]: components["schemas"]["JSONValue"]; + }; + /** @description Any JSON value */ + JSONValue: unknown; + /** + * ListAgentsResponse + * @description Response for listing agents. + */ + ListAgentsResponse: { + /** + * Agents + * @description List of agent summaries + */ + agents: components["schemas"]["AgentSummary"][]; + /** @description Pagination metadata */ + pagination: components["schemas"]["PaginationInfo"]; + }; + /** + * ListControlsResponse + * @description Response for listing controls. + */ + ListControlsResponse: { + /** + * Controls + * @description List of control summaries + */ + controls: components["schemas"]["ControlSummary"][]; + /** @description Pagination metadata */ + pagination: components["schemas"]["PaginationInfo"]; + }; + /** + * ListEvaluatorConfigsResponse + * @description Response for listing evaluator configs. + */ + ListEvaluatorConfigsResponse: { + /** + * Evaluator Configs + * @description List of evaluator configs + */ + evaluator_configs: components["schemas"]["EvaluatorConfigItem"][]; + /** @description Pagination metadata */ + pagination: components["schemas"]["PaginationInfo"]; + }; + /** + * ListEvaluatorsResponse + * @description Response for listing agent's evaluator schemas. + */ + ListEvaluatorsResponse: { + /** Evaluators */ + evaluators: components["schemas"]["EvaluatorSchemaItem"][]; + pagination: components["schemas"]["PaginationInfo"]; + }; + /** + * LoginRequest + * @description Request body for the /login endpoint. + */ + LoginRequest: { + /** Api Key */ + api_key: string; + }; + /** + * LoginResponse + * @description Response body for the /login endpoint. + */ + LoginResponse: { + /** Authenticated */ + authenticated: boolean; + /** Is Admin */ + is_admin: boolean; + }; + /** + * PaginationInfo + * @description Pagination metadata for cursor-based pagination. + */ + PaginationInfo: { + /** + * Has More + * @description Whether there are more pages available + */ + has_more: boolean; + /** + * Limit + * @description Number of items per page + */ + limit: number; + /** + * Next Cursor + * @description Cursor for fetching the next page (null if no more pages) + */ + next_cursor?: string | null; + /** + * Total + * @description Total number of items + */ + total: number; + }; + /** + * PatchAgentRequest + * @description Request to modify an agent (remove steps/evaluators). + */ + PatchAgentRequest: { + /** + * Remove Evaluators + * @description Evaluator names to remove from the agent + */ + remove_evaluators?: string[]; + /** + * Remove Steps + * @description Step identifiers to remove from the agent + */ + remove_steps?: components["schemas"]["StepKey"][]; + }; + /** + * PatchAgentResponse + * @description Response from agent modification. + */ + PatchAgentResponse: { + /** + * Evaluators Removed + * @description Evaluator names that were removed + */ + evaluators_removed?: string[]; + /** + * Steps Removed + * @description Step identifiers that were removed + */ + steps_removed?: components["schemas"]["StepKey"][]; + }; + /** + * PatchControlRequest + * @description Request to update control metadata (name, enabled status). + */ + PatchControlRequest: { + /** + * Enabled + * @description Enable or disable the control + */ + enabled?: boolean | null; + /** + * Name + * @description New name for the control + */ + name?: string | null; + }; + /** + * PatchControlResponse + * @description Response from control metadata update. + */ + PatchControlResponse: { + /** + * Enabled + * @description Current enabled status (if control has data configured) + */ + enabled?: boolean | null; + /** + * Name + * @description Current control name (may have changed) + */ + name: string; + /** + * Success + * @description Whether the update succeeded + */ + success: boolean; + }; + /** + * RemoveAgentControlResponse + * @description Response for removing a direct agent-control association. + */ + RemoveAgentControlResponse: { + /** + * Control Still Active + * @description True if the control remains active via policy association(s) + */ + control_still_active: boolean; + /** + * Removed Direct Association + * @description True if a direct agent-control link was removed + */ + removed_direct_association: boolean; + /** + * Success + * @description Whether the request succeeded + */ + success: boolean; + }; + /** + * SetControlDataRequest + * @description Request to update control configuration data. + */ + SetControlDataRequest: { + /** @description Control configuration data (replaces existing) */ + data: components["schemas"]["ControlDefinition-Input"]; + }; + /** SetControlDataResponse */ + SetControlDataResponse: { + /** + * Success + * @description Whether the control data was updated + */ + success: boolean; + }; + /** + * SetPolicyResponse + * @description Compatibility response for singular policy assignment endpoint. + */ + SetPolicyResponse: { + /** + * Old Policy Id + * @description Previously associated policy ID, if any + */ + old_policy_id?: number | null; + /** + * Success + * @description Whether the request succeeded + */ + success: boolean; + }; + /** + * StatsResponse + * @description Response model for agent-level aggregated statistics. + * + * Contains agent-level totals (with optional timeseries) and per-control breakdown. + * + * Attributes: + * agent_name: Agent identifier + * time_range: Time range used + * totals: Agent-level aggregate statistics (includes timeseries) + * controls: Per-control breakdown for discovery and detail + */ + StatsResponse: { + /** + * Agent Name + * @description Agent identifier + */ + agent_name: string; + /** + * Controls + * @description Per-control breakdown + */ + controls: components["schemas"]["ControlStats"][]; + /** + * Time Range + * @description Time range used + */ + time_range: string; + /** @description Agent-level aggregate statistics */ + totals: components["schemas"]["StatsTotals"]; + }; + /** + * StatsTotals + * @description Agent-level aggregate statistics. + * + * Invariant: execution_count = match_count + non_match_count + error_count + * + * Matches have actions (allow, deny, steer, warn, log) tracked in action_counts. + * sum(action_counts.values()) == match_count + * + * Attributes: + * execution_count: Total executions across all controls + * match_count: Total matches across all controls (evaluator matched) + * non_match_count: Total non-matches across all controls (evaluator didn't match) + * error_count: Total errors across all controls (evaluation failed) + * action_counts: Breakdown of actions for matched executions + * timeseries: Time-series data points (only when include_timeseries=true) + */ + StatsTotals: { + /** + * Action Counts + * @description Action breakdown for matches: {allow, deny, steer, warn, log} + */ + action_counts?: { + [key: string]: number; + }; + /** + * Error Count + * @description Total errors + * @default 0 + */ + error_count: number; + /** + * Execution Count + * @description Total executions + */ + execution_count: number; + /** + * Match Count + * @description Total matches + * @default 0 + */ + match_count: number; + /** + * Non Match Count + * @description Total non-matches + * @default 0 + */ + non_match_count: number; + /** + * Timeseries + * @description Time-series data points (only when include_timeseries=true) + */ + timeseries?: components["schemas"]["TimeseriesBucket"][] | null; + }; + /** + * SteeringContext + * @description Steering context for steer actions. + * + * This model provides an extensible structure for steering guidance. + * Future fields could include severity, categories, suggested_actions, etc. + * @example { + * "message": "This large transfer requires user verification. Request 2FA code from user, verify it, then retry the transaction with verified_2fa=True." + * } + * @example { + * "message": "Transfer exceeds daily limit. Steps: 1) Ask user for business justification, 2) Request manager approval with amount and justification, 3) If approved, retry with manager_approved=True and justification filled in." + * } + */ + SteeringContext: { + /** + * Message + * @description Guidance message explaining what needs to be corrected and how + */ + message: string; + }; + /** + * Step + * @description Runtime payload for an agent step invocation. + */ + Step: { + /** @description Optional context (conversation history, metadata, etc.) */ + context?: components["schemas"]["JSONObject"] | null; + /** @description Input content for this step */ + input: components["schemas"]["JSONValue"]; + /** + * Name + * @description Step name (tool name or model/chain id) + */ + name: string; + /** @description Output content for this step (None for pre-checks) */ + output?: components["schemas"]["JSONValue"] | null; + /** + * Type + * @description Step type (e.g., 'tool', 'llm') + */ + type: string; + }; + /** + * StepKey + * @description Identifies a registered step schema by type and name. + */ + StepKey: { + /** + * Name + * @description Registered step name + */ + name: string; + /** + * Type + * @description Step type + */ + type: string; + }; + /** + * StepSchema + * @description Schema for a registered agent step. + * @example { + * "description": "Search the internal knowledge base", + * "input_schema": { + * "query": { + * "description": "Search query", + * "type": "string" + * } + * }, + * "name": "search_knowledge_base", + * "output_schema": { + * "results": { + * "items": { + * "type": "object" + * }, + * "type": "array" + * } + * }, + * "type": "tool" + * } + * @example { + * "description": "Customer support response generation", + * "input_schema": { + * "messages": { + * "items": { + * "type": "object" + * }, + * "type": "array" + * } + * }, + * "name": "support-answer", + * "output_schema": { + * "text": { + * "type": "string" + * } + * }, + * "type": "llm" + * } + */ + StepSchema: { + /** + * Description + * @description Optional description of the step + */ + description?: string | null; + /** + * Input Schema + * @description JSON schema describing step input + */ + input_schema?: { + [key: string]: unknown; + } | null; + /** + * Metadata + * @description Additional metadata for the step + */ + metadata?: { + [key: string]: unknown; + } | null; + /** + * Name + * @description Unique name for the step + */ + name: string; + /** + * Output Schema + * @description JSON schema describing step output + */ + output_schema?: { + [key: string]: unknown; + } | null; + /** + * Type + * @description Step type for this schema (e.g., 'tool', 'llm') + */ + type: string; + }; + /** + * TimeseriesBucket + * @description Single data point in a time-series. + * + * Represents aggregated metrics for a single time bucket. + * + * Attributes: + * timestamp: Start time of the bucket (UTC, always timezone-aware) + * execution_count: Total executions in this bucket + * match_count: Number of matches in this bucket + * non_match_count: Number of non-matches in this bucket + * error_count: Number of errors in this bucket + * action_counts: Breakdown of actions for matched executions + * avg_confidence: Average confidence score (None if no executions) + * avg_duration_ms: Average execution duration in milliseconds (None if no data) + */ + TimeseriesBucket: { + /** + * Action Counts + * @description Action breakdown: {allow, deny, steer, warn, log} + */ + action_counts?: { + [key: string]: number; + }; + /** + * Avg Confidence + * @description Average confidence score + */ + avg_confidence?: number | null; + /** + * Avg Duration Ms + * @description Average duration (ms) + */ + avg_duration_ms?: number | null; + /** + * Error Count + * @description Errors in bucket + */ + error_count: number; + /** + * Execution Count + * @description Total executions in bucket + */ + execution_count: number; + /** + * Match Count + * @description Matches in bucket + */ + match_count: number; + /** + * Non Match Count + * @description Non-matches in bucket + */ + non_match_count: number; + /** + * Timestamp + * Format: date-time + * @description Start time of the bucket (UTC) + */ + timestamp: string; + }; + /** + * UpdateEvaluatorConfigRequest + * @description Request to replace an evaluator config template. + */ + UpdateEvaluatorConfigRequest: { + /** + * Config + * @description Evaluator-specific configuration + */ + config: { + [key: string]: unknown; + }; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Evaluator + * @description Evaluator name (built-in or custom) + */ + evaluator: string; + /** + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** + * ValidateControlDataRequest + * @description Request to validate control configuration data without saving. + */ + ValidateControlDataRequest: { + /** @description Control configuration data to validate */ + data: components["schemas"]["ControlDefinition-Input"]; + }; + /** ValidateControlDataResponse */ + ValidateControlDataResponse: { + /** + * Success + * @description Whether the control data is valid + */ + success: boolean; + }; + /** ValidationError */ + ValidationError: { + /** Context */ + ctx?: Record; + /** Input */ + input?: unknown; + /** Location */ + loc: (string | number)[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } export type $defs = Record; export interface operations { - list_agents_api_v1_agents_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - name?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Paginated list of agent summaries */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ListAgentsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - init_agent_api_v1_agents_initAgent_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['InitAgentRequest']; - }; - }; - responses: { - /** @description Agent registration status with active controls */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['InitAgentResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_agent_api_v1_agents__agent_name__get: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Agent metadata and registered steps */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetAgentResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - patch_agent_api_v1_agents__agent_name__patch: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['PatchAgentRequest']; - }; - }; - responses: { - /** @description Lists of removed items */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['PatchAgentResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_agent_controls_api_v1_agents__agent_name__controls_get: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of controls from agent policy and direct associations */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AgentControlsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - add_agent_control_api_v1_agents__agent_name__controls__control_id__post: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AssocResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - remove_agent_control_api_v1_agents__agent_name__controls__control_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['RemoveAgentControlResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_agent_evaluators_api_v1_agents__agent_name__evaluators_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - }; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Evaluator schemas registered with this agent */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ListEvaluatorsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_agent_evaluator_api_v1_agents__agent_name__evaluators__evaluator_name__get: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - evaluator_name: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Evaluator schema details */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['EvaluatorSchemaItem']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_agent_policies_api_v1_agents__agent_name__policies_get: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of policy IDs */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetAgentPoliciesResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - remove_all_agent_policies_api_v1_agents__agent_name__policies_delete: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AssocResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - add_agent_policy_api_v1_agents__agent_name__policies__policy_id__post: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - policy_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AssocResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - remove_agent_policy_api_v1_agents__agent_name__policies__policy_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - policy_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AssocResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_agent_policy_api_v1_agents__agent_name__policy_get: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Policy ID */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetPolicyResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - delete_agent_policy_api_v1_agents__agent_name__policy_delete: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['DeletePolicyResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - set_agent_policy_api_v1_agents__agent_name__policy__policy_id__post: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - policy_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success status with previous policy ID */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SetPolicyResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_controls_api_v1_controls_get: { - parameters: { - query?: { - /** @description Control ID to start after */ - cursor?: number | null; - limit?: number; - /** @description Filter by name (partial, case-insensitive) */ - name?: string | null; - /** @description Filter by enabled status */ - enabled?: boolean | null; - /** @description Filter by step type (built-ins: 'tool', 'llm') */ - step_type?: string | null; - /** @description Filter by stage ('pre' or 'post') */ - stage?: string | null; - /** @description Filter by execution ('server' or 'sdk') */ - execution?: string | null; - /** @description Filter by tag */ - tag?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Paginated list of controls */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ListControlsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - create_control_api_v1_controls_put: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateControlRequest']; - }; - }; - responses: { - /** @description Created control ID */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['CreateControlResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - validate_control_data_api_v1_controls_validate_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['ValidateControlDataRequest']; - }; - }; - responses: { - /** @description Validation result */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ValidateControlDataResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_control_api_v1_controls__control_id__get: { - parameters: { - query?: never; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Control metadata and configuration */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetControlResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - delete_control_api_v1_controls__control_id__delete: { - parameters: { - query?: { - /** @description If true, dissociate from all policy/agent links before deleting. If false, fail if control is associated with any policy or agent. */ - force?: boolean; - }; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Deletion confirmation with dissociation info */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['DeleteControlResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - patch_control_api_v1_controls__control_id__patch: { - parameters: { - query?: never; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['PatchControlRequest']; - }; - }; - responses: { - /** @description Updated control information */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['PatchControlResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_control_data_api_v1_controls__control_id__data_get: { - parameters: { - query?: never; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Control data payload */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetControlDataResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - set_control_data_api_v1_controls__control_id__data_put: { - parameters: { - query?: never; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['SetControlDataRequest']; - }; - }; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SetControlDataResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - evaluate_api_v1_evaluation_post: { - parameters: { - query?: never; - header?: { - 'X-Trace-Id'?: string | null; - 'X-Span-Id'?: string | null; - }; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['EvaluationRequest']; - }; - }; - responses: { - /** @description Safety analysis result */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['EvaluationResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_evaluator_configs_api_v1_evaluator_configs_get: { - parameters: { - query?: { - /** @description Evaluator config ID to start after */ - cursor?: number | null; - limit?: number; - /** @description Filter by name (partial, case-insensitive) */ - name?: string | null; - /** @description Filter by evaluator name */ - evaluator?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Paginated list of evaluator configs */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ListEvaluatorConfigsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - create_evaluator_config_api_v1_evaluator_configs_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateEvaluatorConfigRequest']; - }; - }; - responses: { - /** @description Created evaluator config */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['EvaluatorConfigItem']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_evaluator_config_api_v1_evaluator_configs__config_id__get: { - parameters: { - query?: never; - header?: never; - path: { - config_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Evaluator config details */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['EvaluatorConfigItem']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - update_evaluator_config_api_v1_evaluator_configs__config_id__put: { - parameters: { - query?: never; - header?: never; - path: { - config_id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['UpdateEvaluatorConfigRequest']; - }; - }; - responses: { - /** @description Updated evaluator config */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['EvaluatorConfigItem']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - delete_evaluator_config_api_v1_evaluator_configs__config_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - config_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Deletion confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['DeleteEvaluatorConfigResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_evaluators_api_v1_evaluators_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Dictionary of evaluator name to evaluator info */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - [key: string]: components['schemas']['EvaluatorInfo']; - }; - }; - }; - }; - }; - ingest_events_api_v1_observability_events_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['BatchEventsRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 202: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['BatchEventsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - query_events_api_v1_observability_events_query_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['EventQueryRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['EventQueryResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_stats_api_v1_observability_stats_get: { - parameters: { - query: { - agent_name: string; - time_range?: - | '1m' - | '5m' - | '15m' - | '1h' - | '24h' - | '7d' - | '30d' - | '180d' - | '365d'; - include_timeseries?: boolean; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['StatsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_control_stats_api_v1_observability_stats_controls__control_id__get: { - parameters: { - query: { - agent_name: string; - time_range?: - | '1m' - | '5m' - | '15m' - | '1h' - | '24h' - | '7d' - | '30d' - | '180d' - | '365d'; - include_timeseries?: boolean; - }; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ControlStatsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_status_api_v1_observability_status_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - [key: string]: unknown; - }; - }; - }; - }; - }; - create_policy_api_v1_policies_put: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreatePolicyRequest']; - }; - }; - responses: { - /** @description Created policy ID */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['CreatePolicyResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_policy_controls_api_v1_policies__policy_id__controls_get: { - parameters: { - query?: never; - header?: never; - path: { - policy_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of control IDs */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetPolicyControlsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - add_control_to_policy_api_v1_policies__policy_id__controls__control_id__post: { - parameters: { - query?: never; - header?: never; - path: { - policy_id: number; - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AssocResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - remove_control_from_policy_api_v1_policies__policy_id__controls__control_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - policy_id: number; - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AssocResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - health_check_health_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Server health status */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HealthResponse']; + get_config_api_config_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Configuration flags for UI behavior */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ConfigResponse"]; + }; + }; + }; + }; + login_api_login_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LoginRequest"]; + }; + }; + responses: { + /** @description Authentication result; sets HttpOnly session cookie on success */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LoginResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + logout_api_logout_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + list_agents_api_v1_agents_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + name?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of agent summaries */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListAgentsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + init_agent_api_v1_agents_initAgent_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["InitAgentRequest"]; + }; + }; + responses: { + /** @description Agent registration status with active controls */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["InitAgentResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_agent_api_v1_agents__agent_name__get: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Agent metadata and registered steps */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetAgentResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + patch_agent_api_v1_agents__agent_name__patch: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PatchAgentRequest"]; + }; + }; + responses: { + /** @description Lists of removed items */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchAgentResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_agent_controls_api_v1_agents__agent_name__controls_get: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of controls from agent policy and direct associations */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AgentControlsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + add_agent_control_api_v1_agents__agent_name__controls__control_id__post: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AssocResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + remove_agent_control_api_v1_agents__agent_name__controls__control_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RemoveAgentControlResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_agent_evaluators_api_v1_agents__agent_name__evaluators_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + }; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Evaluator schemas registered with this agent */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListEvaluatorsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_agent_evaluator_api_v1_agents__agent_name__evaluators__evaluator_name__get: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + evaluator_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Evaluator schema details */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EvaluatorSchemaItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_agent_policies_api_v1_agents__agent_name__policies_get: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of policy IDs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetAgentPoliciesResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + remove_all_agent_policies_api_v1_agents__agent_name__policies_delete: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AssocResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + add_agent_policy_api_v1_agents__agent_name__policies__policy_id__post: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + policy_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AssocResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + remove_agent_policy_api_v1_agents__agent_name__policies__policy_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + policy_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AssocResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_agent_policy_api_v1_agents__agent_name__policy_get: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Policy ID */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetPolicyResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_agent_policy_api_v1_agents__agent_name__policy_delete: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeletePolicyResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + set_agent_policy_api_v1_agents__agent_name__policy__policy_id__post: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + policy_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success status with previous policy ID */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SetPolicyResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_controls_api_v1_controls_get: { + parameters: { + query?: { + /** @description Control ID to start after */ + cursor?: number | null; + limit?: number; + /** @description Filter by name (partial, case-insensitive) */ + name?: string | null; + /** @description Filter by enabled status */ + enabled?: boolean | null; + /** @description Filter by step type (built-ins: 'tool', 'llm') */ + step_type?: string | null; + /** @description Filter by stage ('pre' or 'post') */ + stage?: string | null; + /** @description Filter by execution ('server' or 'sdk') */ + execution?: string | null; + /** @description Filter by tag */ + tag?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of controls */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListControlsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_control_api_v1_controls_put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateControlRequest"]; + }; + }; + responses: { + /** @description Created control ID */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreateControlResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + validate_control_data_api_v1_controls_validate_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ValidateControlDataRequest"]; + }; + }; + responses: { + /** @description Validation result */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ValidateControlDataResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_control_api_v1_controls__control_id__get: { + parameters: { + query?: never; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Control metadata and configuration */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetControlResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_control_api_v1_controls__control_id__delete: { + parameters: { + query?: { + /** @description If true, dissociate from all policy/agent links before deleting. If false, fail if control is associated with any policy or agent. */ + force?: boolean; + }; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deletion confirmation with dissociation info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeleteControlResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + patch_control_api_v1_controls__control_id__patch: { + parameters: { + query?: never; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PatchControlRequest"]; + }; + }; + responses: { + /** @description Updated control information */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchControlResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_control_data_api_v1_controls__control_id__data_get: { + parameters: { + query?: never; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Control data payload */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetControlDataResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + set_control_data_api_v1_controls__control_id__data_put: { + parameters: { + query?: never; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetControlDataRequest"]; + }; + }; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SetControlDataResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + evaluate_api_v1_evaluation_post: { + parameters: { + query?: never; + header?: { + "X-Trace-Id"?: string | null; + "X-Span-Id"?: string | null; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EvaluationRequest"]; + }; + }; + responses: { + /** @description Safety analysis result */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EvaluationResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_evaluator_configs_api_v1_evaluator_configs_get: { + parameters: { + query?: { + /** @description Evaluator config ID to start after */ + cursor?: number | null; + limit?: number; + /** @description Filter by name (partial, case-insensitive) */ + name?: string | null; + /** @description Filter by evaluator name */ + evaluator?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of evaluator configs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListEvaluatorConfigsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_evaluator_config_api_v1_evaluator_configs_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateEvaluatorConfigRequest"]; + }; + }; + responses: { + /** @description Created evaluator config */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EvaluatorConfigItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_evaluator_config_api_v1_evaluator_configs__config_id__get: { + parameters: { + query?: never; + header?: never; + path: { + config_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Evaluator config details */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EvaluatorConfigItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_evaluator_config_api_v1_evaluator_configs__config_id__put: { + parameters: { + query?: never; + header?: never; + path: { + config_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateEvaluatorConfigRequest"]; + }; + }; + responses: { + /** @description Updated evaluator config */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EvaluatorConfigItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_evaluator_config_api_v1_evaluator_configs__config_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + config_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deletion confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeleteEvaluatorConfigResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_evaluators_api_v1_evaluators_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Dictionary of evaluator name to evaluator info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: components["schemas"]["EvaluatorInfo"]; + }; + }; + }; + }; + }; + ingest_events_api_v1_observability_events_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BatchEventsRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchEventsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + query_events_api_v1_observability_events_query_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EventQueryRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EventQueryResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_stats_api_v1_observability_stats_get: { + parameters: { + query: { + agent_name: string; + time_range?: "1m" | "5m" | "15m" | "1h" | "24h" | "7d" | "30d" | "180d" | "365d"; + include_timeseries?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StatsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_control_stats_api_v1_observability_stats_controls__control_id__get: { + parameters: { + query: { + agent_name: string; + time_range?: "1m" | "5m" | "15m" | "1h" | "24h" | "7d" | "30d" | "180d" | "365d"; + include_timeseries?: boolean; + }; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ControlStatsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_status_api_v1_observability_status_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + }; + }; + create_policy_api_v1_policies_put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreatePolicyRequest"]; + }; + }; + responses: { + /** @description Created policy ID */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreatePolicyResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_policy_controls_api_v1_policies__policy_id__controls_get: { + parameters: { + query?: never; + header?: never; + path: { + policy_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of control IDs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetPolicyControlsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + add_control_to_policy_api_v1_policies__policy_id__controls__control_id__post: { + parameters: { + query?: never; + header?: never; + path: { + policy_id: number; + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AssocResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + remove_control_from_policy_api_v1_policies__policy_id__controls__control_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + policy_id: number; + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AssocResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + health_check_health_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Server health status */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HealthResponse"]; + }; + }; }; - }; }; - }; } diff --git a/ui/src/core/api/types.ts b/ui/src/core/api/types.ts index abb335a5..a4720f5f 100644 --- a/ui/src/core/api/types.ts +++ b/ui/src/core/api/types.ts @@ -76,6 +76,9 @@ export type ControlStage = NonNullable< export type ControlScope = components['schemas']['ControlScope']; export type ControlSelector = components['schemas']['ControlSelector']; export type ControlAction = components['schemas']['ControlAction']; +export type ConditionNodeInput = components['schemas']['ConditionNode-Input']; +export type ConditionNodeOutput = components['schemas']['ConditionNode-Output']; +export type ConditionNode = ConditionNodeInput | ConditionNodeOutput; export type ControlDefinitionInput = components['schemas']['ControlDefinition-Input']; export type ControlDefinitionOutput = diff --git a/ui/src/core/page-components/agent-detail/modals/add-new-control/index.tsx b/ui/src/core/page-components/agent-detail/modals/add-new-control/index.tsx index 9fbf0949..5fba8fd6 100644 --- a/ui/src/core/page-components/agent-detail/modals/add-new-control/index.tsx +++ b/ui/src/core/page-components/agent-detail/modals/add-new-control/index.tsx @@ -123,12 +123,14 @@ export function AddNewControlModal({ step_types: ['llm'], stages: ['post'] as ('post' | 'pre')[], }, - selector: { - path: '*', - }, - evaluator: { - name: selectedEvaluator.id, - config: getDefaultConfigForEvaluator(selectedEvaluator.id), + condition: { + selector: { + path: '*', + }, + evaluator: { + name: selectedEvaluator.id, + config: getDefaultConfigForEvaluator(selectedEvaluator.id), + }, }, action: { decision: 'deny' as const }, }, diff --git a/ui/src/core/page-components/agent-detail/modals/edit-control/condition-builder.ts b/ui/src/core/page-components/agent-detail/modals/edit-control/condition-builder.ts new file mode 100644 index 00000000..10a1eb9c --- /dev/null +++ b/ui/src/core/page-components/agent-detail/modals/edit-control/condition-builder.ts @@ -0,0 +1,420 @@ +import { getEvaluator } from '@/core/evaluators'; + +import type { + ConditionNode, + ConditionNodeInput, + ValidationErrorItem, +} from '@/core/api/types'; + +const VALID_SELECTOR_ROOTS = ['input', 'output', 'name', 'type', 'context', '*']; + +export type ConditionBuilderLeaf = { + id: string; + kind: 'leaf'; + selectorPath: string; + evaluatorName: string; + config: Record; +}; + +export type ConditionBuilderGroup = { + id: string; + kind: 'and' | 'or'; + children: ConditionBuilderNode[]; +}; + +export type ConditionBuilderNot = { + id: string; + kind: 'not'; + child: ConditionBuilderNode; +}; + +export type ConditionBuilderNode = + | ConditionBuilderLeaf + | ConditionBuilderGroup + | ConditionBuilderNot; + +export type ConditionBuilderError = { + field: string | null; + message: string; +}; + +export function createNodeId(): string { + return `condition-${Math.random().toString(36).slice(2, 10)}`; +} + +export function createLeafNode( + evaluatorName = 'regex', + selectorPath = '*', + config?: Record +): ConditionBuilderLeaf { + const evaluator = getEvaluator(evaluatorName); + return { + id: createNodeId(), + kind: 'leaf', + selectorPath, + evaluatorName, + config: config ?? evaluator?.toConfig(evaluator.initialValues) ?? {}, + }; +} + +export function createGroupNode( + kind: 'and' | 'or' = 'and', + children: ConditionBuilderNode[] = [createLeafNode()] +): ConditionBuilderGroup { + return { + id: createNodeId(), + kind, + children, + }; +} + +export function createNotNode( + child: ConditionBuilderNode = createLeafNode() +): ConditionBuilderNot { + return { + id: createNodeId(), + kind: 'not', + child, + }; +} + +export function deserializeConditionNode(node: ConditionNode): ConditionBuilderNode { + if (node.selector && node.evaluator) { + return createLeafNode( + node.evaluator.name, + node.selector.path ?? '*', + (node.evaluator.config as Record) ?? {} + ); + } + + if (node.and?.length) { + return createGroupNode( + 'and', + node.and.map((child) => deserializeConditionNode(child)) + ); + } + + if (node.or?.length) { + return createGroupNode( + 'or', + node.or.map((child) => deserializeConditionNode(child)) + ); + } + + if (node.not) { + return createNotNode(deserializeConditionNode(node.not)); + } + + return createLeafNode(); +} + +export function serializeConditionNode( + node: ConditionBuilderNode +): ConditionNodeInput { + if (node.kind === 'leaf') { + return { + selector: { path: node.selectorPath }, + evaluator: { + name: node.evaluatorName, + config: node.config, + }, + }; + } + + if (node.kind === 'not') { + return { + not: serializeConditionNode(node.child), + }; + } + + return { + [node.kind]: node.children.map((child) => serializeConditionNode(child)), + }; +} + +export function updateConditionNode( + root: ConditionBuilderNode, + targetId: string, + updater: (node: ConditionBuilderNode) => ConditionBuilderNode +): ConditionBuilderNode { + if (root.id === targetId) { + return updater(root); + } + + if (root.kind === 'not') { + return { + ...root, + child: updateConditionNode(root.child, targetId, updater), + }; + } + + if (root.kind === 'and' || root.kind === 'or') { + return { + ...root, + children: root.children.map((child) => + updateConditionNode(child, targetId, updater) + ), + }; + } + + return root; +} + +export function insertChildNode( + root: ConditionBuilderNode, + parentId: string, + child: ConditionBuilderNode +): ConditionBuilderNode { + return updateConditionNode(root, parentId, (node) => { + if (node.kind !== 'and' && node.kind !== 'or') { + return node; + } + return { + ...node, + children: [...node.children, child], + }; + }); +} + +export function replaceConditionNode( + root: ConditionBuilderNode, + targetId: string, + replacement: ConditionBuilderNode +): ConditionBuilderNode { + if (root.id === targetId) { + return replacement; + } + + if (root.kind === 'not') { + return { + ...root, + child: replaceConditionNode(root.child, targetId, replacement), + }; + } + + if (root.kind === 'and' || root.kind === 'or') { + return { + ...root, + children: root.children.map((child) => + child.id === targetId + ? replacement + : replaceConditionNode(child, targetId, replacement) + ), + }; + } + + return root; +} + +export function deleteConditionNode( + root: ConditionBuilderNode, + targetId: string +): ConditionBuilderNode { + if (root.id === targetId) { + return createLeafNode(); + } + + if (root.kind === 'not') { + if (root.child.id === targetId) { + return createLeafNode(); + } + return { + ...root, + child: deleteConditionNode(root.child, targetId), + }; + } + + if (root.kind === 'and' || root.kind === 'or') { + const nextChildren = root.children + .filter((child) => child.id !== targetId) + .map((child) => deleteConditionNode(child, targetId)); + + return { + ...root, + children: nextChildren.length > 0 ? nextChildren : [createLeafNode()], + }; + } + + return root; +} + +export function moveConditionNode( + root: ConditionBuilderNode, + targetId: string, + direction: 'up' | 'down' +): ConditionBuilderNode { + if (root.kind === 'not') { + return { + ...root, + child: moveConditionNode(root.child, targetId, direction), + }; + } + + if (root.kind === 'and' || root.kind === 'or') { + const index = root.children.findIndex((child) => child.id === targetId); + if (index >= 0) { + const swapIndex = direction === 'up' ? index - 1 : index + 1; + if (swapIndex < 0 || swapIndex >= root.children.length) { + return root; + } + + const nextChildren = [...root.children]; + [nextChildren[index], nextChildren[swapIndex]] = [ + nextChildren[swapIndex]!, + nextChildren[index]!, + ]; + return { + ...root, + children: nextChildren, + }; + } + + return { + ...root, + children: root.children.map((child) => + moveConditionNode(child, targetId, direction) + ), + }; + } + + return root; +} + +export function wrapConditionNodeWithNot( + root: ConditionBuilderNode, + targetId: string +): ConditionBuilderNode { + return replaceConditionNode(root, targetId, createNotNode(getNodeById(root, targetId))); +} + +export function unwrapNotConditionNode( + root: ConditionBuilderNode, + targetId: string +): ConditionBuilderNode { + return updateConditionNode(root, targetId, (node) => { + if (node.kind !== 'not') { + return node; + } + return node.child; + }); +} + +export function getNodeById( + root: ConditionBuilderNode, + targetId: string +): ConditionBuilderNode { + if (root.id === targetId) { + return root; + } + + if (root.kind === 'not') { + return getNodeById(root.child, targetId); + } + + if (root.kind === 'and' || root.kind === 'or') { + for (const child of root.children) { + try { + return getNodeById(child, targetId); + } catch { + // Continue searching siblings. + } + } + } + + throw new Error(`Condition node '${targetId}' not found`); +} + +export function validateConditionTree( + node: ConditionBuilderNode, + path = 'data.condition', + depth = 1, + maxDepth = 6 +): ConditionBuilderError[] { + const errors: ConditionBuilderError[] = []; + + if (depth > maxDepth) { + errors.push({ + field: path, + message: `Condition nesting depth exceeds maximum of ${maxDepth}`, + }); + } + + if (node.kind === 'leaf') { + if (!node.selectorPath.trim()) { + errors.push({ + field: `${path}.selector.path`, + message: 'Selector path is required', + }); + } else { + const root = node.selectorPath.split('.')[0]; + if (!VALID_SELECTOR_ROOTS.includes(root)) { + errors.push({ + field: `${path}.selector.path`, + message: `Invalid path root '${root}'. Must be one of: ${VALID_SELECTOR_ROOTS.join(', ')}`, + }); + } + } + + if (!node.evaluatorName.trim()) { + errors.push({ + field: `${path}.evaluator.name`, + message: 'Evaluator is required', + }); + } + + const evaluator = getEvaluator(node.evaluatorName); + if (evaluator?.validate) { + const formValues = evaluator.fromConfig(node.config); + for (const [field, validate] of Object.entries(evaluator.validate)) { + const message = validate( + (formValues as Record)[field], + formValues + ); + if (message) { + errors.push({ + field: `${path}.evaluator.config.${field}`, + message, + }); + } + } + } + + return errors; + } + + if (node.kind === 'not') { + return [ + ...errors, + ...validateConditionTree(node.child, `${path}.not`, depth + 1, maxDepth), + ]; + } + + if (node.children.length === 0) { + errors.push({ + field: `${path}.${node.kind}`, + message: `'${node.kind}' must contain at least one child condition`, + }); + } + + node.children.forEach((child, index) => { + errors.push( + ...validateConditionTree( + child, + `${path}.${node.kind}[${index}]`, + depth + 1, + maxDepth + ) + ); + }); + + return errors; +} + +export function getConditionErrorsForPath( + errors: ValidationErrorItem[] | ConditionBuilderError[], + path: string +): string[] { + return errors + .filter((error) => error.field === path || error.field?.startsWith(`${path}.`)) + .map((error) => error.message); +} diff --git a/ui/src/core/page-components/agent-detail/modals/edit-control/condition-tree-editor.tsx b/ui/src/core/page-components/agent-detail/modals/edit-control/condition-tree-editor.tsx new file mode 100644 index 00000000..7fa150f7 --- /dev/null +++ b/ui/src/core/page-components/agent-detail/modals/edit-control/condition-tree-editor.tsx @@ -0,0 +1,540 @@ +import { + Alert, + Autocomplete, + Badge, + Button, + Group, + Paper, + Select, + Stack, + Text, + Textarea, +} from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { + IconAlertCircle, + IconArrowDown, + IconArrowUp, + IconBraces, + IconPlus, + IconTrash, +} from '@tabler/icons-react'; +import { useEffect, useMemo, useState } from 'react'; + +import type { ValidationErrorItem } from '@/core/api/types'; +import { evaluators, getEvaluator } from '@/core/evaluators'; + +import { + createGroupNode, + createLeafNode, + deleteConditionNode, + type ConditionBuilderError, + type ConditionBuilderLeaf, + type ConditionBuilderNode, + getConditionErrorsForPath, + insertChildNode, + moveConditionNode, + replaceConditionNode, + updateConditionNode, + unwrapNotConditionNode, + wrapConditionNodeWithNot, +} from './condition-builder'; + +type ConditionTreeEditorProps = { + rootNode: ConditionBuilderNode; + onChange: (node: ConditionBuilderNode) => void; + errors: ValidationErrorItem[] | ConditionBuilderError[]; + warningDepth?: number; + maxDepth?: number; +}; + +type LeafEvaluatorFormProps = { + node: ConditionBuilderLeaf; + path: string; + errors: string[]; + onChange: (node: ConditionBuilderLeaf) => void; +}; + +const SELECTOR_OPTIONS = ['*', 'input', 'output', 'name', 'type', 'context']; + +function LeafEvaluatorForm({ + node, + path, + errors, + onChange, +}: LeafEvaluatorFormProps) { + const evaluator = getEvaluator(node.evaluatorName); + const [jsonText, setJsonText] = useState(() => + JSON.stringify(node.config, null, 2) + ); + const [jsonError, setJsonError] = useState(null); + const configSignature = useMemo(() => JSON.stringify(node.config), [node.config]); + + const form = useForm({ + initialValues: evaluator?.fromConfig(node.config) ?? {}, + validate: evaluator?.validate, + }); + + useEffect(() => { + setJsonText(JSON.stringify(node.config, null, 2)); + setJsonError(null); + }, [configSignature]); + + useEffect(() => { + if (!evaluator) { + return; + } + form.setValues(evaluator.fromConfig(node.config)); + form.clearErrors(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [evaluator?.id, configSignature]); + + useEffect(() => { + if (!evaluator) { + return; + } + const nextConfig = evaluator.toConfig(form.values); + if (JSON.stringify(nextConfig) === configSignature) { + return; + } + onChange({ + ...node, + config: nextConfig, + }); + }, [configSignature, evaluator, form.values, node, onChange]); + + if (!evaluator) { + return ( + +