mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-10 16:24:02 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9b99c1fa8 | |||
| a8fd33a758 | |||
| 7346129d0e | |||
| eb8f39f84e |
@@ -2276,12 +2276,14 @@ async def agent_tool_manifest(request: Request):
|
||||
async def api_capabilities(request: Request):
|
||||
"""Return full API manifest so the agent knows every available endpoint."""
|
||||
from services.openclaw_channel import READ_COMMANDS, WRITE_COMMANDS, detect_tier
|
||||
from services.openclaw_routing import routing_manifest
|
||||
from services.config import get_settings
|
||||
tier = detect_tier()
|
||||
access_tier = str(get_settings().OPENCLAW_ACCESS_TIER or "restricted").strip().lower()
|
||||
return {
|
||||
"ok": True,
|
||||
"version": "0.9.82",
|
||||
"routing": routing_manifest(),
|
||||
"auth": {
|
||||
"method": "HMAC-SHA256",
|
||||
"headers": ["X-SB-Timestamp", "X-SB-Nonce", "X-SB-Signature"],
|
||||
@@ -2397,8 +2399,16 @@ async def api_capabilities(request: Request):
|
||||
"description": "Compact server-side ship search by MMSI/IMO/name/query, including yacht-owner enrichment.",
|
||||
},
|
||||
"find_entity": {
|
||||
"args": {"query": "str (optional)", "entity_type": "aircraft|ship|person|event|infrastructure (optional)", "callsign": "str (optional)", "registration": "str (optional)", "icao24": "str (optional)", "mmsi": "str (optional)", "imo": "str (optional)", "name": "str (optional)", "owner": "str (optional)", "layers": "list[str] (optional)", "limit": "int (default 10)"},
|
||||
"description": "Exact-first resolver for planes, ships, operators, callsigns, registrations, MMSI/IMO, and named entities. Use before tracking to avoid fuzzy prompt matching.",
|
||||
"args": {"query": "str (optional)", "entity_type": "aircraft|ship|person|event|infrastructure (optional)", "callsign": "str (optional)", "registration": "str (optional)", "icao24": "str (optional)", "mmsi": "str (optional)", "imo": "str (optional)", "name": "str (optional)", "owner": "str (optional)", "layers": "list[str] (optional)", "limit": "int (default 10)", "fallback_search": "bool (default false)", "confirm_fuzzy": "bool (alias for fallback_search)"},
|
||||
"description": "Exact-first resolver for planes, ships, operators, callsigns, registrations, MMSI/IMO, and named entities. Skips fuzzy search unless fallback_search=true or no exact match.",
|
||||
},
|
||||
"route_query": {
|
||||
"args": {"text": "str", "lat": "float (optional)", "lng": "float (optional)", "radius_km": "float (default 50)", "compact": "bool (default true)"},
|
||||
"description": "Deterministic intent router — returns recommended fast command, alternates, and latency estimate. Preferred entry for natural-language reads.",
|
||||
},
|
||||
"run_playbook": {
|
||||
"args": {"name": "str", "query": "str (optional)", "lat": "float (optional)", "lng": "float (optional)"},
|
||||
"description": "Execute a named batch plan (hot_snapshot, morning_brief, monitor_heartbeat, track_snapshot, area_brief, entity_recon).",
|
||||
},
|
||||
"correlate_entity": {
|
||||
"args": {"query": "str (optional)", "entity_type": "str (optional)", "callsign": "str (optional)", "registration": "str (optional)", "icao24": "str (optional)", "mmsi": "str (optional)", "imo": "str (optional)", "name": "str (optional)", "owner": "str (optional)", "radius_km": "float (default 100)", "limit": "int (default 10)"},
|
||||
@@ -2578,7 +2588,8 @@ async def api_capabilities(request: Request):
|
||||
"layers are serialized, unchanged layers transfer zero bytes. The client tracks versions "
|
||||
"automatically from SSE events and previous responses. "
|
||||
"3) Pass compact=true on every read command for compressed_v1 responses (~60-90% smaller). "
|
||||
"4) Use targeted commands first (find_flights, search_telemetry, entities_near). "
|
||||
"4) Use route_query / find_entity / run_playbook before search_telemetry. "
|
||||
"Expensive commands require confirm_expensive=true. "
|
||||
"Reserve get_telemetry/get_slow_telemetry for rare full-context pulls.",
|
||||
"pins": "Pins are server-side, NOT localStorage. Use place_pin command or POST /api/ai/pins. The agent can place and delete pins.",
|
||||
"tracking": "To track a specific aircraft without polling: use add_watch with track_callsign or track_registration. Over SSE, you'll get instant push alerts.",
|
||||
|
||||
@@ -87,6 +87,9 @@ READ_COMMANDS = frozenset({
|
||||
"osint_lookup",
|
||||
"osint_tools",
|
||||
"entity_expand",
|
||||
# Agent routing helpers
|
||||
"route_query",
|
||||
"run_playbook",
|
||||
})
|
||||
|
||||
WRITE_COMMANDS = frozenset({
|
||||
@@ -643,6 +646,19 @@ def _compact_query_result(result: Any) -> Any:
|
||||
# Command dispatcher
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _expensive_gate(cmd: str, args: dict[str, Any]) -> dict[str, Any] | None:
|
||||
from services.openclaw_routing import EXPENSIVE_GATE_MESSAGE, requires_expensive_confirm
|
||||
|
||||
if requires_expensive_confirm(cmd, args):
|
||||
return {
|
||||
"ok": False,
|
||||
"detail": EXPENSIVE_GATE_MESSAGE,
|
||||
"code": "expensive_command_blocked",
|
||||
"hint": "route_query",
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def _dispatch_command(cmd: str, args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Route a command to the appropriate AI Intel function.
|
||||
|
||||
@@ -650,6 +666,43 @@ def _dispatch_command(cmd: str, args: dict[str, Any]) -> dict[str, Any]:
|
||||
Commands run in an isolated thread (via _execute_command) so they
|
||||
do not need or touch the caller's event loop.
|
||||
"""
|
||||
blocked = _expensive_gate(cmd, args)
|
||||
if blocked is not None:
|
||||
return blocked
|
||||
|
||||
if cmd == "route_query":
|
||||
from services.openclaw_routing import route_query
|
||||
|
||||
result = route_query(
|
||||
text=str(args.get("text", "") or args.get("query", "") or ""),
|
||||
lat=args.get("lat"),
|
||||
lng=args.get("lng"),
|
||||
radius_km=float(args.get("radius_km", 50) or 50),
|
||||
compact=bool(args.get("compact", True)),
|
||||
)
|
||||
return {"ok": True, "data": result}
|
||||
|
||||
if cmd == "run_playbook":
|
||||
from services.openclaw_routing import plan_playbook
|
||||
|
||||
plan = plan_playbook(str(args.get("name", "") or args.get("playbook", "")), args)
|
||||
if not plan.get("ok"):
|
||||
return plan
|
||||
batch_results: list[dict[str, Any]] = []
|
||||
for item in plan.get("batch", []):
|
||||
inner_cmd = str(item.get("cmd", "")).strip().lower()
|
||||
inner_args = item.get("args") or {}
|
||||
inner_result = _dispatch_command(inner_cmd, inner_args)
|
||||
batch_results.append({"cmd": inner_cmd, **inner_result})
|
||||
return {
|
||||
"ok": True,
|
||||
"data": {
|
||||
"playbook": plan.get("playbook"),
|
||||
"description": plan.get("description", ""),
|
||||
"results": batch_results,
|
||||
},
|
||||
}
|
||||
|
||||
if cmd == "get_telemetry":
|
||||
from services.telemetry import get_cached_telemetry_refs
|
||||
data = get_cached_telemetry_refs()
|
||||
@@ -731,6 +784,7 @@ def _dispatch_command(cmd: str, args: dict[str, Any]) -> dict[str, Any]:
|
||||
owner=str(args.get("owner", "") or args.get("operator", "") or ""),
|
||||
layers=args.get("layers") if isinstance(args.get("layers"), (list, tuple)) else None,
|
||||
limit=args.get("limit", 10),
|
||||
fallback_search=bool(args.get("fallback_search") or args.get("confirm_fuzzy")),
|
||||
)
|
||||
if _wants_compact(args):
|
||||
compact = dict(result)
|
||||
@@ -1092,6 +1146,7 @@ def _dispatch_command(cmd: str, args: dict[str, Any]) -> dict[str, Any]:
|
||||
owner=str(args.get("owner", "") or args.get("operator", "") or ""),
|
||||
layers=args.get("layers") if isinstance(args.get("layers"), (list, tuple)) else None,
|
||||
limit=5,
|
||||
fallback_search=True,
|
||||
)
|
||||
best = lookup.get("best_match") if isinstance(lookup.get("best_match"), dict) else {}
|
||||
group = str(best.get("group", "") or entity_type).lower()
|
||||
|
||||
@@ -0,0 +1,500 @@
|
||||
"""Deterministic OpenClaw routing — intent → fastest command.
|
||||
|
||||
Keeps expensive fuzzy scans and full-layer dumps out of the default agent path.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
EXPENSIVE_COMMANDS = frozenset({
|
||||
"search_telemetry",
|
||||
"get_telemetry",
|
||||
"get_slow_telemetry",
|
||||
"get_report",
|
||||
})
|
||||
|
||||
EXPENSIVE_GATE_MESSAGE = (
|
||||
"expensive command blocked — use route_query, find_entity, run_playbook, or targeted reads. "
|
||||
"Pass confirm_expensive=true only when fuzzy search or full dumps are intentional."
|
||||
)
|
||||
|
||||
LATENCY_TIER_MS: dict[str, int] = {
|
||||
"channel_status": 5,
|
||||
"route_query": 5,
|
||||
"get_summary": 10,
|
||||
"what_changed": 15,
|
||||
"search_news": 15,
|
||||
"find_flights": 25,
|
||||
"find_ships": 25,
|
||||
"find_entity": 30,
|
||||
"entities_near": 30,
|
||||
"brief_area": 30,
|
||||
"get_layer_slice": 50,
|
||||
"correlate_entity": 15,
|
||||
"entity_expand": 40,
|
||||
"osint_lookup": 200,
|
||||
"run_playbook": 120,
|
||||
"search_telemetry": 8000,
|
||||
"get_telemetry": 3500,
|
||||
"get_slow_telemetry": 1500,
|
||||
"get_report": 5000,
|
||||
}
|
||||
|
||||
RE_N_NUMBER = re.compile(r"\bN\d{1,5}[A-Z]{0,2}\b", re.I)
|
||||
RE_CALLSIGN = re.compile(r"\b[A-Z]{2,4}\d{1,4}[A-Z]?\b")
|
||||
RE_MMSI = re.compile(r"\b\d{9}\b")
|
||||
RE_CVE = re.compile(r"\bCVE-\d{4}-\d+\b", re.I)
|
||||
RE_IPV4 = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
|
||||
RE_DOMAIN = re.compile(
|
||||
r"\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z]{2,})\b",
|
||||
re.I,
|
||||
)
|
||||
|
||||
KNOWN_CALLSIGNS = frozenset({
|
||||
"AF1", "AF2", "EXEC1", "EXEC2", "SAM", "STALK52", "SPAR19", "SPAR20",
|
||||
})
|
||||
|
||||
PLAYBOOKS: dict[str, dict[str, Any]] = {
|
||||
"hot_snapshot": {
|
||||
"description": "Summary + hot layers + what changed (one batch)",
|
||||
"batch": [
|
||||
{"cmd": "get_summary", "args": {"compact": True}},
|
||||
{
|
||||
"cmd": "get_layer_slice",
|
||||
"args": {
|
||||
"layers": [
|
||||
"news",
|
||||
"telegram_osint",
|
||||
"military_flights",
|
||||
"private_jets",
|
||||
"earthquakes",
|
||||
],
|
||||
"limit_per_layer": 10,
|
||||
"compact": True,
|
||||
},
|
||||
},
|
||||
{"cmd": "what_changed", "args": {"compact": True}},
|
||||
],
|
||||
},
|
||||
"status_check": {
|
||||
"description": "Channel health + layer counts",
|
||||
"batch": [
|
||||
{"cmd": "channel_status", "args": {}},
|
||||
{"cmd": "get_summary", "args": {"compact": True}},
|
||||
],
|
||||
},
|
||||
"morning_brief": {
|
||||
"description": "Operator morning digest layers",
|
||||
"batch": [
|
||||
{"cmd": "get_summary", "args": {"compact": True}},
|
||||
{"cmd": "what_changed", "args": {"compact": True}},
|
||||
{
|
||||
"cmd": "get_layer_slice",
|
||||
"args": {
|
||||
"layers": [
|
||||
"news",
|
||||
"telegram_osint",
|
||||
"gdelt",
|
||||
"earthquakes",
|
||||
"crowdthreat",
|
||||
"military_flights",
|
||||
],
|
||||
"limit_per_layer": 15,
|
||||
"compact": True,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"monitor_heartbeat": {
|
||||
"description": "Low-latency monitor poll (replaces full telemetry pull)",
|
||||
"batch": [
|
||||
{"cmd": "what_changed", "args": {"compact": True}},
|
||||
{
|
||||
"cmd": "get_layer_slice",
|
||||
"args": {
|
||||
"layers": [
|
||||
"military_flights",
|
||||
"ships",
|
||||
"earthquakes",
|
||||
"liveuamap",
|
||||
"crowdthreat",
|
||||
"uap_sightings",
|
||||
"firms_fires",
|
||||
"gps_jamming",
|
||||
"wastewater",
|
||||
],
|
||||
"limit_per_layer": 200,
|
||||
"compact": True,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def routing_manifest() -> dict[str, Any]:
|
||||
"""Machine-readable routing hints for /api/ai/capabilities."""
|
||||
return {
|
||||
"default_read": "find_entity",
|
||||
"preferred_entry": "route_query",
|
||||
"client_wrapper": "ShadowBrokerClient.ask",
|
||||
"batch_playbook": "run_playbook",
|
||||
"last_resort": "search_telemetry",
|
||||
"expensive_commands": sorted(EXPENSIVE_COMMANDS),
|
||||
"latency_tier_ms": LATENCY_TIER_MS,
|
||||
"anti_patterns": [
|
||||
"search_telemetry for known tail numbers, callsigns, owners, or MMSI",
|
||||
"get_telemetry for routine reads — use get_layer_slice or run_playbook hot_snapshot",
|
||||
"sequential send_command loops — use send_batch or run_playbook",
|
||||
"/api/health for liveness — use channel_status",
|
||||
"empty layers: [] on get_layer_slice — pass explicit layer names",
|
||||
],
|
||||
"recipes": [
|
||||
{
|
||||
"intent": "natural language question",
|
||||
"use": "route_query → recommended cmd, or ShadowBrokerClient.ask()",
|
||||
},
|
||||
{
|
||||
"intent": "known person/aircraft",
|
||||
"use": "find_entity(query=...) or find_flights(owner=...)",
|
||||
},
|
||||
{
|
||||
"intent": "news / telegram topic",
|
||||
"use": "search_news(query=...)",
|
||||
},
|
||||
{
|
||||
"intent": "near a point",
|
||||
"use": "entities_near or brief_area",
|
||||
},
|
||||
{
|
||||
"intent": "hot snapshot",
|
||||
"use": "run_playbook(name=hot_snapshot)",
|
||||
},
|
||||
],
|
||||
"playbooks": {
|
||||
name: {"description": spec.get("description", "")}
|
||||
for name, spec in PLAYBOOKS.items()
|
||||
},
|
||||
"agent_surface": {
|
||||
"primary": ["ask", "send_batch", "channel_status"],
|
||||
"writes": [
|
||||
"place_pin",
|
||||
"add_watch",
|
||||
"inject_data",
|
||||
"place_analysis_zone",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def requires_expensive_confirm(cmd: str, args: dict[str, Any] | None) -> bool:
|
||||
if cmd not in EXPENSIVE_COMMANDS:
|
||||
return False
|
||||
if isinstance(args, dict) and args.get("confirm_expensive") is True:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _compact_args(args: dict[str, Any], *, compact: bool) -> dict[str, Any]:
|
||||
out = dict(args)
|
||||
if compact and "compact" not in out:
|
||||
out["compact"] = True
|
||||
return out
|
||||
|
||||
|
||||
def _estimate_ms(cmd: str) -> int:
|
||||
return int(LATENCY_TIER_MS.get(cmd, 100))
|
||||
|
||||
|
||||
def _news_query(text: str) -> str:
|
||||
cleaned = text
|
||||
for prefix in (
|
||||
"news about",
|
||||
"news on",
|
||||
"telegram",
|
||||
"headlines about",
|
||||
"headlines on",
|
||||
"latest on",
|
||||
"search news for",
|
||||
):
|
||||
if cleaned.lower().startswith(prefix):
|
||||
cleaned = cleaned[len(prefix):].strip()
|
||||
return cleaned.strip(" ?.")
|
||||
|
||||
|
||||
def route_query(
|
||||
text: str = "",
|
||||
*,
|
||||
lat: float | None = None,
|
||||
lng: float | None = None,
|
||||
radius_km: float = 50,
|
||||
compact: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Map natural-language intent to the fastest command (no LLM)."""
|
||||
raw = str(text or "").strip()
|
||||
lowered = raw.lower()
|
||||
avoid = ["search_telemetry", "get_telemetry", "get_slow_telemetry"]
|
||||
alternates: list[dict[str, Any]] = []
|
||||
|
||||
if not raw and lat is not None and lng is not None:
|
||||
recommended = {
|
||||
"cmd": "brief_area",
|
||||
"args": _compact_args(
|
||||
{"lat": lat, "lng": lng, "radius_km": radius_km},
|
||||
compact=compact,
|
||||
),
|
||||
}
|
||||
return {
|
||||
"intent": "area_brief",
|
||||
"recommended": recommended,
|
||||
"alternates": [{"cmd": "entities_near", "args": recommended["args"]}],
|
||||
"avoid": avoid,
|
||||
"estimated_ms": _estimate_ms("brief_area"),
|
||||
}
|
||||
|
||||
if not raw:
|
||||
recommended = {"cmd": "get_summary", "args": _compact_args({}, compact=compact)}
|
||||
return {
|
||||
"intent": "discovery",
|
||||
"recommended": recommended,
|
||||
"alternates": [{"cmd": "channel_status", "args": {}}],
|
||||
"avoid": avoid,
|
||||
"estimated_ms": _estimate_ms("get_summary"),
|
||||
}
|
||||
|
||||
cve_match = RE_CVE.search(raw)
|
||||
if cve_match:
|
||||
recommended = {
|
||||
"cmd": "osint_lookup",
|
||||
"args": _compact_args({"tool": "cve", "cve": cve_match.group(0).upper()}, compact=compact),
|
||||
}
|
||||
return _route_result("cve_lookup", recommended, avoid, alternates)
|
||||
|
||||
ip_match = RE_IPV4.search(raw)
|
||||
if ip_match and ("ip" in lowered or "address" in lowered or lowered.count(".") >= 3):
|
||||
recommended = {
|
||||
"cmd": "osint_lookup",
|
||||
"args": _compact_args({"tool": "ip", "ip": ip_match.group(0)}, compact=compact),
|
||||
}
|
||||
alternates.append({"cmd": "entity_expand", "args": {"type": "ip", "id": ip_match.group(0)}})
|
||||
return _route_result("ip_lookup", recommended, avoid, alternates)
|
||||
|
||||
if "whois" in lowered or ("dns" in lowered and RE_DOMAIN.search(raw)):
|
||||
domain = (RE_DOMAIN.search(raw) or re.search(r"\b([a-z0-9-]+\.[a-z]{2,})\b", raw, re.I))
|
||||
tool = "whois" if "whois" in lowered else "dns"
|
||||
domain_value = domain.group(0) if domain else raw
|
||||
recommended = {
|
||||
"cmd": "osint_lookup",
|
||||
"args": _compact_args({"tool": tool, "domain": domain_value}, compact=compact),
|
||||
}
|
||||
return _route_result("domain_lookup", recommended, avoid, alternates)
|
||||
|
||||
if "sanction" in lowered or "ofac" in lowered:
|
||||
recommended = {
|
||||
"cmd": "osint_lookup",
|
||||
"args": _compact_args({"tool": "sanctions", "query": raw}, compact=compact),
|
||||
}
|
||||
return _route_result("sanctions_lookup", recommended, avoid, alternates)
|
||||
|
||||
mmsi_match = RE_MMSI.search(raw)
|
||||
if mmsi_match and any(k in lowered for k in ("mmsi", "ship", "vessel", "yacht", "boat", "maritime")):
|
||||
recommended = {
|
||||
"cmd": "find_ships",
|
||||
"args": _compact_args({"mmsi": mmsi_match.group(0)}, compact=compact),
|
||||
}
|
||||
alternates.append({"cmd": "find_entity", "args": {"mmsi": mmsi_match.group(0), "entity_type": "ship"}})
|
||||
return _route_result("maritime_identifier", recommended, avoid, alternates)
|
||||
|
||||
n_match = RE_N_NUMBER.search(raw)
|
||||
if n_match:
|
||||
reg = n_match.group(0).upper()
|
||||
recommended = {
|
||||
"cmd": "find_flights",
|
||||
"args": _compact_args({"registration": reg}, compact=compact),
|
||||
}
|
||||
alternates.append({"cmd": "find_entity", "args": {"registration": reg, "entity_type": "aircraft"}})
|
||||
return _route_result("tail_number", recommended, avoid, alternates)
|
||||
|
||||
# callsign tokens
|
||||
tokens = re.findall(r"\b[A-Z0-9]{2,8}\b", raw.upper())
|
||||
for token in tokens:
|
||||
if token in KNOWN_CALLSIGNS or RE_CALLSIGN.fullmatch(token):
|
||||
recommended = {
|
||||
"cmd": "find_flights",
|
||||
"args": _compact_args({"callsign": token}, compact=compact),
|
||||
}
|
||||
alternates.append({"cmd": "find_entity", "args": {"callsign": token, "entity_type": "aircraft"}})
|
||||
return _route_result("callsign", recommended, avoid, alternates)
|
||||
|
||||
if any(k in lowered for k in ("news", "telegram", "headline", "headlines", "gdelt")):
|
||||
recommended = {
|
||||
"cmd": "search_news",
|
||||
"args": _compact_args({"query": _news_query(raw), "limit": 10}, compact=compact),
|
||||
}
|
||||
alternates.append({
|
||||
"cmd": "get_layer_slice",
|
||||
"args": {"layers": ["telegram_osint", "news"], "limit_per_layer": 10, "compact": compact},
|
||||
})
|
||||
return _route_result("news_search", recommended, avoid, alternates)
|
||||
|
||||
if lat is not None and lng is not None and any(
|
||||
k in lowered for k in ("near", "around", "within", "radius", "brief", "aoi")
|
||||
):
|
||||
recommended = {
|
||||
"cmd": "brief_area",
|
||||
"args": _compact_args(
|
||||
{"lat": lat, "lng": lng, "radius_km": radius_km, "query": raw},
|
||||
compact=compact,
|
||||
),
|
||||
}
|
||||
alternates.append({
|
||||
"cmd": "entities_near",
|
||||
"args": {"lat": lat, "lng": lng, "radius_km": radius_km, "compact": compact},
|
||||
})
|
||||
return _route_result("area_brief", recommended, avoid, alternates)
|
||||
|
||||
if any(k in lowered for k in ("what changed", "updates", "delta", "since last")):
|
||||
recommended = {"cmd": "what_changed", "args": _compact_args({}, compact=compact)}
|
||||
return _route_result("incremental_poll", recommended, avoid, alternates)
|
||||
|
||||
if any(k in lowered for k in ("summary", "status", "layers populated", "what data")):
|
||||
recommended = {"cmd": "get_summary", "args": _compact_args({}, compact=compact)}
|
||||
alternates.append({"cmd": "channel_status", "args": {}})
|
||||
return _route_result("discovery", recommended, avoid, alternates)
|
||||
|
||||
if any(k in lowered for k in ("recon", "whois", "dns lookup", "cve", "mac address")):
|
||||
recommended = {
|
||||
"cmd": "osint_tools",
|
||||
"args": {},
|
||||
}
|
||||
return _route_result("recon_discovery", recommended, avoid, alternates)
|
||||
|
||||
entity_type = ""
|
||||
if any(k in lowered for k in ("ship", "vessel", "yacht", "boat", "maritime", "carrier")):
|
||||
entity_type = "ship"
|
||||
elif any(k in lowered for k in ("jet", "plane", "flight", "aircraft", "helicopter", "tail")):
|
||||
entity_type = "aircraft"
|
||||
|
||||
owner_hint = ""
|
||||
if any(k in lowered for k in ("owner", "operated by", "'s jet", "'s yacht", "belongs to")):
|
||||
owner_hint = raw
|
||||
for phrase in ("where is", "find", "track", "locate", "jet", "yacht", "plane", "flight", "ship"):
|
||||
owner_hint = re.sub(rf"\b{phrase}\b", "", owner_hint, flags=re.I).strip()
|
||||
|
||||
entity_args: dict[str, Any] = {"query": raw, "compact": compact}
|
||||
if entity_type:
|
||||
entity_args["entity_type"] = entity_type
|
||||
if owner_hint and len(owner_hint) >= 3:
|
||||
entity_args["owner"] = owner_hint
|
||||
|
||||
recommended = {
|
||||
"cmd": "find_entity",
|
||||
"args": _compact_args(entity_args, compact=compact),
|
||||
}
|
||||
alternates = [
|
||||
{"cmd": "search_news", "args": {"query": raw, "limit": 10, "compact": compact}},
|
||||
]
|
||||
if any(k in lowered for k in ("near", "around")):
|
||||
alternates.append({
|
||||
"cmd": "search_telemetry",
|
||||
"args": {"query": raw, "limit": 10, "confirm_expensive": True, "compact": compact},
|
||||
})
|
||||
|
||||
return _route_result("entity_lookup", recommended, avoid, alternates)
|
||||
|
||||
|
||||
def _route_result(
|
||||
intent: str,
|
||||
recommended: dict[str, Any],
|
||||
avoid: list[str],
|
||||
alternates: list[dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
cmd = str(recommended.get("cmd", ""))
|
||||
return {
|
||||
"intent": intent,
|
||||
"recommended": recommended,
|
||||
"alternates": alternates,
|
||||
"avoid": avoid,
|
||||
"estimated_ms": _estimate_ms(cmd),
|
||||
}
|
||||
|
||||
|
||||
def plan_playbook(name: str, args: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""Resolve a named playbook to a command batch."""
|
||||
playbook = str(name or "").strip().lower()
|
||||
params = dict(args or {})
|
||||
if not playbook:
|
||||
return {"ok": False, "detail": "playbook name required"}
|
||||
|
||||
if playbook == "track_snapshot":
|
||||
query = str(params.get("query", "") or params.get("name", "") or "").strip()
|
||||
if not query:
|
||||
return {"ok": False, "detail": "track_snapshot requires query"}
|
||||
return {
|
||||
"ok": True,
|
||||
"playbook": playbook,
|
||||
"description": "Resolve entity for tracking",
|
||||
"batch": [
|
||||
{
|
||||
"cmd": "find_entity",
|
||||
"args": {
|
||||
"query": query,
|
||||
"entity_type": params.get("entity_type", ""),
|
||||
"fallback_search": True,
|
||||
"compact": True,
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
if playbook == "area_brief":
|
||||
lat = params.get("lat")
|
||||
lng = params.get("lng")
|
||||
if lat is None or lng is None:
|
||||
return {"ok": False, "detail": "area_brief requires lat and lng"}
|
||||
return {
|
||||
"ok": True,
|
||||
"playbook": playbook,
|
||||
"description": "Brief an area of interest",
|
||||
"batch": [
|
||||
{
|
||||
"cmd": "brief_area",
|
||||
"args": {
|
||||
"lat": lat,
|
||||
"lng": lng,
|
||||
"radius_km": params.get("radius_km", 50),
|
||||
"query": params.get("query", ""),
|
||||
"compact": True,
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
if playbook == "entity_recon":
|
||||
query = str(params.get("query", "") or params.get("ip", "") or "").strip()
|
||||
ip_match = RE_IPV4.search(query)
|
||||
if not ip_match:
|
||||
return {"ok": False, "detail": "entity_recon requires an IP in query"}
|
||||
return {
|
||||
"ok": True,
|
||||
"playbook": playbook,
|
||||
"description": "IP recon + entity graph",
|
||||
"batch": [
|
||||
{"cmd": "osint_lookup", "args": {"tool": "ip", "ip": ip_match.group(0), "compact": True}},
|
||||
{"cmd": "entity_expand", "args": {"type": "ip", "id": ip_match.group(0)}},
|
||||
],
|
||||
}
|
||||
|
||||
spec = PLAYBOOKS.get(playbook)
|
||||
if not spec:
|
||||
known = sorted(PLAYBOOKS) + ["track_snapshot", "area_brief", "entity_recon"]
|
||||
return {"ok": False, "detail": f"unknown playbook: {playbook}", "known": known}
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"playbook": playbook,
|
||||
"description": spec.get("description", ""),
|
||||
"batch": [dict(item) for item in spec.get("batch", [])],
|
||||
}
|
||||
@@ -1549,11 +1549,13 @@ def find_entity(
|
||||
owner: str = "",
|
||||
layers: list[str] | tuple[str, ...] | None = None,
|
||||
limit: int = 10,
|
||||
fallback_search: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Find a named entity across aircraft, maritime, and general telemetry.
|
||||
|
||||
This is an intent-level lookup for agents. It tries high-precision
|
||||
aircraft/ship fields first, then falls back to the universal search index.
|
||||
aircraft/ship fields first, then optionally falls back to the universal
|
||||
search index only when ``fallback_search`` is True (opt-in fuzzy scan).
|
||||
"""
|
||||
effective_query = str(query or name or owner or callsign or registration or icao24 or mmsi or imo or "").strip()
|
||||
if not effective_query:
|
||||
@@ -1628,16 +1630,18 @@ def find_entity(
|
||||
seen.add(key)
|
||||
results.append(normalized)
|
||||
|
||||
search_layers = requested_layers or _entity_layers_for_type(entity_type)
|
||||
search_result = search_telemetry(query=effective_query, layers=search_layers, limit=limit)
|
||||
if search_result.get("results"):
|
||||
strategies.append("universal_index")
|
||||
for item in search_result.get("results") or []:
|
||||
normalized = _normalize_entity_result(item)
|
||||
key = _entity_key(normalized)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
results.append(normalized)
|
||||
search_layers = list(requested_layers or _entity_layers_for_type(entity_type) or [])
|
||||
search_result: dict[str, Any] = {"results": [], "searched_layers": search_layers}
|
||||
if fallback_search:
|
||||
search_result = search_telemetry(query=effective_query, layers=search_layers, limit=limit)
|
||||
if search_result.get("results"):
|
||||
strategies.append("universal_index")
|
||||
for item in search_result.get("results") or []:
|
||||
normalized = _normalize_entity_result(item)
|
||||
key = _entity_key(normalized)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
results.append(normalized)
|
||||
|
||||
results.sort(
|
||||
key=lambda item: (
|
||||
|
||||
@@ -466,15 +466,55 @@ def test_find_entity_prioritizes_aircraft_operator_and_callsign(sample_store, mo
|
||||
|
||||
monkeypatch.setattr(telemetry, "get_data_version", lambda: 130)
|
||||
|
||||
by_operator = telemetry.find_entity(query="patriots jet", limit=5)
|
||||
by_operator = telemetry.find_entity(owner="Patriots", limit=5)
|
||||
assert by_operator["best_match"]["group"] == "aircraft"
|
||||
assert by_operator["best_match"]["label"] == "OXE2116"
|
||||
|
||||
fuzzy = telemetry.find_entity(query="patriots jet", limit=5, fallback_search=True)
|
||||
assert fuzzy["best_match"]["group"] == "aircraft"
|
||||
|
||||
by_callsign = telemetry.find_entity(callsign="AF1", entity_type="aircraft", limit=5)
|
||||
assert by_callsign["best_match"]["callsign"] == "AF1"
|
||||
assert by_callsign["best_match"]["alert_operator"] == "POTUS"
|
||||
|
||||
|
||||
def test_find_entity_skips_fuzzy_when_exact_match(sample_store, monkeypatch):
|
||||
import services.telemetry as telemetry
|
||||
|
||||
monkeypatch.setattr(telemetry, "get_data_version", lambda: 200)
|
||||
calls: list[str] = []
|
||||
|
||||
def _fake_search(*_args, **_kwargs):
|
||||
calls.append("search")
|
||||
return {"results": [], "searched_layers": []}
|
||||
|
||||
monkeypatch.setattr(telemetry, "search_telemetry", _fake_search)
|
||||
|
||||
result = telemetry.find_entity(callsign="AF1", entity_type="aircraft", fallback_search=False)
|
||||
assert result["best_match"]["callsign"] == "AF1"
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_find_entity_fuzzy_only_when_fallback_or_empty(sample_store, monkeypatch):
|
||||
import services.telemetry as telemetry
|
||||
|
||||
monkeypatch.setattr(telemetry, "get_data_version", lambda: 201)
|
||||
calls: list[str] = []
|
||||
|
||||
def _fake_search(*_args, **_kwargs):
|
||||
calls.append("search")
|
||||
return {"results": [], "searched_layers": []}
|
||||
|
||||
monkeypatch.setattr(telemetry, "search_telemetry", _fake_search)
|
||||
|
||||
empty = telemetry.find_entity(query="zzzznonexistententity", fallback_search=False)
|
||||
assert empty["best_match"] is None
|
||||
assert calls == []
|
||||
|
||||
telemetry.find_entity(query="zzzznonexistententity", fallback_search=True)
|
||||
assert calls == ["search"]
|
||||
|
||||
|
||||
def test_find_entity_prioritizes_maritime_owner_and_identifiers(sample_store, monkeypatch):
|
||||
import services.telemetry as telemetry
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""OpenClaw routing, playbooks, and expensive-command gate."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from services.openclaw_channel import _dispatch_command
|
||||
from services.openclaw_routing import (
|
||||
EXPENSIVE_COMMANDS,
|
||||
plan_playbook,
|
||||
requires_expensive_confirm,
|
||||
route_query,
|
||||
routing_manifest,
|
||||
)
|
||||
|
||||
|
||||
def test_routing_manifest_has_agent_surface():
|
||||
manifest = routing_manifest()
|
||||
assert manifest["preferred_entry"] == "route_query"
|
||||
assert manifest["client_wrapper"] == "ShadowBrokerClient.ask"
|
||||
assert "search_telemetry" in manifest["expensive_commands"]
|
||||
assert "hot_snapshot" in manifest["playbooks"]
|
||||
|
||||
|
||||
def test_route_query_tail_number():
|
||||
plan = route_query("track N628TS position")
|
||||
assert plan["recommended"]["cmd"] == "find_flights"
|
||||
assert plan["recommended"]["args"]["registration"] == "N628TS"
|
||||
assert "search_telemetry" in plan["avoid"]
|
||||
|
||||
|
||||
def test_route_query_callsign():
|
||||
plan = route_query("where is AF1 right now")
|
||||
assert plan["recommended"]["cmd"] == "find_flights"
|
||||
assert plan["recommended"]["args"]["callsign"] == "AF1"
|
||||
|
||||
|
||||
def test_route_query_news():
|
||||
plan = route_query("telegram news about Iran tanker")
|
||||
assert plan["recommended"]["cmd"] == "search_news"
|
||||
|
||||
|
||||
def test_route_query_cve():
|
||||
plan = route_query("details for CVE-2024-1234")
|
||||
assert plan["recommended"]["cmd"] == "osint_lookup"
|
||||
assert plan["recommended"]["args"]["tool"] == "cve"
|
||||
|
||||
|
||||
def test_route_query_default_entity():
|
||||
plan = route_query("where is the patriots jet")
|
||||
assert plan["recommended"]["cmd"] == "find_entity"
|
||||
assert plan["recommended"]["args"]["query"]
|
||||
|
||||
|
||||
def test_expensive_gate_blocks_search_telemetry():
|
||||
assert requires_expensive_confirm("search_telemetry", {"query": "test"})
|
||||
assert not requires_expensive_confirm(
|
||||
"search_telemetry",
|
||||
{"query": "test", "confirm_expensive": True},
|
||||
)
|
||||
result = _dispatch_command("search_telemetry", {"query": "test"})
|
||||
assert result["ok"] is False
|
||||
assert result.get("code") == "expensive_command_blocked"
|
||||
|
||||
|
||||
def test_expensive_gate_blocks_get_telemetry():
|
||||
result = _dispatch_command("get_telemetry", {})
|
||||
assert result["ok"] is False
|
||||
assert result.get("code") == "expensive_command_blocked"
|
||||
|
||||
|
||||
def test_dispatch_route_query():
|
||||
result = _dispatch_command("route_query", {"text": "news about carrier strike"})
|
||||
assert result["ok"] is True
|
||||
assert result["data"]["recommended"]["cmd"] == "search_news"
|
||||
|
||||
|
||||
def test_dispatch_run_playbook_hot_snapshot():
|
||||
result = _dispatch_command("run_playbook", {"name": "status_check"})
|
||||
assert result["ok"] is True
|
||||
cmds = [item["cmd"] for item in result["data"]["results"]]
|
||||
assert cmds == ["channel_status", "get_summary"]
|
||||
|
||||
|
||||
def test_plan_playbook_track_snapshot_requires_query():
|
||||
plan = plan_playbook("track_snapshot", {})
|
||||
assert plan["ok"] is False
|
||||
plan_ok = plan_playbook("track_snapshot", {"query": "patriots jet"})
|
||||
assert plan_ok["ok"] is True
|
||||
assert plan_ok["batch"][0]["cmd"] == "find_entity"
|
||||
|
||||
|
||||
def test_expensive_commands_set():
|
||||
assert "get_report" in EXPENSIVE_COMMANDS
|
||||
assert "route_query" not in EXPENSIVE_COMMANDS
|
||||
@@ -101,54 +101,17 @@ const BUG_FIXES = [
|
||||
'OpenClaw agents can invoke the Recon panel backends via `osint_lookup` without raw `/api/osint/*` HTTP calls or local-operator browser auth.',
|
||||
];
|
||||
|
||||
const CONTRIBUTORS = [
|
||||
type ChangelogContributor = {
|
||||
name: string;
|
||||
desc: string;
|
||||
pr?: string;
|
||||
};
|
||||
|
||||
const CONTRIBUTORS: ChangelogContributor[] = [
|
||||
{
|
||||
name: 'OSIRIS (simplifaisoul/osiris)',
|
||||
desc: 'MIT-licensed recon stack — adapted for ShadowBroker proxy model (see backend/third_party/osiris/NOTICE.md)',
|
||||
},
|
||||
{
|
||||
name: '@Alienmajik',
|
||||
desc: 'Raspberry Pi 5 support — ARM64 packaging, headless deployment notes, and runtime tuning for Pi-class hardware',
|
||||
},
|
||||
{
|
||||
name: '@wa1id',
|
||||
desc: 'CCTV ingestion fix — fresh SQLite connections per ingest, persistent DB path, startup hydration, cluster clickability',
|
||||
pr: '#92',
|
||||
},
|
||||
{
|
||||
name: '@AlborzNazari',
|
||||
desc: 'Spain DGT + Madrid CCTV sources and STIX 2.1 threat intelligence export endpoint',
|
||||
pr: '#91',
|
||||
},
|
||||
{
|
||||
name: '@adust09',
|
||||
desc: 'Power plants layer, East Asia intel coverage (JSDF bases, ICAO enrichment, Taiwan news sources, military classification)',
|
||||
pr: '#71, #72, #76, #77, #87',
|
||||
},
|
||||
{
|
||||
name: '@Xpirix',
|
||||
desc: 'LocateBar style and interaction improvements',
|
||||
pr: '#78',
|
||||
},
|
||||
{
|
||||
name: '@imqdcr',
|
||||
desc: 'Ship toggle split into 4 categories + stable MMSI/callsign entity IDs for map markers',
|
||||
pr: '#52',
|
||||
},
|
||||
{
|
||||
name: '@csysp',
|
||||
desc: 'Dismissible threat alerts + stable entity IDs for GDELT & News popups + UI declutter',
|
||||
pr: '#48, #61, #63',
|
||||
},
|
||||
{
|
||||
name: '@suranyami',
|
||||
desc: 'Parallel multi-arch Docker builds (11min → 3min) + runtime BACKEND_URL fix',
|
||||
pr: '#35, #44',
|
||||
},
|
||||
{
|
||||
name: '@chr0n1x',
|
||||
desc: 'Kubernetes / Helm chart architecture for high-availability deployments',
|
||||
},
|
||||
];
|
||||
|
||||
export function useChangelog() {
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
'use client';
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { GripHorizontal, Terminal } from 'lucide-react';
|
||||
|
||||
const STORAGE_KEY = 'sb_agent_shell_dims';
|
||||
const SHELL_FONT_PX = 14;
|
||||
const MIN_SHELL_WIDTH = 300;
|
||||
const MIN_SHELL_HEIGHT = 220;
|
||||
const STRETCH_WIDTH_RATIO = 2.15;
|
||||
const STRETCH_MIN_WIDTH = 520;
|
||||
|
||||
type ShellSize = { w: number; h: number };
|
||||
|
||||
function readStoredSize(): ShellSize | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw) as ShellSize;
|
||||
if (
|
||||
typeof parsed?.w === 'number' &&
|
||||
typeof parsed?.h === 'number' &&
|
||||
parsed.w >= MIN_SHELL_WIDTH &&
|
||||
parsed.h >= MIN_SHELL_HEIGHT
|
||||
) {
|
||||
return parsed;
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function writeStoredSize(size: ShellSize) {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(size));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
function clampSize(size: ShellSize, anchorLeft: number): ShellSize {
|
||||
const maxW = Math.max(MIN_SHELL_WIDTH, window.innerWidth - anchorLeft - 12);
|
||||
const maxH = Math.max(MIN_SHELL_HEIGHT, window.innerHeight - 12);
|
||||
return {
|
||||
w: Math.min(Math.max(size.w, MIN_SHELL_WIDTH), maxW),
|
||||
h: Math.min(Math.max(size.h, MIN_SHELL_HEIGHT), maxH),
|
||||
};
|
||||
}
|
||||
|
||||
function defaultStretchedSize(anchor: DOMRect): ShellSize {
|
||||
const stretchedW = Math.max(anchor.width * STRETCH_WIDTH_RATIO, STRETCH_MIN_WIDTH);
|
||||
return clampSize({ w: stretchedW, h: anchor.height }, anchor.left);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
anchorRef: React.RefObject<HTMLElement | null>;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
export default function AgentShellPanel({ anchorRef, active }: Props) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [anchorRect, setAnchorRect] = useState<DOMRect | null>(null);
|
||||
const [size, setSize] = useState<ShellSize>({ w: STRETCH_MIN_WIDTH, h: 360 });
|
||||
const [pos, setPos] = useState({ x: 0, y: 0 });
|
||||
const [userResized, setUserResized] = useState(Boolean(readStoredSize()));
|
||||
const resizeRef = useRef<{
|
||||
edge: 'e' | 's' | 'se';
|
||||
startX: number;
|
||||
startY: number;
|
||||
origW: number;
|
||||
origH: number;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const measureAnchor = useCallback(() => {
|
||||
const el = anchorRef.current;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
setAnchorRect(rect);
|
||||
setPos({ x: rect.left, y: rect.top });
|
||||
|
||||
if (!userResized) {
|
||||
setSize(defaultStretchedSize(rect));
|
||||
return;
|
||||
}
|
||||
|
||||
const stored = readStoredSize();
|
||||
if (stored) {
|
||||
setSize(clampSize(stored, rect.left));
|
||||
} else {
|
||||
setSize(defaultStretchedSize(rect));
|
||||
}
|
||||
}, [anchorRef, userResized]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
measureAnchor();
|
||||
|
||||
const el = anchorRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const observer = new ResizeObserver(() => measureAnchor());
|
||||
observer.observe(el);
|
||||
|
||||
const onWindowChange = () => measureAnchor();
|
||||
window.addEventListener('resize', onWindowChange);
|
||||
window.addEventListener('scroll', onWindowChange, true);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener('resize', onWindowChange);
|
||||
window.removeEventListener('scroll', onWindowChange, true);
|
||||
};
|
||||
}, [active, anchorRef, measureAnchor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active || userResized) return;
|
||||
const el = anchorRef.current;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const base = { w: rect.width, h: rect.height };
|
||||
setSize(base);
|
||||
setPos({ x: rect.left, y: rect.top });
|
||||
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
setSize(defaultStretchedSize(rect));
|
||||
});
|
||||
return () => window.cancelAnimationFrame(frame);
|
||||
}, [active, anchorRef, userResized]);
|
||||
|
||||
const beginResize = (edge: 'e' | 's' | 'se') => (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
resizeRef.current = {
|
||||
edge,
|
||||
startX: event.clientX,
|
||||
startY: event.clientY,
|
||||
origW: size.w,
|
||||
origH: size.h,
|
||||
};
|
||||
|
||||
const onMove = (ev: MouseEvent) => {
|
||||
if (!resizeRef.current) return;
|
||||
const dx = ev.clientX - resizeRef.current.startX;
|
||||
const dy = ev.clientY - resizeRef.current.startY;
|
||||
const { edge: ed, origW, origH } = resizeRef.current;
|
||||
const anchorLeft = anchorRef.current?.getBoundingClientRect().left ?? pos.x;
|
||||
const next: ShellSize = {
|
||||
w: ed === 's' ? origW : origW + dx,
|
||||
h: ed === 'e' ? origH : origH + dy,
|
||||
};
|
||||
setUserResized(true);
|
||||
setSize(clampSize(next, anchorLeft));
|
||||
};
|
||||
|
||||
const onUp = () => {
|
||||
resizeRef.current = null;
|
||||
window.removeEventListener('mousemove', onMove);
|
||||
window.removeEventListener('mouseup', onUp);
|
||||
setSize((current) => {
|
||||
writeStoredSize(current);
|
||||
return current;
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', onMove);
|
||||
window.addEventListener('mouseup', onUp);
|
||||
};
|
||||
|
||||
const snapToStretchedDefault = () => {
|
||||
const el = anchorRef.current;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
setUserResized(false);
|
||||
try {
|
||||
window.localStorage.removeItem(STORAGE_KEY);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
setPos({ x: rect.left, y: rect.top });
|
||||
setSize(defaultStretchedSize(rect));
|
||||
};
|
||||
|
||||
if (!mounted || !active || !anchorRect) {
|
||||
return (
|
||||
<div className="flex-1 min-h-0 flex flex-col items-center justify-center px-4 py-6 text-center border-l-2 border-cyan-800/20">
|
||||
<Terminal size={18} className="text-cyan-400 mb-2" />
|
||||
<div className="text-sm font-mono tracking-[0.2em] text-cyan-300">AGENT SHELL</div>
|
||||
<div className="mt-2 text-[13px] font-mono text-[var(--text-secondary)] leading-relaxed">
|
||||
Expand Mesh Chat to open the local agent shell.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const shell = (
|
||||
<div
|
||||
className="pointer-events-auto z-[250] flex flex-col border border-cyan-800/50 bg-[#05080c]/96 shadow-[0_18px_60px_rgba(0,0,0,0.55),0_0_0_1px_rgba(34,211,238,0.08)] backdrop-blur-sm"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: pos.x,
|
||||
top: pos.y,
|
||||
width: size.w,
|
||||
height: size.h,
|
||||
transition: userResized ? undefined : 'width 180ms ease-out',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 border-b border-cyan-900/40 px-3 py-2 shrink-0 select-none">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Terminal size={13} className="text-cyan-400 shrink-0" />
|
||||
<span className="text-[13px] font-mono tracking-[0.18em] text-cyan-300">AGENT SHELL</span>
|
||||
<span className="hidden sm:inline text-[12px] font-mono text-slate-500 truncate">
|
||||
local CLI · user cwd
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={snapToStretchedDefault}
|
||||
className="px-2 py-1 text-[12px] font-mono tracking-[0.14em] text-cyan-300/80 border border-cyan-800/40 hover:bg-cyan-950/30 transition-colors"
|
||||
title="Reset size to default stretch from Mesh Chat panel"
|
||||
>
|
||||
SNAP
|
||||
</button>
|
||||
<GripHorizontal size={14} className="text-cyan-600/60" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex-1 min-h-0 overflow-auto styled-scrollbar px-3 py-2 font-mono text-cyan-100/90"
|
||||
style={{ fontSize: SHELL_FONT_PX, lineHeight: 1.55 }}
|
||||
>
|
||||
<div className="text-slate-400">ShadowBroker agent shell (PTY wiring next)</div>
|
||||
<div className="text-slate-500 mt-1">Working directory: set your own path in Settings.</div>
|
||||
<div className="mt-3 text-emerald-300/90">$ openclaw</div>
|
||||
<div className="text-slate-500">$ codex</div>
|
||||
<div className="text-slate-500">$ gemini</div>
|
||||
<div className="mt-3 text-cyan-300/80 animate-pulse">█</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-cyan-900/30 px-3 py-1.5 text-[12px] font-mono text-slate-500 shrink-0">
|
||||
Drag right/bottom edges to resize · {Math.round(size.w)}×{Math.round(size.h)}px · {SHELL_FONT_PX}px font
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="absolute top-2 bottom-2 right-0 w-1.5 cursor-e-resize"
|
||||
onMouseDown={beginResize('e')}
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="absolute left-2 right-2 bottom-0 h-1.5 cursor-s-resize"
|
||||
onMouseDown={beginResize('s')}
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="absolute right-0 bottom-0 h-3 w-3 cursor-se-resize"
|
||||
onMouseDown={beginResize('se')}
|
||||
aria-hidden
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-1 min-h-0 flex flex-col items-center justify-center px-3 py-4 text-center border-l-2 border-cyan-800/20">
|
||||
<div className="text-[12px] font-mono tracking-[0.16em] text-cyan-500/80">SHELL ACTIVE</div>
|
||||
<div className="mt-1 text-[13px] font-mono text-[var(--text-secondary)] leading-relaxed">
|
||||
Panel stretched from Mesh Chat. Drag edges on the shell to resize.
|
||||
</div>
|
||||
</div>
|
||||
{createPortal(shell, document.body)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import AgentShellPanel from './AgentShellPanel';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Antenna,
|
||||
@@ -89,6 +90,7 @@ function describeGateCompatReason(reason: string, gateId: string): string {
|
||||
// NO direct trust-mutating imports — all mutations go through the hook.
|
||||
|
||||
const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
const panelBoxRef = useRef<HTMLDivElement>(null);
|
||||
const ctrl = useMeshChatController(props);
|
||||
const {
|
||||
// UI state
|
||||
@@ -398,6 +400,7 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
>
|
||||
{/* Single unified box — matches Data Layers panel skin */}
|
||||
<div
|
||||
ref={panelBoxRef}
|
||||
className={`bg-[#0a0a0a]/90 backdrop-blur-sm border border-cyan-900/40 flex flex-col relative overflow-hidden`}
|
||||
style={{ boxShadow: '0 0 15px rgba(8,145,178,0.06), inset 0 0 20px rgba(0,0,0,0.4)', ...(expanded ? { flex: '1 1 0', minHeight: 0 } : {}) }}
|
||||
>
|
||||
@@ -435,16 +438,15 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
{ key: 'meshtastic' as Tab, label: 'MESH', icon: <Radio size={10} />, badge: 0 },
|
||||
{
|
||||
key: 'dms' as Tab,
|
||||
label: 'DEAD DROP',
|
||||
icon: <Lock size={10} />,
|
||||
badge: totalDmNotify,
|
||||
label: 'AGENT SHELL',
|
||||
icon: <Terminal size={10} />,
|
||||
badge: 0,
|
||||
},
|
||||
].map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => {
|
||||
setActiveTab(tab.key);
|
||||
if (tab.key === 'dms') setDmView('contacts');
|
||||
}}
|
||||
className={`flex-1 flex items-center justify-center gap-1 py-1.5 text-[12px] font-mono tracking-wider transition-colors ${
|
||||
activeTab === tab.key
|
||||
@@ -541,11 +543,17 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
|
||||
{/* CONTENT AREA */}
|
||||
<div className="flex-1 overflow-hidden flex flex-col min-h-0">
|
||||
{activeTab === 'dms' && (
|
||||
<AgentShellPanel
|
||||
anchorRef={panelBoxRef}
|
||||
active={expanded && activeTab === 'dms'}
|
||||
/>
|
||||
)}
|
||||
{dashboardRestrictedTab && (
|
||||
<div className="flex-1 overflow-y-auto styled-scrollbar px-4 py-6 border-l-2 border-cyan-800/25 flex items-center justify-center">
|
||||
<div className="max-w-md w-full border border-cyan-900/30 bg-cyan-950/10 px-5 py-6 text-center">
|
||||
<div className="inline-flex items-center justify-center w-11 h-11 border border-cyan-700/40 bg-black/30 text-cyan-300 mb-3">
|
||||
{activeTab === 'infonet' ? <Shield size={17} /> : <Lock size={17} />}
|
||||
<Shield size={17} />
|
||||
</div>
|
||||
<div className="text-sm font-mono tracking-[0.24em] text-cyan-300 mb-2">
|
||||
{dashboardRestrictedTitle}
|
||||
@@ -1484,8 +1492,8 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ─── Dead Drop Tab ─── */}
|
||||
{!dashboardRestrictedTab && activeTab === 'dms' && (
|
||||
{/* Dead Drop chat UI moved to Infonet Terminal → Messages */}
|
||||
{false && !dashboardRestrictedTab && activeTab === 'dms' && (
|
||||
<>
|
||||
{/* Sub-nav: Contacts | Inbox | Muted | (back to contacts from chat) */}
|
||||
<div className="flex items-center gap-1 px-3 py-1.5 border-b border-[var(--border-primary)]/30 shrink-0">
|
||||
@@ -2259,21 +2267,26 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
|
||||
</div>
|
||||
|
||||
{/* INPUT BAR */}
|
||||
{dashboardRestrictedTab ? (
|
||||
{activeTab === 'dms' ? (
|
||||
<div className="mx-2 mb-2 mt-1 border border-cyan-800/40 bg-black/30 shrink-0 relative">
|
||||
<span className="absolute -top-[7px] left-3 bg-[var(--bg-primary)] px-1 text-[11px] font-mono text-cyan-700/60 tracking-[0.15em] select-none">
|
||||
SHELL
|
||||
</span>
|
||||
<div className="px-3 py-2.5 text-[13px] font-mono text-[var(--text-secondary)] leading-relaxed">
|
||||
Local bash/cmd launches here (desktop). Set your own working directory in Settings.
|
||||
</div>
|
||||
</div>
|
||||
) : dashboardRestrictedTab ? (
|
||||
<div className="mx-2 mb-2 mt-1 border border-cyan-800/40 bg-black/30 shrink-0 relative">
|
||||
<span className="absolute -top-[7px] left-3 bg-[var(--bg-primary)] px-1 text-[11px] font-mono text-cyan-700/60 tracking-[0.15em] select-none">
|
||||
ACCESS
|
||||
</span>
|
||||
<div className="px-3 py-3 flex flex-col gap-2">
|
||||
<div className="text-[12px] font-mono tracking-widest text-[var(--text-muted)] uppercase">
|
||||
{activeTab === 'infonet'
|
||||
? '→ PRIVATE INFONET / TERMINAL ONLY'
|
||||
: '→ DEAD DROP / TERMINAL ONLY'}
|
||||
→ PRIVATE INFONET / TERMINAL ONLY
|
||||
</div>
|
||||
<div className="text-[13px] font-mono text-[var(--text-secondary)] leading-[1.65]">
|
||||
{activeTab === 'infonet'
|
||||
? 'Private gate posting and reading are restricted to the terminal for now. Dashboard support is coming soon.'
|
||||
: 'Secure messages are restricted to the terminal for now. Dashboard inbox, requests, and compose are coming soon.'}
|
||||
Private gate posting and reading are restricted to the terminal for now. Dashboard support is coming soon.
|
||||
</div>
|
||||
<button
|
||||
onClick={openTerminal}
|
||||
|
||||
@@ -4122,12 +4122,10 @@ export function useMeshChatController({
|
||||
const dmTrustHint = buildDmTrustHint(selectedContactInfo);
|
||||
const dmTrustPrimaryAction = dmTrustPrimaryActionLabel(selectedContactInfo);
|
||||
const wormholeDescriptor = getWormholeIdentityDescriptor();
|
||||
const dashboardRestrictedTab: boolean = activeTab === 'infonet' || activeTab === 'dms';
|
||||
const dashboardRestrictedTitle = activeTab === 'infonet' ? 'INFONET RESTRICTED' : 'DEAD DROP RESTRICTED';
|
||||
const dashboardRestrictedTab: boolean = activeTab === 'infonet';
|
||||
const dashboardRestrictedTitle = 'INFONET RESTRICTED';
|
||||
const dashboardRestrictedDetail =
|
||||
activeTab === 'infonet'
|
||||
? 'Private Wormhole gate activity is staying in the terminal for this build. Dashboard integration is coming soon.'
|
||||
: 'Secure Dead Drop stays in the terminal for this build. Dashboard inbox and compose surfaces are coming soon.';
|
||||
'Private Wormhole gate activity is staying in the terminal for this build. Dashboard integration is coming soon.';
|
||||
const selectedGateKey = selectedGate.trim().toLowerCase();
|
||||
const selectedGatePersonaList = selectedGateKey ? gatePersonas[selectedGateKey] || [] : [];
|
||||
const selectedGateActivePersonaId = selectedGateKey ? activeGatePersonaId[selectedGateKey] || '' : '';
|
||||
|
||||
@@ -14,6 +14,44 @@ running on `localhost:8000`. It tracks military flights, ships, satellites, SIGI
|
||||
earthquakes, fires, GDELT conflict events, prediction markets, and 30+ other data
|
||||
layers — all with geographic coordinates.
|
||||
|
||||
## Agent Fast Path (read first)
|
||||
|
||||
ShadowBroker exposes dozens of read commands. **Do not explore them.** Use the
|
||||
three-tool surface:
|
||||
|
||||
| Tool | When |
|
||||
|------|------|
|
||||
| `await sb.ask("natural language question")` | **Default for reads** — server routes to fastest command |
|
||||
| `await sb.run_playbook("hot_snapshot")` | Pre-batched snapshots (morning brief, monitor poll, status) |
|
||||
| `await sb.channel_status()` | Liveness (~5 ms) — never `/api/health` |
|
||||
|
||||
**Latency tiers:** `find_entity` / `find_flights` / `search_news` / `entities_near` → **⚡ <30 ms**.
|
||||
`search_telemetry` / `get_telemetry` / `get_report` → **🔴 seconds** — blocked unless `confirm_expensive=true`.
|
||||
|
||||
```python
|
||||
# Default read path (route + execute)
|
||||
answer = await sb.ask("where is the Patriots jet")
|
||||
|
||||
# Named batch plans
|
||||
brief = await sb.run_playbook("hot_snapshot")
|
||||
monitor = await sb.run_playbook("monitor_heartbeat")
|
||||
|
||||
# Structured lookup when you already parsed fields
|
||||
entity = await sb.send_command("find_entity", {"owner": "musk", "compact": True})
|
||||
|
||||
# Multi-command — always batch, never sequential loops
|
||||
batch = await sb.send_batch([
|
||||
{"cmd": "get_summary", "args": {"compact": True}},
|
||||
{"cmd": "what_changed", "args": {"compact": True}},
|
||||
])
|
||||
```
|
||||
|
||||
**Playbooks:** `hot_snapshot`, `morning_brief`, `status_check`, `monitor_heartbeat`, `track_snapshot`, `area_brief`, `entity_recon`.
|
||||
|
||||
**Anti-patterns:** `search_telemetry` for known tail numbers; `get_telemetry` for routine polls; sequential `send_command` loops; empty `layers: []` on `get_layer_slice`.
|
||||
|
||||
Load machine-readable routing hints once: `GET /api/ai/capabilities` → `routing`.
|
||||
|
||||
## How to Use This Skill
|
||||
|
||||
Import the client and call methods:
|
||||
@@ -118,8 +156,11 @@ The channel operates over HMAC-authenticated HTTP with body-integrity binding:
|
||||
| `sb.stream_updates()` | SSE push: `layer_changed`, alerts, tasks | **Open first, keep open** — tells you exactly which layers updated |
|
||||
| `await sb.get_layer_slice(["ships", "gdelt"])` | Only the requested layers, with per-layer incremental | **Primary fetch method** — automatically skips layers you already have |
|
||||
| `await sb.send_command("get_summary")` | Lightweight counts-only summary | Discover what data exists before pulling anything |
|
||||
| `await sb.ask("...")` | **Route + execute** | **Default** for natural-language reads |
|
||||
| `await sb.send_command("find_entity", {...})` | Exact-first entity resolver | Parsed person/tail/callsign/MMSI — skips fuzzy unless `fallback_search=true` |
|
||||
| `await sb.send_command("find_flights", {...})` | Targeted flight search | When you know the domain (callsign, tail number) |
|
||||
| `await sb.send_command("search_telemetry", {...})` | Cross-layer keyword search | When you don't know which layer has the answer |
|
||||
| `await sb.send_command("route_query", {...})` | Routing plan only | Inspect recommended command before executing |
|
||||
| `await sb.send_command("search_telemetry", {...})` | Cross-layer fuzzy search | **Last resort** — requires `confirm_expensive=true` |
|
||||
|
||||
**Full telemetry dumps (use sparingly — large payloads):**
|
||||
|
||||
@@ -598,10 +639,10 @@ When the user asks a question, follow this decision tree:
|
||||
fresh data, pushes alerts instantly, and eliminates blind polling.
|
||||
|
||||
2. **Does ShadowBroker have this data already?**
|
||||
- **Start with `get_summary()`** to see what layers are populated and their counts.
|
||||
- **Known domain** (flight callsign, ship name, keyword) → use the targeted command:
|
||||
`find_flights`, `find_ships`, `search_news`, `entities_near`, `search_telemetry`
|
||||
- **Unknown domain** → `search_telemetry` (cross-layer keyword search, ranked results)
|
||||
- **Natural language** → `await sb.ask(question)` (routes server-side)
|
||||
- **Batch snapshot** → `await sb.run_playbook("hot_snapshot")`
|
||||
- **Known domain** → `find_entity`, `find_flights`, `find_ships`, `search_news`, `entities_near`
|
||||
- **Unknown domain** → `find_entity` first; only then `search_telemetry` with `confirm_expensive=true`
|
||||
- **Need specific layers** → `get_layer_slice(["military_flights", "gdelt"])` — only
|
||||
fetches layers that changed since your last call (per-layer incremental).
|
||||
- **Near a location** → `entities_near()` or `get_near_me()` (scans all layers within radius)
|
||||
|
||||
@@ -424,6 +424,27 @@ def query_snapshots(
|
||||
return results
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Playbook flattening (monitor_heartbeat → detect_anomalies shape)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _layers_from_playbook(playbook_data: dict) -> dict:
|
||||
"""Turn run_playbook(monitor_heartbeat) results into a layer-keyed dict."""
|
||||
merged: dict[str, list] = {}
|
||||
if not isinstance(playbook_data, dict):
|
||||
return merged
|
||||
for item in playbook_data.get("results") or []:
|
||||
if not isinstance(item, dict) or not item.get("ok"):
|
||||
continue
|
||||
payload = item.get("data") if isinstance(item.get("data"), dict) else {}
|
||||
if item.get("cmd") == "get_layer_slice":
|
||||
layers = payload.get("layers") if isinstance(payload.get("layers"), dict) else {}
|
||||
for layer, rows in layers.items():
|
||||
if isinstance(rows, list):
|
||||
merged[layer] = rows
|
||||
return merged
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main heartbeat handler
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -437,8 +458,14 @@ async def heartbeat(sb_client) -> list[str]:
|
||||
messages = []
|
||||
|
||||
try:
|
||||
# 1. Pull fresh telemetry (fast + slow merged for full visibility)
|
||||
data = await sb_client.get_full_telemetry()
|
||||
# 1. Low-latency monitor poll (playbook) — fallback to legacy full pull
|
||||
try:
|
||||
playbook_data = await sb_client.run_playbook("monitor_heartbeat", {})
|
||||
data = _layers_from_playbook(playbook_data)
|
||||
except Exception:
|
||||
data = {}
|
||||
if not data:
|
||||
data = await sb_client.get_full_telemetry()
|
||||
|
||||
# 2. Run anomaly detection
|
||||
anomalies = detect_anomalies(data, _state)
|
||||
|
||||
@@ -270,6 +270,82 @@ class ShadowBrokerClient:
|
||||
r = await self._get("/api/ai/channel/status")
|
||||
return r.json()
|
||||
|
||||
@staticmethod
|
||||
def unwrap_channel_result(resp: dict) -> dict:
|
||||
"""Extract inner command payload from /api/ai/channel/command response."""
|
||||
if not isinstance(resp, dict):
|
||||
return {}
|
||||
result = resp.get("result")
|
||||
if not isinstance(result, dict):
|
||||
return {}
|
||||
if result.get("ok"):
|
||||
data = result.get("data")
|
||||
return data if isinstance(data, dict) else {}
|
||||
return result
|
||||
|
||||
async def route_query(
|
||||
self,
|
||||
text: str,
|
||||
*,
|
||||
lat: float | None = None,
|
||||
lng: float | None = None,
|
||||
radius_km: float = 50,
|
||||
compact: bool = True,
|
||||
) -> dict:
|
||||
"""Server-side intent routing — returns recommended command (no LLM)."""
|
||||
args: dict[str, Any] = {"text": text, "radius_km": radius_km, "compact": compact}
|
||||
if lat is not None:
|
||||
args["lat"] = lat
|
||||
if lng is not None:
|
||||
args["lng"] = lng
|
||||
resp = await self.send_command("route_query", args)
|
||||
return self.unwrap_channel_result(resp)
|
||||
|
||||
async def run_playbook(self, name: str, args: dict | None = None) -> dict:
|
||||
"""Execute a named server playbook (batched, concurrent)."""
|
||||
payload = {"name": name, **(args or {})}
|
||||
resp = await self.send_command("run_playbook", payload)
|
||||
return self.unwrap_channel_result(resp)
|
||||
|
||||
async def ask(
|
||||
self,
|
||||
question: str,
|
||||
*,
|
||||
lat: float | None = None,
|
||||
lng: float | None = None,
|
||||
radius_km: float = 50,
|
||||
execute: bool = True,
|
||||
) -> dict:
|
||||
"""Natural-language read: route_query → recommended command (one round-trip or two)."""
|
||||
route = await self.route_query(
|
||||
question,
|
||||
lat=lat,
|
||||
lng=lng,
|
||||
radius_km=radius_km,
|
||||
compact=True,
|
||||
)
|
||||
if not route:
|
||||
return {"ok": False, "detail": "route_query returned no plan"}
|
||||
|
||||
if not execute:
|
||||
return {"ok": True, "route": route}
|
||||
|
||||
recommended = route.get("recommended") or {}
|
||||
cmd = str(recommended.get("cmd", "") or "").strip()
|
||||
cmd_args = recommended.get("args") or {}
|
||||
if not cmd:
|
||||
return {"ok": False, "detail": "route produced no command", "route": route}
|
||||
|
||||
exec_resp = await self.send_command(cmd, cmd_args)
|
||||
exec_inner = exec_resp.get("result") if isinstance(exec_resp.get("result"), dict) else {}
|
||||
return {
|
||||
"ok": bool(exec_resp.get("ok") and exec_inner.get("ok")),
|
||||
"route": route,
|
||||
"command": cmd,
|
||||
"args": cmd_args,
|
||||
"result": exec_inner,
|
||||
}
|
||||
|
||||
async def send_batch(self, commands: list[dict]) -> dict:
|
||||
"""Send multiple commands in a single HTTP round-trip.
|
||||
|
||||
|
||||
@@ -24,6 +24,22 @@ entry_points:
|
||||
requirements:
|
||||
- httpx>=0.25.0
|
||||
|
||||
# Thin agent surface — expose only these to the LLM tool picker
|
||||
agent_surface:
|
||||
primary_reads:
|
||||
- sb_query.ShadowBrokerClient.ask
|
||||
- sb_query.ShadowBrokerClient.run_playbook
|
||||
- sb_query.ShadowBrokerClient.send_batch
|
||||
- sb_query.ShadowBrokerClient.channel_status
|
||||
writes:
|
||||
- sb_query.ShadowBrokerClient.place_pin
|
||||
- sb_query.ShadowBrokerClient.place_pins_batch
|
||||
blocked_without_confirm:
|
||||
- search_telemetry
|
||||
- get_telemetry
|
||||
- get_slow_telemetry
|
||||
- get_report
|
||||
|
||||
# Capabilities declared
|
||||
capabilities:
|
||||
- live_telemetry # Real-time OSINT data (flights, ships, SIGINT, etc.)
|
||||
|
||||
Reference in New Issue
Block a user