Files
rengine/toolbox/runner.py
T
Yogesh OjhaandClaude Opus 5 00623281c7 feat(toolbox): verdict-first results, estate diffs and one input that routes
Three changes to how a result is shaped.

A tool now leads with a hero: headline, identity mark, the one number that
matters, a meter and state marks. WHOIS opens on days to expiry, CVE on
exploitation likelihood against the population, HTTP on the status code,
DNS on the address count, subdomains on how many are not in inventory. The
record follows underneath in a quieter register.

Every result is diffed against what reNgine already holds, which is the
reason to run these here rather than in a browser tab: an AS reports the
hosts it carries in this project, a CVE its findings, a registrant the
other domains that share it, a subdomain sweep how many names are already
known. `toolbox/estate.py` is the one place those reads live.

Values are chainable. A nameserver, address, CNAME or AS number in any
result carries a `lookup`, so an investigation runs WHOIS to nameserver to
DNS to address to AS without retyping.

`POST /toolbox/lookup` classifies a raw value and runs every tool that
answers it without being asked. A domain runs DNS and WHOIS and offers the
HTTP probe and the subdomain sweep; `Tool.accepts` and `Tool.auto` are the
declarations behind it, and the registry rejects a tool naming a
`value_field` its Input has no field for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtRcxFCPSYxW5sXam3SUWB
2026-09-09 13:00:24 +05:30

97 lines
3.0 KiB
Python

"""The one place a tool result becomes a stored run."""
from __future__ import annotations
from datetime import datetime
from pydantic import ValidationError
from shared.definitions.toolbox import Block, BlockKind, RunStatus, ToolRunRead
from shared.utils.datetime import utc_now
from shared.utils.text import strip_control
from toolbox.base import ToolError, ToolInput, ToolOutcome
from toolbox.registry import ToolSpec
MAX_ERROR_LENGTH = 400
QUEUE_DEADLINE_SECONDS = 120
RUN_DEADLINE_SECONDS = 420
def validate(spec: ToolSpec, payload: dict) -> ToolInput:
try:
return spec.tool_cls.Input.model_validate(payload)
except ValidationError as exc:
raise ToolError(_readable(exc)) from exc
def _readable(exc: ValidationError) -> str:
parts = []
for error in exc.errors()[:3]:
field = ".".join(str(p) for p in error.get("loc", ()) if p != "body")
message = error.get("msg", "is invalid")
parts.append(f"{field}: {message}" if field else message)
return "; ".join(parts) or "The input is not valid."
def _elapsed(run: ToolRunRead) -> int:
start = run.started_at or run.queued_at
try:
began = datetime.fromisoformat(start)
except (TypeError, ValueError):
return 0
return max(0, int((utc_now() - began).total_seconds() * 1000))
_SELF_CARRYING = frozenset({BlockKind.HERO.value, BlockKind.IMAGE.value})
def _worth_showing(block: Block) -> bool:
"""An empty block with no empty-state text is not rendered."""
if block.kind in _SELF_CARRYING:
return bool(block.headline or block.src)
if block.facts or block.rows or block.tags or block.text:
return True
return bool(block.empty)
def _age(stamp: str | None) -> float:
try:
return (utc_now() - datetime.fromisoformat(stamp or "")).total_seconds()
except (TypeError, ValueError):
return 0.0
def expire(run: ToolRunRead) -> ToolRunRead:
"""A run past its deadline is recorded as failed rather than left pending."""
if (
run.status == RunStatus.QUEUED.value
and _age(run.queued_at) > QUEUE_DEADLINE_SECONDS
):
return fail(run, "No worker accepted this run.")
if (
run.status == RunStatus.RUNNING.value
and _age(run.started_at) > RUN_DEADLINE_SECONDS
):
return fail(run, "The worker stopped reporting on this run.")
return run
def complete(run: ToolRunRead, outcome: ToolOutcome) -> ToolRunRead:
run.status = RunStatus.COMPLETED.value
run.summary = outcome.summary
run.blocks = [b for b in outcome.blocks if _worth_showing(b)]
run.caveats = outcome.caveats
run.pivot = outcome.pivot
run.raw = outcome.raw
run.finished_at = utc_now().isoformat()
run.duration_ms = _elapsed(run)
return run
def fail(run: ToolRunRead, message: str) -> ToolRunRead:
run.status = RunStatus.FAILED.value
run.error = strip_control(str(message))[:MAX_ERROR_LENGTH] or "The run failed."
run.finished_at = utc_now().isoformat()
run.duration_ms = _elapsed(run)
return run