4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
146 lines
6.6 KiB
Python
146 lines
6.6 KiB
Python
"""Abstract base class for all investigation tool actions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC
|
|
from collections.abc import Sequence
|
|
from typing import Any, ClassVar
|
|
|
|
from pydantic import BaseModel
|
|
|
|
from config.constants.investigation import DEFAULT_APPROVAL_EXPIRY_SECONDS
|
|
from core.domain.types.evidence import EvidenceSource
|
|
from core.domain.types.retrieval import RetrievalControls
|
|
from core.domain.types.tools import ToolSurface
|
|
from core.tool_framework.metadata import EvidenceType, SideEffectLevel, ToolMetadata
|
|
from core.tool_framework.registry_metadata import BaseToolRegistryMetadata
|
|
|
|
|
|
class BaseTool(ABC):
|
|
"""Abstract base class for every investigation tool.
|
|
|
|
Subclass contract
|
|
-----------------
|
|
* Declare all metadata as **ClassVars** (``name``, ``description``,
|
|
``input_schema``, ``source``, etc.). ``__init_subclass__`` validates
|
|
them through ``ToolMetadata`` on class creation, so missing or
|
|
ill-typed declarations fail at import time rather than at runtime.
|
|
* Implement ``run(**kwargs)`` — *not* declared here to avoid forcing a
|
|
fixed signature on every subclass. The planner invokes the tool
|
|
through ``__call__``, which delegates to ``run`` via
|
|
``telemetry.invoke_tool`` so exceptions are always captured and
|
|
converted to a structured ``{"error": ..., "exception_type": ...}``
|
|
dict rather than propagating to the agent loop.
|
|
* Override ``is_available`` and ``extract_params`` when the tool
|
|
requires specific data-source checks or needs to pull kwargs from the
|
|
investigation sources dict.
|
|
* Do **not** declare ``run`` with positional arguments — the call site
|
|
always uses keyword arguments: ``tool_instance.run(**kwargs)``.
|
|
"""
|
|
|
|
name: ClassVar[str]
|
|
description: ClassVar[str]
|
|
display_name: ClassVar[str | None] = None
|
|
input_schema: ClassVar[dict[str, Any]] # JSON Schema — consumed by LLM planner
|
|
input_model: ClassVar[type[BaseModel] | None] = None
|
|
source: ClassVar[EvidenceSource]
|
|
source_id: ClassVar[str | None] = None
|
|
evidence_type: ClassVar[EvidenceType | None] = None
|
|
side_effect_level: ClassVar[SideEffectLevel | None] = None
|
|
use_cases: ClassVar[Sequence[str]] = ()
|
|
examples: ClassVar[Sequence[str]] = ()
|
|
anti_examples: ClassVar[Sequence[str]] = ()
|
|
requires: ClassVar[Sequence[str]] = ()
|
|
outputs: ClassVar[dict[str, str]] = {} # Output field -> description (optional, for prompting)
|
|
output_schema: ClassVar[dict[str, Any] | None] = None
|
|
output_model: ClassVar[type[BaseModel] | None] = None
|
|
injected_params: ClassVar[Sequence[str]] = ()
|
|
retrieval_controls: ClassVar[RetrievalControls] = (
|
|
RetrievalControls()
|
|
) # Declares supported controls
|
|
surfaces: ClassVar[tuple[ToolSurface, ...]] = ("investigation",)
|
|
tags: ClassVar[Sequence[str]] = ()
|
|
parallel_safe: ClassVar[bool] = True
|
|
requires_approval: ClassVar[bool] = False # Whether this tool needs approval from messaging
|
|
approval_reason: ClassVar[str] = "" # Human-readable reason for requiring approval
|
|
approval_expiry_seconds: ClassVar[int] = DEFAULT_APPROVAL_EXPIRY_SECONDS
|
|
accepts_runtime_context: ClassVar[bool] = False
|
|
|
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
super().__init_subclass__(**kwargs)
|
|
metadata = cls.metadata()
|
|
cls.name = metadata.name
|
|
cls.description = metadata.description
|
|
cls.display_name = metadata.display_name
|
|
cls.input_schema = metadata.input_schema
|
|
cls.source = metadata.source
|
|
cls.source_id = metadata.source_id
|
|
cls.evidence_type = metadata.evidence_type
|
|
cls.side_effect_level = metadata.side_effect_level
|
|
cls.use_cases = tuple(metadata.use_cases)
|
|
cls.examples = tuple(metadata.examples)
|
|
cls.anti_examples = tuple(metadata.anti_examples)
|
|
cls.requires = tuple(metadata.requires)
|
|
cls.outputs = metadata.outputs
|
|
cls.output_schema = metadata.output_schema
|
|
cls.injected_params = tuple(metadata.injected_params)
|
|
cls.retrieval_controls = metadata.retrieval_controls
|
|
registry = cls.registry_metadata()
|
|
cls.surfaces = registry.surfaces
|
|
cls.tags = registry.tags
|
|
cls.parallel_safe = registry.parallel_safe
|
|
|
|
@classmethod
|
|
def metadata(cls) -> ToolMetadata:
|
|
"""Return validated tool metadata for this subclass."""
|
|
return ToolMetadata.model_validate(
|
|
{
|
|
"name": getattr(cls, "name", ""),
|
|
"description": getattr(cls, "description", ""),
|
|
"display_name": getattr(cls, "display_name", None),
|
|
"input_schema": getattr(cls, "input_schema", {}),
|
|
"source_id": getattr(cls, "source_id", None),
|
|
"source": getattr(cls, "source", ""),
|
|
"evidence_type": getattr(cls, "evidence_type", None),
|
|
"side_effect_level": getattr(cls, "side_effect_level", None),
|
|
"use_cases": list(getattr(cls, "use_cases", [])),
|
|
"examples": list(getattr(cls, "examples", [])),
|
|
"anti_examples": list(getattr(cls, "anti_examples", [])),
|
|
"requires": list(getattr(cls, "requires", [])),
|
|
"outputs": dict(getattr(cls, "outputs", {})),
|
|
"output_schema": getattr(cls, "output_schema", None),
|
|
"injected_params": list(getattr(cls, "injected_params", [])),
|
|
"retrieval_controls": getattr(cls, "retrieval_controls", RetrievalControls()),
|
|
}
|
|
)
|
|
|
|
@classmethod
|
|
def registry_metadata(cls) -> BaseToolRegistryMetadata:
|
|
"""Return validated registry/runtime metadata for this subclass."""
|
|
return BaseToolRegistryMetadata.model_validate(
|
|
{
|
|
"surfaces": getattr(cls, "surfaces", ("investigation",)),
|
|
"tags": tuple(getattr(cls, "tags", ())),
|
|
"parallel_safe": getattr(cls, "parallel_safe", True),
|
|
}
|
|
)
|
|
|
|
def __call__(self, **kwargs: Any) -> dict[str, Any]:
|
|
from core.tool_framework.telemetry import invoke_tool
|
|
|
|
return invoke_tool(self.run, name=self.name, source=str(self.source), kwargs=kwargs) # type: ignore[attr-defined, no-any-return]
|
|
|
|
def is_available(self, _sources: dict[str, dict]) -> bool:
|
|
"""Return True when required data sources are present.
|
|
|
|
Override per tool. Default allows the tool to always run.
|
|
"""
|
|
return True
|
|
|
|
def extract_params(self, _sources: dict[str, dict]) -> dict[str, Any]:
|
|
"""Extract the kwargs to pass to ``run()`` from the available sources.
|
|
|
|
Override per tool. Default returns an empty dict.
|
|
"""
|
|
return {}
|