mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-20 23:31:31 +02:00
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
97 lines
3.0 KiB
Python
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
|