Compare commits

..

7 Commits

Author SHA1 Message Date
BigBodyCobain d1e1be4016 Replace mock Agent Shell overlay with inline xterm PTY and dock/expand UX.
Uses a local-operator WebSocket bash session, keeps the map interactive, and SNAP docks the shell back into Mesh Chat instead of a floating blurred panel.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 11:30:50 -06:00
BigBodyCobain 0afb85e241 Fix MeshChat behavior tests after Agent Shell tab replaced dashboard Dead Drop UI.
Point trust and dm-add assertions at Infonet Messages and MeshTerminal where those flows now live.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 09:44:44 -06:00
BigBodyCobain 039a0f9d0c Remove dead Drop dashboard UI so Agent Shell frontend build passes.
Dead Drop chat stays in Infonet Terminal; Mesh Chat dms tab is Agent Shell only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 09:40:54 -06:00
BigBodyCobain b9b99c1fa8 Replace Mesh Chat Dead Drop tab with stretchable Agent Shell panel.
Anchors to the Mesh Chat box, stretches on tab enter, and supports user resize without changing the fixed left column width.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 00:26:58 -06:00
BigBodyCobain a8fd33a758 Add OpenClaw fast-path routing with playbooks and expensive-command gate.
Move intent routing into route_query/ask, short-circuit find_entity fuzzy search, and document the thin three-tool agent surface so Hermes avoids multi-second search_telemetry by default.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 21:32:08 -06:00
BigBodyCobain 7346129d0e Fix ChangelogModal TypeScript after contributor trim.
Declare optional pr on contributor entries so the build type-check passes with OSIRIS-only credits.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 00:14:09 -06:00
BigBodyCobain eb8f39f84e Fix v0.9.82 changelog credits: drop stale contributor tags.
Remove recycled names from older releases; keep only OSIRIS third-party attribution for this cycle.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 23:30:42 -06:00
22 changed files with 1484 additions and 901 deletions
+2
View File
@@ -370,6 +370,7 @@ osint_router = _load_optional_router("routers.osint")
scm_router = _load_optional_router("routers.scm") scm_router = _load_optional_router("routers.scm")
entity_graph_router = _load_optional_router("routers.entity_graph") entity_graph_router = _load_optional_router("routers.entity_graph")
intel_feeds_router = _load_optional_router("routers.intel_feeds") intel_feeds_router = _load_optional_router("routers.intel_feeds")
agent_shell_router = _load_optional_router("routers.agent_shell")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -3651,6 +3652,7 @@ app.include_router(osint_router)
app.include_router(scm_router) app.include_router(scm_router)
app.include_router(entity_graph_router) app.include_router(entity_graph_router)
app.include_router(intel_feeds_router) app.include_router(intel_feeds_router)
app.include_router(agent_shell_router)
from services.data_fetcher import update_all_data from services.data_fetcher import update_all_data
+195
View File
@@ -0,0 +1,195 @@
"""Local-operator PTY WebSocket for the Mesh Chat agent shell."""
from __future__ import annotations
import asyncio
import fcntl
import hmac
import json
import logging
import os
import pty
import select
import signal
import struct
import sys
import termios
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
from pydantic import BaseModel, Field
from auth import (
_current_admin_key,
_debug_mode_enabled,
_is_trusted_local_runtime_host,
require_local_operator,
)
from services.agent_shell_settings import (
get_agent_shell_settings,
set_agent_shell_working_directory,
)
logger = logging.getLogger(__name__)
router = APIRouter(tags=["agent-shell"])
class AgentShellSettingsUpdate(BaseModel):
working_directory: str = Field(min_length=1)
def _set_winsize(fd: int, rows: int, cols: int) -> None:
winsize = struct.pack("HHHH", rows, cols, 0, 0)
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
async def _authorize_agent_shell_ws(ws: WebSocket, admin_key_query: str = "") -> None:
host = (ws.client.host or "").lower() if ws.client else ""
if _is_trusted_local_runtime_host(host) or (_debug_mode_enabled() and host == "test"):
return
admin_key = _current_admin_key()
presented = str(admin_key_query or ws.headers.get("x-admin-key", "") or "").strip()
if admin_key and presented and hmac.compare_digest(presented.encode(), admin_key.encode()):
return
await ws.close(code=4403, reason="local operator access only")
raise WebSocketDisconnect()
def _resolve_shell_cwd(requested: str) -> str:
requested = str(requested or "").strip()
if requested:
resolved = os.path.abspath(os.path.expanduser(requested))
if os.path.isdir(resolved):
return resolved
return get_agent_shell_settings()["working_directory"]
def _default_shell() -> str:
if sys.platform == "win32":
return os.environ.get("COMSPEC", "cmd.exe")
return os.environ.get("SHELL", "/bin/bash")
async def _relay_pty(master_fd: int, proc: asyncio.subprocess.Process, ws: WebSocket) -> None:
loop = asyncio.get_running_loop()
while True:
if proc.returncode is not None:
break
try:
readable, _, _ = await loop.run_in_executor(
None, lambda: select.select([master_fd], [], [], 0.05)
)
except Exception:
break
if master_fd in readable:
try:
chunk = os.read(master_fd, 4096)
except OSError:
break
if not chunk:
break
await ws.send_bytes(chunk)
try:
message = await asyncio.wait_for(ws.receive(), timeout=0.05)
except asyncio.TimeoutError:
continue
if message.get("type") == "websocket.disconnect":
break
if message.get("type") != "websocket.receive":
continue
if message.get("bytes"):
os.write(master_fd, message["bytes"])
continue
text = message.get("text")
if not text:
continue
try:
payload = json.loads(text)
except json.JSONDecodeError:
os.write(master_fd, text.encode("utf-8", errors="replace"))
continue
if payload.get("type") == "resize":
rows = int(payload.get("rows") or 24)
cols = int(payload.get("cols") or 80)
_set_winsize(master_fd, max(rows, 2), max(cols, 2))
@router.get("/api/agent-shell/settings", dependencies=[Depends(require_local_operator)])
async def read_agent_shell_settings() -> dict[str, Any]:
return get_agent_shell_settings()
@router.put("/api/agent-shell/settings", dependencies=[Depends(require_local_operator)])
async def write_agent_shell_settings(body: AgentShellSettingsUpdate) -> dict[str, Any]:
try:
return set_agent_shell_working_directory(body.working_directory)
except ValueError as exc:
detail = str(exc)
if detail == "working_directory_not_found":
raise HTTPException(status_code=400, detail="Working directory does not exist") from exc
raise HTTPException(status_code=400, detail="Working directory is required") from exc
@router.websocket("/api/agent-shell/ws")
async def agent_shell_websocket(
ws: WebSocket,
cwd: str = Query(default=""),
cols: int = Query(default=80),
rows: int = Query(default=24),
admin_key: str = Query(default=""),
) -> None:
await ws.accept()
try:
await _authorize_agent_shell_ws(ws, admin_key)
except WebSocketDisconnect:
return
if sys.platform == "win32":
await ws.send_text(
json.dumps(
{
"type": "error",
"message": "Host PTY is not available on Windows backend builds yet. Use the ShadowBroker desktop app or run the backend in Docker/Linux for an embedded shell.",
}
)
)
await ws.close(code=1011)
return
shell_cwd = _resolve_shell_cwd(cwd)
shell = _default_shell()
master_fd, slave_fd = pty.openpty()
_set_winsize(master_fd, max(rows, 2), max(cols, 2))
env = os.environ.copy()
env.setdefault("TERM", "xterm-256color")
env.setdefault("COLORTERM", "truecolor")
proc = await asyncio.create_subprocess_exec(
shell,
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
cwd=shell_cwd,
env=env,
preexec_fn=os.setsid,
)
os.close(slave_fd)
try:
await _relay_pty(master_fd, proc, ws)
finally:
try:
os.close(master_fd)
except OSError:
pass
if proc.returncode is None:
try:
os.killpg(proc.pid, signal.SIGHUP)
except ProcessLookupError:
pass
try:
await asyncio.wait_for(proc.wait(), timeout=2.0)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
+14 -3
View File
@@ -2276,12 +2276,14 @@ async def agent_tool_manifest(request: Request):
async def api_capabilities(request: Request): async def api_capabilities(request: Request):
"""Return full API manifest so the agent knows every available endpoint.""" """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_channel import READ_COMMANDS, WRITE_COMMANDS, detect_tier
from services.openclaw_routing import routing_manifest
from services.config import get_settings from services.config import get_settings
tier = detect_tier() tier = detect_tier()
access_tier = str(get_settings().OPENCLAW_ACCESS_TIER or "restricted").strip().lower() access_tier = str(get_settings().OPENCLAW_ACCESS_TIER or "restricted").strip().lower()
return { return {
"ok": True, "ok": True,
"version": "0.9.82", "version": "0.9.82",
"routing": routing_manifest(),
"auth": { "auth": {
"method": "HMAC-SHA256", "method": "HMAC-SHA256",
"headers": ["X-SB-Timestamp", "X-SB-Nonce", "X-SB-Signature"], "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.", "description": "Compact server-side ship search by MMSI/IMO/name/query, including yacht-owner enrichment.",
}, },
"find_entity": { "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)"}, "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. Use before tracking to avoid fuzzy prompt matching.", "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": { "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)"}, "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 " "layers are serialized, unchanged layers transfer zero bytes. The client tracks versions "
"automatically from SSE events and previous responses. " "automatically from SSE events and previous responses. "
"3) Pass compact=true on every read command for compressed_v1 responses (~60-90% smaller). " "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.", "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.", "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.", "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.",
+48
View File
@@ -0,0 +1,48 @@
"""Operator settings for the embedded agent shell (working directory)."""
from __future__ import annotations
import json
import logging
import os
import threading
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__)
_SETTINGS_FILE = Path(__file__).resolve().parent.parent / "data" / "agent_shell_settings.json"
_LOCK = threading.Lock()
def _default_working_directory() -> str:
return os.environ.get("AGENT_SHELL_DEFAULT_CWD") or os.environ.get("HOME") or "/app"
def get_agent_shell_settings() -> dict[str, Any]:
with _LOCK:
if not _SETTINGS_FILE.exists():
return {"working_directory": _default_working_directory()}
try:
payload = json.loads(_SETTINGS_FILE.read_text(encoding="utf-8"))
except Exception:
logger.warning("agent_shell_settings_unreadable")
return {"working_directory": _default_working_directory()}
cwd = str(payload.get("working_directory") or "").strip() or _default_working_directory()
return {"working_directory": cwd}
def set_agent_shell_working_directory(path: str) -> dict[str, Any]:
normalized = str(path or "").strip()
if not normalized:
raise ValueError("working_directory_required")
resolved = os.path.abspath(os.path.expanduser(normalized))
if not os.path.isdir(resolved):
raise ValueError("working_directory_not_found")
with _LOCK:
_SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
_SETTINGS_FILE.write_text(
json.dumps({"working_directory": resolved}, indent=2) + "\n",
encoding="utf-8",
)
return {"working_directory": resolved}
+55
View File
@@ -87,6 +87,9 @@ READ_COMMANDS = frozenset({
"osint_lookup", "osint_lookup",
"osint_tools", "osint_tools",
"entity_expand", "entity_expand",
# Agent routing helpers
"route_query",
"run_playbook",
}) })
WRITE_COMMANDS = frozenset({ WRITE_COMMANDS = frozenset({
@@ -643,6 +646,19 @@ def _compact_query_result(result: Any) -> Any:
# Command dispatcher # 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]: def _dispatch_command(cmd: str, args: dict[str, Any]) -> dict[str, Any]:
"""Route a command to the appropriate AI Intel function. """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 Commands run in an isolated thread (via _execute_command) so they
do not need or touch the caller's event loop. 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": if cmd == "get_telemetry":
from services.telemetry import get_cached_telemetry_refs from services.telemetry import get_cached_telemetry_refs
data = 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 ""), owner=str(args.get("owner", "") or args.get("operator", "") or ""),
layers=args.get("layers") if isinstance(args.get("layers"), (list, tuple)) else None, layers=args.get("layers") if isinstance(args.get("layers"), (list, tuple)) else None,
limit=args.get("limit", 10), limit=args.get("limit", 10),
fallback_search=bool(args.get("fallback_search") or args.get("confirm_fuzzy")),
) )
if _wants_compact(args): if _wants_compact(args):
compact = dict(result) 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 ""), owner=str(args.get("owner", "") or args.get("operator", "") or ""),
layers=args.get("layers") if isinstance(args.get("layers"), (list, tuple)) else None, layers=args.get("layers") if isinstance(args.get("layers"), (list, tuple)) else None,
limit=5, limit=5,
fallback_search=True,
) )
best = lookup.get("best_match") if isinstance(lookup.get("best_match"), dict) else {} best = lookup.get("best_match") if isinstance(lookup.get("best_match"), dict) else {}
group = str(best.get("group", "") or entity_type).lower() group = str(best.get("group", "") or entity_type).lower()
+500
View File
@@ -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", [])],
}
+6 -2
View File
@@ -1549,11 +1549,13 @@ def find_entity(
owner: str = "", owner: str = "",
layers: list[str] | tuple[str, ...] | None = None, layers: list[str] | tuple[str, ...] | None = None,
limit: int = 10, limit: int = 10,
fallback_search: bool = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Find a named entity across aircraft, maritime, and general telemetry. """Find a named entity across aircraft, maritime, and general telemetry.
This is an intent-level lookup for agents. It tries high-precision 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() effective_query = str(query or name or owner or callsign or registration or icao24 or mmsi or imo or "").strip()
if not effective_query: if not effective_query:
@@ -1628,7 +1630,9 @@ def find_entity(
seen.add(key) seen.add(key)
results.append(normalized) results.append(normalized)
search_layers = requested_layers or _entity_layers_for_type(entity_type) 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) search_result = search_telemetry(query=effective_query, layers=search_layers, limit=limit)
if search_result.get("results"): if search_result.get("results"):
strategies.append("universal_index") strategies.append("universal_index")
@@ -0,0 +1,14 @@
def test_agent_shell_settings_roundtrip(tmp_path, monkeypatch):
from services import agent_shell_settings
settings_path = tmp_path / "agent_shell_settings.json"
workdir = tmp_path / "workspace"
workdir.mkdir()
monkeypatch.setattr(agent_shell_settings, "_SETTINGS_FILE", settings_path)
assert agent_shell_settings.get_agent_shell_settings()["working_directory"]
saved = agent_shell_settings.set_agent_shell_working_directory(str(workdir))
assert saved["working_directory"] == str(workdir.resolve())
assert agent_shell_settings.get_agent_shell_settings()["working_directory"] == str(workdir.resolve())
+41 -1
View File
@@ -466,15 +466,55 @@ def test_find_entity_prioritizes_aircraft_operator_and_callsign(sample_store, mo
monkeypatch.setattr(telemetry, "get_data_version", lambda: 130) 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"]["group"] == "aircraft"
assert by_operator["best_match"]["label"] == "OXE2116" 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) by_callsign = telemetry.find_entity(callsign="AF1", entity_type="aircraft", limit=5)
assert by_callsign["best_match"]["callsign"] == "AF1" assert by_callsign["best_match"]["callsign"] == "AF1"
assert by_callsign["best_match"]["alert_operator"] == "POTUS" 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): def test_find_entity_prioritizes_maritime_owner_and_identifiers(sample_store, monkeypatch):
import services.telemetry as telemetry import services.telemetry as telemetry
+93
View File
@@ -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
+17 -1
View File
@@ -11,6 +11,8 @@
"@mapbox/point-geometry": "^1.1.0", "@mapbox/point-geometry": "^1.1.0",
"@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.1", "@tauri-apps/plugin-updater": "^2.10.1",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
"framer-motion": "^12.38.0", "framer-motion": "^12.38.0",
"hls.js": "^1.6.15", "hls.js": "^1.6.15",
"lucide-react": "^0.575.0", "lucide-react": "^0.575.0",
@@ -3251,6 +3253,21 @@
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
} }
}, },
"node_modules/@xterm/addon-fit": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz",
"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
"license": "MIT"
},
"node_modules/@xterm/xterm": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
"integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==",
"license": "MIT",
"workspaces": [
"addons/*"
]
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.16.0", "version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -7374,7 +7391,6 @@
"version": "8.5.15", "version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
+2
View File
@@ -23,6 +23,8 @@
"@mapbox/point-geometry": "^1.1.0", "@mapbox/point-geometry": "^1.1.0",
"@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.1", "@tauri-apps/plugin-updater": "^2.10.1",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
"framer-motion": "^12.38.0", "framer-motion": "^12.38.0",
"hls.js": "^1.6.15", "hls.js": "^1.6.15",
"lucide-react": "^0.575.0", "lucide-react": "^0.575.0",
@@ -114,23 +114,24 @@ describe('MeshChat behavior - policy wiring', () => {
expect(controller).toContain('timer = setTimeout(() => void poll(classification.refreshCount), classification.delay);'); expect(controller).toContain('timer = setTimeout(() => void poll(classification.refreshCount), classification.delay);');
}); });
it('dead-drop UI distinguishes invite-pinned trust from TOFU-only', () => { it('dead-drop trust copy lives in shared hints and Infonet Messages', () => {
const index = readSource('../../components/MeshChat/index.tsx'); const hints = readSource('../../mesh/meshPrivacyHints.ts');
expect(index).toContain('getContactTrustSummary'); expect(hints).toContain('INVITE PINNED');
expect(index).toContain('INVITE PINNED'); expect(hints).toContain('FIRST CONTACT (TOFU ONLY)');
expect(index).toContain('TOFU ONLY'); expect(hints).toContain('anchored by an imported signed invite');
expect(index).toContain('anchored by an imported signed invite'); const messages = readSource('../../components/InfonetTerminal/MessagesView.tsx');
expect(index).toContain('rootWitnessContinuityLabel'); expect(messages).toContain('getContactTrustSummary');
expect(index).toContain('RECOVER ROOT'); expect(messages).toContain('rootWitnessContinuityLabel');
expect(index).toContain('!selectedContactTrustSummary?.rootMismatch');
}); });
it('request UI does not route ordinary request flow through legacy add-contact lookup', () => { it('MeshChat Agent Shell tab keeps legacy dm-add guidance in MeshTerminal', () => {
const index = readSource('../../components/MeshChat/index.tsx'); const index = readSource('../../components/MeshChat/index.tsx');
expect(index).toContain('handleRequestComposerAction'); expect(index).toContain('AgentShellPanel');
expect(index).toContain('SHELL');
expect(index).not.toContain('handleAddContact().catch(() =>'); expect(index).not.toContain('handleAddContact().catch(() =>');
expect(index).toContain('dm add'); const terminal = readSource('../../components/MeshTerminal.tsx');
expect(index).toContain('legacy migration'); expect(terminal).toContain('dm add');
expect(terminal).toContain('legacy migration');
}); });
it('controller blocks trust-new-key when the stable root changed', () => { it('controller blocks trust-new-key when the stable root changed', () => {
+7 -44
View File
@@ -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.', '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)', name: 'OSIRIS (simplifaisoul/osiris)',
desc: 'MIT-licensed recon stack — adapted for ShadowBroker proxy model (see backend/third_party/osiris/NOTICE.md)', 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() { export function useChangelog() {
@@ -0,0 +1,232 @@
'use client';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Terminal } from 'lucide-react';
import { Terminal as XTerm } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import '@xterm/xterm/css/xterm.css';
import { resolveAgentShellWsUrl } from '@/lib/agentShellWs';
const SHELL_FONT_PX = 14;
const CWD_STORAGE_KEY = 'sb_agent_shell_cwd';
type Props = {
active: boolean;
expanded: boolean;
onExpandedChange: (expanded: boolean) => void;
};
function readStoredCwd(): string {
if (typeof window === 'undefined') return '';
try {
return window.localStorage.getItem(CWD_STORAGE_KEY) || '';
} catch {
return '';
}
}
export default function AgentShellPanel({ active, expanded, onExpandedChange }: Props) {
const hostRef = useRef<HTMLDivElement>(null);
const termRef = useRef<XTerm | null>(null);
const fitRef = useRef<FitAddon | null>(null);
const wsRef = useRef<WebSocket | null>(null);
const [status, setStatus] = useState<'idle' | 'connecting' | 'open' | 'error'>('idle');
const [statusDetail, setStatusDetail] = useState('');
const [cwd, setCwd] = useState('');
const disconnect = useCallback(() => {
wsRef.current?.close();
wsRef.current = null;
termRef.current?.dispose();
termRef.current = null;
fitRef.current = null;
setStatus('idle');
}, []);
const fitTerminal = useCallback(() => {
const fit = fitRef.current;
const term = termRef.current;
const ws = wsRef.current;
if (!fit || !term) return;
fit.fit();
if (ws?.readyState === WebSocket.OPEN) {
ws.send(
JSON.stringify({
type: 'resize',
cols: term.cols,
rows: term.rows,
}),
);
}
}, []);
const connect = useCallback(() => {
if (!active || !hostRef.current) return;
disconnect();
const term = new XTerm({
fontFamily: 'var(--font-roboto-mono), ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
fontSize: SHELL_FONT_PX,
lineHeight: 1.35,
cursorBlink: true,
theme: {
background: '#04070b',
foreground: '#d9f7ff',
cursor: '#22d3ee',
selectionBackground: '#0e7490',
},
scrollback: 5000,
});
const fit = new FitAddon();
term.loadAddon(fit);
term.open(hostRef.current);
fit.fit();
termRef.current = term;
fitRef.current = fit;
const storedCwd = readStoredCwd();
setCwd(storedCwd);
setStatus('connecting');
setStatusDetail('');
const ws = new WebSocket(resolveAgentShellWsUrl(storedCwd));
ws.binaryType = 'arraybuffer';
wsRef.current = ws;
ws.onopen = () => {
setStatus('open');
fit.fit();
ws.send(
JSON.stringify({
type: 'resize',
cols: term.cols,
rows: term.rows,
}),
);
term.focus();
};
ws.onmessage = (event) => {
if (typeof event.data === 'string') {
try {
const payload = JSON.parse(event.data) as { type?: string; message?: string };
if (payload.type === 'error') {
setStatus('error');
setStatusDetail(payload.message || 'Shell unavailable');
term.writeln(`\r\n\x1b[31m${payload.message || 'Shell unavailable'}\x1b[0m`);
return;
}
} catch {
term.write(event.data);
return;
}
}
if (event.data instanceof ArrayBuffer) {
term.write(new Uint8Array(event.data));
}
};
ws.onerror = () => {
setStatus('error');
setStatusDetail('Could not connect to the local agent shell endpoint.');
term.writeln('\r\n\x1b[31mCould not connect to the local agent shell endpoint.\x1b[0m');
};
ws.onclose = () => {
setStatus((prev) => (prev === 'error' ? prev : 'idle'));
term.writeln('\r\n\x1b[90m[session closed]\x1b[0m');
};
term.onData((data) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(new TextEncoder().encode(data));
}
});
}, [active, disconnect]);
useEffect(() => {
if (!active) {
disconnect();
return;
}
connect();
return () => disconnect();
}, [active, connect, disconnect]);
useEffect(() => {
if (!active) return;
const timer = window.setTimeout(() => fitTerminal(), expanded ? 220 : 0);
return () => window.clearTimeout(timer);
}, [active, expanded, fitTerminal]);
useEffect(() => {
if (!active) return;
const onResize = () => fitTerminal();
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, [active, fitTerminal]);
if (!active) {
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={16} className="text-cyan-400 mb-2" />
<div className="text-sm font-mono tracking-[0.2em] text-cyan-300">LOCAL SHELL</div>
<div className="mt-2 text-[13px] font-mono text-[var(--text-secondary)] leading-relaxed">
Expand Mesh Chat to open your operator shell.
</div>
</div>
);
}
return (
<div className="flex-1 min-h-0 flex flex-col border-l-2 border-cyan-800/25 bg-[#04070b]">
<div className="flex items-center justify-between gap-2 border-b border-cyan-900/40 px-2 py-1.5 shrink-0">
<div className="min-w-0 text-[12px] font-mono tracking-[0.14em] text-cyan-300/90 truncate">
{cwd ? cwd : 'operator shell'}
</div>
<div className="flex items-center gap-1 shrink-0">
{!expanded ? (
<button
type="button"
onClick={() => onExpandedChange(true)}
className="px-2 py-0.5 text-[11px] font-mono tracking-[0.12em] text-cyan-300 border border-cyan-800/40 hover:bg-cyan-950/30"
>
EXPAND
</button>
) : (
<button
type="button"
onClick={() => onExpandedChange(false)}
className="px-2 py-0.5 text-[11px] font-mono tracking-[0.12em] text-cyan-300 border border-cyan-800/40 hover:bg-cyan-950/30"
>
SNAP
</button>
)}
<button
type="button"
onClick={connect}
className="px-2 py-0.5 text-[11px] font-mono tracking-[0.12em] text-slate-400 border border-slate-700/40 hover:bg-white/5"
>
RECONNECT
</button>
</div>
</div>
{status === 'error' && statusDetail && (
<div className="px-2 py-1 text-[12px] font-mono text-amber-300/90 border-b border-amber-900/30 bg-amber-950/10 shrink-0">
{statusDetail}
</div>
)}
<div ref={hostRef} className="flex-1 min-h-[220px] px-1 py-1 overflow-hidden" />
<div className="border-t border-cyan-900/30 px-2 py-1 text-[11px] font-mono text-slate-500 shrink-0">
{status === 'connecting'
? 'Connecting…'
: status === 'open'
? `${expanded ? 'Expanded' : 'Docked'} · ${SHELL_FONT_PX}px · map stays interactive`
: 'Local operator shell'}
</div>
</div>
);
}
+47 -811
View File
@@ -1,6 +1,7 @@
'use client'; 'use client';
import React from 'react'; import React, { useEffect, useRef, useState } from 'react';
import AgentShellPanel from './AgentShellPanel';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { import {
Antenna, Antenna,
@@ -12,6 +13,7 @@ import {
Radio, Radio,
Shield, Shield,
Terminal, Terminal,
SquareTerminal,
UserPlus, UserPlus,
Lock, Lock,
Check, Check,
@@ -89,6 +91,8 @@ function describeGateCompatReason(reason: string, gateId: string): string {
// NO direct trust-mutating imports — all mutations go through the hook. // NO direct trust-mutating imports — all mutations go through the hook.
const MeshChat = React.memo(function MeshChat(props: MeshChatProps) { const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
const panelBoxRef = useRef<HTMLDivElement>(null);
const [shellExpanded, setShellExpanded] = useState(false);
const ctrl = useMeshChatController(props); const ctrl = useMeshChatController(props);
const { const {
// UI state // UI state
@@ -317,6 +321,12 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
recentPrivateFallbackReason, recentPrivateFallbackReason,
onSettingsClick, onSettingsClick,
} = ctrl; } = ctrl;
useEffect(() => {
if (activeTab !== 'dms') {
setShellExpanded(false);
}
}, [activeTab]);
const selectedContactTrustSummary = selectedContactInfo const selectedContactTrustSummary = selectedContactInfo
? getContactTrustSummary(selectedContactInfo) ? getContactTrustSummary(selectedContactInfo)
: null; : null;
@@ -398,7 +408,12 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
> >
{/* Single unified box — matches Data Layers panel skin */} {/* Single unified box — matches Data Layers panel skin */}
<div <div
className={`bg-[#0a0a0a]/90 backdrop-blur-sm border border-cyan-900/40 flex flex-col relative overflow-hidden`} ref={panelBoxRef}
className={`bg-[#0a0a0a]/90 backdrop-blur-sm border border-cyan-900/40 flex flex-col relative overflow-hidden transition-[width,min-width] duration-200 ${
activeTab === 'dms' && shellExpanded
? 'z-[210] min-w-[min(760px,calc(100vw-3rem))] w-[min(760px,calc(100vw-3rem))]'
: ''
}`}
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 } : {}) }} 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 } : {}) }}
> >
{/* HEADER */} {/* HEADER */}
@@ -435,16 +450,15 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
{ key: 'meshtastic' as Tab, label: 'MESH', icon: <Radio size={10} />, badge: 0 }, { key: 'meshtastic' as Tab, label: 'MESH', icon: <Radio size={10} />, badge: 0 },
{ {
key: 'dms' as Tab, key: 'dms' as Tab,
label: 'DEAD DROP', label: 'SHELL',
icon: <Lock size={10} />, icon: <SquareTerminal size={10} />,
badge: totalDmNotify, badge: 0,
}, },
].map((tab) => ( ].map((tab) => (
<button <button
key={tab.key} key={tab.key}
onClick={() => { onClick={() => {
setActiveTab(tab.key); 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 ${ className={`flex-1 flex items-center justify-center gap-1 py-1.5 text-[12px] font-mono tracking-wider transition-colors ${
activeTab === tab.key activeTab === tab.key
@@ -478,21 +492,21 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
</div> </div>
)} )}
{activeTab !== 'meshtastic' && wormholeEnabled && !wormholeReadyState && ( {activeTab !== 'dms' && activeTab !== 'meshtastic' && wormholeEnabled && !wormholeReadyState && (
<div className="px-3 py-2 text-sm font-mono text-red-400/90 border-b border-red-900/30 bg-red-950/20 leading-[1.65] shrink-0"> <div className="px-3 py-2 text-sm font-mono text-red-400/90 border-b border-red-900/30 bg-red-950/20 leading-[1.65] shrink-0">
Wormhole secure mode is enabled but the local agent is not ready. Dead Drop is Wormhole secure mode is enabled but the local agent is not ready. Dead Drop is
blocked until Wormhole is running. blocked until Wormhole is running.
</div> </div>
)} )}
{activeTab !== 'meshtastic' && wormholeEnabled && wormholeReadyState && ( {activeTab !== 'dms' && activeTab !== 'meshtastic' && wormholeEnabled && wormholeReadyState && (
<div className="px-3 py-2 text-sm font-mono text-yellow-400/80 border-b border-yellow-900/20 bg-yellow-950/10 leading-[1.65] shrink-0"> <div className="px-3 py-2 text-sm font-mono text-yellow-400/80 border-b border-yellow-900/20 bg-yellow-950/10 leading-[1.65] shrink-0">
Wormhole secure mode is active. Experimental private-lane operations are routed Wormhole secure mode is active. Experimental private-lane operations are routed
through the local agent and current secure transport paths. through the local agent and current secure transport paths.
</div> </div>
)} )}
{activeTab !== 'meshtastic' && wormholeEnabled && wormholeReadyState && !wormholeRnsReady && ( {activeTab !== 'dms' && activeTab !== 'meshtastic' && wormholeEnabled && wormholeReadyState && !wormholeRnsReady && (
<div className="px-3 py-2 text-sm font-mono text-amber-300/90 border-b border-amber-900/30 bg-amber-950/20 leading-[1.65] shrink-0"> <div className="px-3 py-2 text-sm font-mono text-amber-300/90 border-b border-amber-900/30 bg-amber-950/20 leading-[1.65] shrink-0">
TRANSITIONAL PRIVATE LANE. Wormhole is up and gate chat is available on the TRANSITIONAL PRIVATE LANE. Wormhole is up and gate chat is available on the
transitional lane. Reticulum is still warming Dead Drop / DM requires the transitional lane. Reticulum is still warming Dead Drop / DM requires the
@@ -500,7 +514,7 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
</div> </div>
)} )}
{activeTab !== 'meshtastic' && anonymousModeEnabled && !anonymousModeReady && ( {activeTab !== 'dms' && activeTab !== 'meshtastic' && anonymousModeEnabled && !anonymousModeReady && (
<div className="px-3 py-2 text-sm font-mono text-red-400/90 border-b border-red-900/30 bg-red-950/20 leading-[1.65] shrink-0"> <div className="px-3 py-2 text-sm font-mono text-red-400/90 border-b border-red-900/30 bg-red-950/20 leading-[1.65] shrink-0">
Anonymous mode is active, but hidden transport is not ready. Dead Drop is blocked Anonymous mode is active, but hidden transport is not ready. Dead Drop is blocked
until Wormhole is running over Tor, I2P, or Mixnet. until Wormhole is running over Tor, I2P, or Mixnet.
@@ -541,11 +555,18 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
{/* CONTENT AREA */} {/* CONTENT AREA */}
<div className="flex-1 overflow-hidden flex flex-col min-h-0"> <div className="flex-1 overflow-hidden flex flex-col min-h-0">
{activeTab === 'dms' && (
<AgentShellPanel
active={expanded && activeTab === 'dms'}
expanded={shellExpanded}
onExpandedChange={setShellExpanded}
/>
)}
{dashboardRestrictedTab && ( {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="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="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"> <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>
<div className="text-sm font-mono tracking-[0.24em] text-cyan-300 mb-2"> <div className="text-sm font-mono tracking-[0.24em] text-cyan-300 mb-2">
{dashboardRestrictedTitle} {dashboardRestrictedTitle}
@@ -1484,796 +1505,30 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
</> </>
)} )}
{/* ─── Dead Drop Tab ─── */} {/* Dead Drop chat UI: Infonet Terminal → Messages */}
{!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">
{dmView === 'chat' ? (
<>
<button
onClick={() => {
setDmView('contacts');
setSelectedContact('');
setDmMessages([]);
}}
className="text-[13px] font-mono text-[var(--text-muted)] hover:text-cyan-400 transition-colors"
>
&lt; BACK
</button>
<span className="text-sm font-mono text-cyan-400 ml-2 truncate">
{selectedContact.slice(0, 16)}
</span>
{(() => {
const c = contacts[selectedContact];
if (!c) return null;
const trust = getContactTrustSummary(c);
if (trust?.transparencyConflict) {
return (
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-red-500/40 text-red-300 bg-red-950/20">
HISTORY CONFLICT
</span>
);
}
if (trust?.state === 'continuity_broken') {
return (
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-red-500/40 text-red-300 bg-red-950/20">
CONTINUITY BROKEN
</span>
);
}
if (trust?.state === 'mismatch') {
return (
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-orange-500/40 text-orange-300 bg-orange-950/20">
PREKEY CHANGED
</span>
);
}
if (trust?.registryMismatch) {
return (
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-red-500/40 text-red-400 bg-red-950/20">
KEY MISMATCH
</span>
);
}
if (trust?.state === 'sas_verified') {
return (
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-green-500/40 text-green-400 bg-green-950/20">
SAS VERIFIED
</span>
);
}
if (trust?.state === 'invite_pinned') {
return (
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-emerald-500/40 text-emerald-300 bg-emerald-950/20">
INVITE PINNED
</span>
);
}
if (trust?.state === 'tofu_pinned') {
return (
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-amber-500/30 text-amber-300 bg-amber-950/10">
TOFU ONLY
</span>
);
}
return null;
})()}
{(() => {
const c = contacts[selectedContact];
if (!c) return null;
if (c.witness_count && c.witness_count > 0) {
return (
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-cyan-500/30 text-cyan-300 bg-cyan-950/10">
WITNESSED {c.witness_count}
</span>
);
}
return null;
})()}
{(() => {
const c = contacts[selectedContact];
if (!c) return null;
if (c.vouch_count && c.vouch_count > 0) {
return (
<span className="ml-2 text-[12px] font-mono px-1.5 py-0.5 border border-purple-500/30 text-purple-300 bg-purple-950/10">
VOUCHES {c.vouch_count}
</span>
);
}
return null;
})()}
<button
onClick={handleDmTrustPrimaryAction}
className="ml-auto text-[12px] font-mono px-2 py-0.5 border border-cyan-800/40 text-cyan-400/90 hover:text-cyan-300 hover:border-cyan-600/60 transition-colors"
>
{dmTrustPrimaryButtonLabel}
</button>
<button
onClick={() => handleVouch(selectedContact)}
className="ml-2 text-[12px] font-mono px-2 py-0.5 border border-purple-800/40 text-purple-400/90 hover:text-purple-300 hover:border-purple-600/60 transition-colors"
>
VOUCH
</button>
<button
onClick={() => void handleRefreshSelectedContact()}
disabled={dmMaintenanceBusy}
className="ml-2 text-[12px] font-mono px-2 py-0.5 border border-amber-800/40 text-amber-300/90 hover:text-amber-200 hover:border-amber-600/60 transition-colors disabled:opacity-40"
>
REFRESH
</button>
<button
onClick={() => void handleResetSelectedContact()}
disabled={dmMaintenanceBusy}
className="ml-2 text-[12px] font-mono px-2 py-0.5 border border-red-800/40 text-red-300/90 hover:text-red-200 hover:border-red-600/60 transition-colors disabled:opacity-40"
>
RESET
</button>
</>
) : (
<>
<button
onClick={() => setDmView('contacts')}
className={`text-[13px] font-mono px-2 py-0.5 transition-colors ${
dmView === 'contacts'
? 'text-cyan-400 bg-cyan-950/30'
: 'text-[var(--text-muted)] hover:text-gray-400'
}`}
>
CONTACTS
</button>
<button
onClick={() => setDmView('inbox')}
className={`text-[13px] font-mono px-2 py-0.5 transition-colors flex items-center gap-1 ${
dmView === 'inbox'
? 'text-cyan-400 bg-cyan-950/30'
: 'text-[var(--text-muted)] hover:text-gray-400'
}`}
>
INBOX
{accessRequests.length > 0 && (
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400 animate-[blink_1s_step-end_infinite]" />
)}
</button>
<button
onClick={() => setDmView('muted')}
className={`text-[13px] font-mono px-2 py-0.5 transition-colors flex items-center gap-1 ${
dmView === 'muted'
? 'text-cyan-400 bg-cyan-950/30'
: 'text-[var(--text-muted)] hover:text-gray-400'
}`}
>
<EyeOff size={8} />
MUTED
{mutedArray.length > 0 && (
<span className="text-[11px] text-[var(--text-muted)]">
({mutedArray.length})
</span>
)}
</button>
<button
onClick={() => setShowAddContact(!showAddContact)}
disabled={secureDmBlocked}
className="ml-auto p-1 hover:bg-[var(--hover-accent)] text-[var(--text-muted)] hover:text-cyan-400 transition-colors"
title="Request access"
>
<UserPlus size={11} />
</button>
</>
)}
</div>
{dmView === 'chat' && showSas && sasPhrase && (
<div className="px-3 pb-1 text-[13px] font-mono text-cyan-400/80 border-b border-[var(--border-primary)]/20">
SAS: <span className="text-cyan-300">{sasPhrase}</span>
{selectedContactInfo &&
selectedContactTrustSummary?.state === 'invite_pinned' && (
<div className="mt-1 text-[12px] font-mono text-emerald-300/90 leading-[1.65]">
This contact was anchored by an imported signed invite. SAS is still useful
as an extra continuity check.
</div>
)}
{selectedContactInfo &&
selectedContactTrustSummary?.state === 'tofu_pinned' && (
<div className="mt-1 text-[12px] font-mono text-amber-300/90 leading-[1.65]">
First contact is still TOFU-only. Compare this phrase out of band before
treating the sender as verified.
</div>
)}
{selectedContactInfo &&
selectedContactTrustSummary?.state !== 'sas_verified' &&
selectedContactTrustSummary?.state !== 'mismatch' &&
selectedContactTrustSummary?.state !== 'continuity_broken' &&
!selectedContactTrustSummary?.transparencyConflict && (
<div className="mt-2 flex items-center gap-1.5">
<input
value={sasConfirmInput}
onChange={(e) => setSasConfirmInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
void handleConfirmSelectedContactSas();
}
}}
placeholder="Type the phrase you both confirmed"
className="flex-1 min-w-0 bg-black/30 border border-cyan-900/30 px-2 py-1 text-[12px] font-mono text-cyan-100 placeholder:text-cyan-700/70 focus:outline-none focus:border-cyan-600/60"
/>
<button
onClick={() => void handleConfirmSelectedContactSas()}
disabled={dmMaintenanceBusy}
className="text-[12px] font-mono px-2 py-1 border border-emerald-800/40 text-emerald-300 hover:text-emerald-200 hover:border-emerald-600/60 transition-colors disabled:opacity-40"
>
CONFIRM SAS
</button>
</div>
)}
{selectedContactInfo &&
selectedContactTrustSummary?.state === 'continuity_broken' &&
selectedContactTrustSummary?.rootMismatch && (
<>
<div className="mt-1 text-[12px] font-mono text-red-300/90 leading-[1.65]">
{`${rootWitnessContinuityLabel(selectedContactTrustSummary)} changed for this contact.`}{' '}
Compare the SAS phrase for the newly observed root, then recover only if
the ceremony checks out.
</div>
<div className="mt-2 flex items-center gap-1.5">
<input
value={sasConfirmInput}
onChange={(e) => setSasConfirmInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
void handleRecoverSelectedContactRootContinuity();
}
}}
placeholder="Type the phrase you both confirmed for the new root"
className="flex-1 min-w-0 bg-black/30 border border-red-900/30 px-2 py-1 text-[12px] font-mono text-cyan-100 placeholder:text-red-700/70 focus:outline-none focus:border-red-600/60"
/>
<button
onClick={() => void handleRecoverSelectedContactRootContinuity()}
disabled={dmMaintenanceBusy}
className="text-[12px] font-mono px-2 py-1 border border-red-800/40 text-red-300 hover:text-red-200 hover:border-red-600/60 transition-colors disabled:opacity-40"
>
RECOVER ROOT
</button>
</div>
</>
)}
{selectedContactInfo?.remotePrekeyMismatch && (
<div className="mt-2 text-[12px] font-mono text-red-300/85 leading-[1.65]">
{selectedContactTrustSummary?.rootMismatch
? `${rootWitnessContinuityLabel(selectedContactTrustSummary)} changed. Recover only after you compare the new SAS phrase out of band.`
: 'Acknowledge the changed fingerprint first, then compare and confirm SAS again.'}
</div>
)}
</div>
)}
{activeTab === 'dms' && !secureDmBlocked && (
<div className="px-3 py-1.5 border-b border-[var(--border-primary)]/20 shrink-0 flex items-center gap-2">
<span
className={`text-[12px] font-mono px-1.5 py-0.5 border ${dmTransportStatus.className}`}
>
{dmTransportStatus.label}
</span>
<span className="text-[12px] font-mono text-[var(--text-muted)]">
{dmTransportMode === 'reticulum'
? 'Direct private delivery active.'
: dmTransportMode === 'hidden'
? 'Hidden transport active.'
: dmTransportMode === 'relay'
? 'Relay fallback active.'
: dmTransportMode === 'ready'
? 'Private lane ready.'
: 'Lower-trust mode.'}
</span>
</div>
)}
{activeTab === 'dms' && unresolvedSenderSealCount > 0 && (
<div className="px-3 py-2 border-b border-red-900/30 bg-red-950/18 text-red-300 leading-[1.65] shrink-0">
<div className="text-[13px] font-mono tracking-[0.18em] mb-1">
UNRESOLVED SEALED SENDERS
</div>
<div className="text-sm font-mono">
{unresolvedSenderSealCount} sealed-sender message
{unresolvedSenderSealCount === 1 ? '' : 's'} could not be mapped to a
trusted contact or verified sender key. Keep Wormhole reachable and refresh
contact trust before relying on them.
</div>
</div>
)}
{activeTab === 'dms' && dmView === 'chat' && dmTrustHint && selectedContactInfo && (
<div
className={`px-3 py-2 border-b leading-[1.65] shrink-0 ${
dmTrustHint.severity === 'danger'
? 'border-red-900/30 bg-red-950/20 text-red-300'
: 'border-amber-900/30 bg-amber-950/10 text-amber-200'
}`}
>
<div className="flex items-start gap-2">
<div className="flex-1 min-w-0">
<div className="text-[13px] font-mono tracking-[0.18em] mb-1">
{dmTrustHint.title}
</div>
<div className="text-sm font-mono">{dmTrustHint.detail}</div>
{selectedContactInfo.remotePrekeyMismatch && (
<div className="mt-2 text-[13px] font-mono text-red-200/85">
pinned {shortTrustFingerprint(selectedContactInfo.remotePrekeyFingerprint)} observed{' '}
{shortTrustFingerprint(selectedContactInfo.remotePrekeyObservedFingerprint)}
</div>
)}
{!selectedContactInfo.remotePrekeyMismatch &&
selectedContactInfo.remotePrekeyRootMismatch && (
<div className="mt-2 text-[13px] font-mono text-red-200/85">
pinned root {shortTrustFingerprint(selectedContactInfo.remotePrekeyRootFingerprint)}
observed root{' '}
{shortTrustFingerprint(selectedContactInfo.remotePrekeyObservedRootFingerprint)}
</div>
)}
{!selectedContactInfo.remotePrekeyMismatch &&
selectedContactTrustSummary?.state === 'tofu_pinned' &&
selectedContactInfo.remotePrekeyFingerprint && (
<div className="mt-2 text-[13px] font-mono text-amber-200/85">
first-sight pin {shortTrustFingerprint(selectedContactInfo.remotePrekeyFingerprint)}
verify before sensitive use
</div>
)}
{!selectedContactInfo.remotePrekeyMismatch &&
selectedContactTrustSummary?.state === 'invite_pinned' &&
(selectedContactInfo.invitePinnedTrustFingerprint ||
selectedContactInfo.remotePrekeyFingerprint) && (
<div className="mt-2 text-[13px] font-mono text-emerald-200/85">
invite pin{' '}
{shortTrustFingerprint(
selectedContactInfo.invitePinnedTrustFingerprint ||
selectedContactInfo.remotePrekeyFingerprint,
)}{' '}
{selectedContactTrustSummary?.rootAttested &&
(selectedContactInfo.invitePinnedRootFingerprint ||
selectedContactInfo.remotePrekeyRootFingerprint)
? ` ${rootWitnessBadgeLabel(selectedContactTrustSummary).toLowerCase()} ${shortTrustFingerprint(
selectedContactInfo.invitePinnedRootFingerprint ||
selectedContactInfo.remotePrekeyRootFingerprint,
)} `
: ''}{' '}
imported out of band before first contact
</div>
)}
{selectedContactTrustSummary?.state === 'continuity_broken' &&
selectedContactTrustSummary?.rootMismatch && (
<div className="mt-2 text-[13px] font-mono text-red-200/85 leading-[1.7]">
{`${rootWitnessContinuityLabel(selectedContactTrustSummary).toLowerCase()} broke for this contact.`}{' '}
Re-verify SAS or replace the signed invite before trusting the new
root.
</div>
)}
{selectedContactInfo.remotePrekeyTransparencyConflict && (
<div className="mt-2 text-[13px] font-mono text-red-200/85 leading-[1.7]">
prekey history conflict observed and trust stays degraded until you
explicitly acknowledge the changed fingerprint.
</div>
)}
{selectedContactInfo.remotePrekeyLookupMode === 'legacy_agent_id' && (
<div className="mt-2 text-[13px] font-mono text-yellow-200/85 leading-[1.7]">
bootstrap path: legacy direct agent ID lookup.
{selectedContactInfo.invitePinnedPrekeyLookupHandle
? ' Refresh from the signed invite to tighten lookup privacy.'
: ' Import or re-import a signed invite to avoid stable-ID lookup.'}
</div>
)}
{selectedContactInfo.remotePrekeyLookupMode === 'invite_lookup_handle' && (
<div className="mt-2 text-[13px] font-mono text-cyan-200/85 leading-[1.7]">
bootstrap path: invite-scoped lookup handle. Stable agent ID was not
required on the lookup path.
</div>
)}
{(selectedContactInfo.witness_count ?? 0) > 0 && (
<div className="mt-2 text-[13px] font-mono text-cyan-200/75 leading-[1.7]">
witness observations: {selectedContactInfo.witness_count}
{selectedContactInfo.witness_checked_at
? `, last seen ${timeAgo(
selectedContactInfo.witness_checked_at > 1_000_000_000_000
? selectedContactInfo.witness_checked_at
: selectedContactInfo.witness_checked_at * 1000,
)}`
: ''}
</div>
)}
</div>
<div className="flex items-center gap-1.5 shrink-0">
<button
onClick={handleDmTrustPrimaryAction}
className="text-[12px] font-mono px-2 py-0.5 border border-cyan-800/40 text-cyan-300 hover:text-cyan-200 hover:border-cyan-600/60 transition-colors"
>
{dmTrustPrimaryButtonLabel}
</button>
{selectedContactInfo.remotePrekeyMismatch &&
!selectedContactTrustSummary?.rootMismatch && (
<button
onClick={() => void handleTrustSelectedRemotePrekey()}
disabled={dmMaintenanceBusy}
className="text-[12px] font-mono px-2 py-0.5 border border-orange-700/40 text-orange-300 hover:text-orange-200 hover:border-orange-500/60 transition-colors disabled:opacity-40"
>
TRUST NEW KEY
</button>
)}
</div>
</div>
</div>
)}
{/* Add contact / request access form */}
<AnimatePresence>
{showAddContact && dmView !== 'chat' && !secureDmBlocked && (
<motion.div
initial={{ height: 0 }}
animate={{ height: 'auto' }}
exit={{ height: 0 }}
className="overflow-hidden border-b border-[var(--border-primary)]/30 shrink-0"
>
<div className="px-3 py-2 space-y-1.5">
<div className="text-[13px] font-mono text-[var(--text-muted)] leading-[1.65]">
Enter an Agent ID for a contact you already pinned with a signed invite
to request Dead Drop access. If you only have older local state, use
terminal <span className="text-yellow-400">dm add</span> only for
legacy migration.
</div>
<div className="flex items-center gap-1.5">
<input
value={addContactId}
onChange={(e) => setAddContactId(e.target.value)}
placeholder="!sb_a3f2c891..."
className="flex-1 bg-[var(--bg-secondary)]/50 border border-[var(--border-primary)] text-sm font-mono text-cyan-300 px-2 py-1 outline-none placeholder:text-[var(--text-muted)]"
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleRequestComposerAction();
}
}}
/>
<button
onClick={handleRequestComposerAction}
disabled={!addContactId.trim() || !hasId}
className="text-[13px] font-mono px-2 py-1 bg-cyan-900/20 text-cyan-400 hover:bg-cyan-800/30 disabled:opacity-30 transition-colors"
>
REQUEST
</button>
</div>
{pendingSent.includes(addContactId.trim()) && (
<div className="text-[13px] font-mono text-yellow-500/70">
Request already sent
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
{/* Content area */}
<div className="flex-1 overflow-y-auto styled-scrollbar px-3 py-1.5 space-y-0.5 border-l-2 border-cyan-800/25">
{secureDmBlocked && (
<div className="flex h-full min-h-[220px] items-center justify-center py-6">
<div className="max-w-sm w-full border border-cyan-900/30 bg-cyan-950/10 px-4 py-5 text-center">
<div className="inline-flex items-center justify-center w-10 h-10 border border-cyan-700/40 bg-black/30 text-cyan-300 mb-3">
<Lock size={16} />
</div>
<div className="text-sm font-mono tracking-[0.24em] text-cyan-300 mb-2">
DEAD DROP LOCKED
</div>
<div className="text-sm font-mono text-[var(--text-secondary)] leading-[1.7]">
Need Wormhole activated.
</div>
<div className="mt-2 text-[13px] font-mono text-cyan-300/70">
Contacts, inbox, and private messages unlock once the private lane is up.
</div>
</div>
</div>
)}
{/* CONTACTS VIEW */}
{!secureDmBlocked && dmView === 'contacts' && (
<>
{contactList.length === 0 && (
<div className="text-sm font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]">
No contacts yet. Use <span className="text-cyan-500/70">+</span> to
request access.
</div>
)}
{contactList.map(([id, c]) => {
const trust = getContactTrustSummary(c);
return (
<div
key={id}
className="flex items-center gap-2 py-1.5 border-b border-[var(--border-primary)]/30 last:border-0 cursor-pointer hover:bg-[var(--bg-secondary)]/50 px-1 -mx-1 transition-colors"
onClick={() => openChat(id)}
>
<Lock size={10} className="text-[var(--text-muted)] shrink-0" />
<span className="text-sm font-mono text-cyan-300 truncate">
{c.alias || id.slice(0, 16)}
</span>
{c.remotePrekeyMismatch && (
<span className="text-[11px] font-mono px-1.5 py-0.5 border border-orange-500/40 text-orange-300 bg-orange-950/20">
REVERIFY
</span>
)}
{!c.remotePrekeyMismatch && c.verify_mismatch && (
<span className="text-[11px] font-mono px-1.5 py-0.5 border border-red-500/40 text-red-300 bg-red-950/20">
MISMATCH
</span>
)}
{!c.remotePrekeyMismatch && !c.verify_mismatch && trust?.state === 'invite_pinned' && (
<span className="text-[11px] font-mono px-1.5 py-0.5 border border-emerald-500/40 text-emerald-300 bg-emerald-950/20">
INVITE PINNED
</span>
)}
{!c.remotePrekeyMismatch && !c.verify_mismatch && trust?.state === 'sas_verified' && (
<span className="text-[11px] font-mono px-1.5 py-0.5 border border-green-500/40 text-green-400 bg-green-950/20">
SAS VERIFIED
</span>
)}
{!c.remotePrekeyMismatch &&
!c.verify_mismatch &&
!c.remotePrekeyTransparencyConflict &&
c.remotePrekeyLookupMode === 'legacy_agent_id' && (
<span className="text-[11px] font-mono px-1.5 py-0.5 border border-yellow-500/30 text-yellow-300 bg-yellow-950/10">
LEGACY LOOKUP
</span>
)}
{!c.remotePrekeyMismatch && !c.verify_mismatch && c.remotePrekeyTransparencyConflict && (
<span className="text-[11px] font-mono px-1.5 py-0.5 border border-red-500/40 text-red-300 bg-red-950/20">
HISTORY CONFLICT
</span>
)}
{!c.remotePrekeyMismatch &&
!c.verify_mismatch &&
trust?.state === 'tofu_pinned' && (
<span className="text-[11px] font-mono px-1.5 py-0.5 border border-amber-500/30 text-amber-300 bg-amber-950/10">
TOFU ONLY
</span>
)}
<button
onClick={(e) => {
e.stopPropagation();
handleBlockDM(id);
}}
className="ml-auto p-0.5 text-[var(--text-muted)] hover:text-red-400 hover:bg-red-900/20 transition-colors"
title="Block"
>
<Ban size={10} />
</button>
</div>
);
})}
{pendingSent.length > 0 && (
<>
<div className="text-[13px] font-mono text-[var(--text-muted)] mt-2 mb-1">
PENDING SENT
</div>
{pendingSent.map((id) => (
<div
key={id}
className="flex items-center gap-2 py-1 text-sm font-mono text-[var(--text-muted)]"
>
<span className="w-1.5 h-1.5 rounded-full bg-yellow-600/50" />
<span className="truncate">{id.slice(0, 16)}</span>
<span className="ml-auto text-[12px] text-[var(--text-muted)]">
awaiting
</span>
</div>
))}
</>
)}
</>
)}
{/* INBOX VIEW — access requests */}
{!secureDmBlocked && dmView === 'inbox' && (
<>
{accessRequests.length === 0 && (
<div className="text-sm font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]">
No incoming requests
</div>
)}
{accessRequests.map((req) => {
const requestActionsAllowed = shouldAllowRequestActions(req);
const recoveryState = req.sender_recovery_state;
return (
<div
key={req.sender_id}
className="py-2 border-b border-[var(--border-primary)]/30 last:border-0"
>
<div className="flex items-center gap-1.5">
<UserPlus size={10} className="text-cyan-500 shrink-0" />
<span className="text-sm font-mono text-cyan-300 truncate">
{req.sender_id.slice(0, 16)}
</span>
{recoveryState === 'verified' && (
<span className="text-[12px] font-mono px-1.5 py-0.5 border border-green-500/30 text-green-400 bg-green-950/20">
VERIFIED
</span>
)}
{recoveryState === 'pending' && (
<span className="text-[12px] font-mono px-1.5 py-0.5 border border-yellow-500/30 text-yellow-300 bg-yellow-950/20">
RECOVERY PENDING
</span>
)}
{recoveryState === 'failed' && (
<span className="text-[12px] font-mono px-1.5 py-0.5 border border-red-500/30 text-red-300 bg-red-950/20">
RECOVERY FAILED
</span>
)}
<span className="text-[12px] font-mono text-[var(--text-muted)] ml-auto shrink-0">
{timeAgo(req.timestamp)}
</span>
</div>
<div className="text-[13px] font-mono text-[var(--text-muted)] mt-0.5 leading-[1.65]">
Requesting Dead Drop access
</div>
{req.geo_hint && (
<div className="text-[12px] font-mono text-[var(--text-muted)] mt-0.5">
Geo hint (not proof): {req.geo_hint}
</div>
)}
{!requestActionsAllowed && (
<div className="text-[12px] font-mono text-yellow-300 mt-0.5 leading-[1.65]">
Sender authority is not verified yet. Actions stay disabled until
local recovery succeeds.
</div>
)}
<div className="flex items-center gap-1.5 mt-1.5">
<button
onClick={() => handleAcceptRequest(req.sender_id)}
disabled={!requestActionsAllowed}
className={`flex items-center gap-1 text-[13px] font-mono px-2 py-0.5 transition-colors ${
requestActionsAllowed
? 'bg-cyan-900/20 text-cyan-400 hover:bg-cyan-800/30'
: 'bg-cyan-950/10 text-cyan-700 cursor-not-allowed opacity-50'
}`}
>
<Check size={9} /> ACCEPT
</button>
<button
onClick={() => handleDenyRequest(req.sender_id)}
disabled={!requestActionsAllowed}
className={`flex items-center gap-1 text-[13px] font-mono px-2 py-0.5 transition-colors ${
requestActionsAllowed
? 'bg-gray-900/30 text-gray-400 hover:bg-gray-800/40'
: 'bg-gray-950/20 text-gray-600 cursor-not-allowed opacity-50'
}`}
>
<X size={9} /> DENY
</button>
<button
onClick={() => handleBlockDM(req.sender_id)}
disabled={!requestActionsAllowed}
className={`flex items-center gap-1 text-[13px] font-mono px-2 py-0.5 ml-auto transition-colors ${
requestActionsAllowed
? 'text-[var(--text-muted)] hover:text-red-400 hover:bg-red-900/20'
: 'text-[var(--text-muted)] opacity-50 cursor-not-allowed'
}`}
>
<Ban size={9} /> BLOCK
</button>
</div>
</div>
);
})}
</>
)}
{/* MUTED LIST VIEW */}
{!secureDmBlocked && dmView === 'muted' && (
<>
{mutedArray.length === 0 && (
<div className="text-sm font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]">
No muted users
</div>
)}
{mutedArray.map((uid) => (
<div
key={uid}
className="flex items-center gap-2 py-1.5 border-b border-[var(--border-primary)]/30 last:border-0 px-1 -mx-1"
>
<EyeOff size={10} className="text-[var(--text-muted)] shrink-0" />
<span className="text-sm font-mono text-[var(--text-secondary)] truncate flex-1">
{uid.slice(0, 20)}
</span>
<button
onClick={() => handleUnmute(uid)}
className="flex items-center gap-1 text-[12px] font-mono px-2 py-0.5 bg-cyan-900/20 text-cyan-500 hover:bg-cyan-800/30 transition-colors"
>
<Eye size={8} /> UNMUTE
</button>
</div>
))}
</>
)}
{/* CHAT VIEW */}
{!secureDmBlocked && dmView === 'chat' && (
<>
{dmMessages.length === 0 && (
<div className="text-sm font-mono text-[var(--text-muted)] text-center py-4 leading-[1.65]">
<Lock size={11} className="inline mr-1 mb-0.5" />
E2E encrypted dead drop no messages yet
</div>
)}
{dmMessages.map((m) => (
<div key={m.msg_id} className="py-0.5 leading-[1.65]">
<div className="flex gap-1.5 text-sm font-mono">
<span
className={`shrink-0 ${
m.sender_id === identity?.nodeId
? 'text-cyan-500'
: 'text-cyan-400'
}`}
>
{m.sender_id === identity?.nodeId
? 'you'
: m.sender_id.slice(0, 12)}
</span>
{m.sender_id !== identity?.nodeId && m.seal_verified === true && (
<span className="text-[12px] font-mono px-1.5 py-0.5 border border-green-500/30 text-green-400 bg-green-950/20">
VERIFIED
</span>
)}
{m.sender_id !== identity?.nodeId && m.seal_resolution_failed && (
<span className="text-[12px] font-mono px-1.5 py-0.5 border border-red-500/30 text-red-300 bg-red-950/20">
SEAL UNRESOLVED
</span>
)}
{m.sender_id !== identity?.nodeId &&
!m.seal_resolution_failed &&
m.seal_verified === false && (
<span className="text-[12px] font-mono px-1.5 py-0.5 border border-red-500/30 text-red-400 bg-red-950/20">
UNVERIFIED
</span>
)}
{m.transport && (
<span
className={`text-[12px] font-mono px-1.5 py-0.5 border ${
m.transport === 'reticulum'
? 'border-green-500/30 text-green-400 bg-green-950/20'
: 'border-yellow-500/30 text-yellow-400 bg-yellow-950/20'
}`}
>
{m.transport === 'reticulum' ? 'DIRECT' : 'RELAY'}
</span>
)}
<span className="text-[var(--text-secondary)] break-words whitespace-pre-wrap flex-1">
{m.plaintext || '[encrypted]'}
</span>
<span className="text-[var(--text-muted)] shrink-0 text-[13px]">
{timeAgo(m.timestamp)}
</span>
</div>
</div>
))}
</>
)}
<div ref={messagesEndRef} />
</div>
</>
)}
</div> </div>
{/* INPUT BAR */} {/* 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">
Real local shell over PTY. EXPAND widens this panel; SNAP docks it back into Mesh Chat.
</div>
</div>
) : dashboardRestrictedTab ? (
<div className="mx-2 mb-2 mt-1 border border-cyan-800/40 bg-black/30 shrink-0 relative"> <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"> <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 ACCESS
</span> </span>
<div className="px-3 py-3 flex flex-col gap-2"> <div className="px-3 py-3 flex flex-col gap-2">
<div className="text-[12px] font-mono tracking-widest text-[var(--text-muted)] uppercase"> <div className="text-[12px] font-mono tracking-widest text-[var(--text-muted)] uppercase">
{activeTab === 'infonet' PRIVATE INFONET / TERMINAL ONLY
? '→ PRIVATE INFONET / TERMINAL ONLY'
: '→ DEAD DROP / TERMINAL ONLY'}
</div> </div>
<div className="text-[13px] font-mono text-[var(--text-secondary)] leading-[1.65]"> <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.
? '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.'}
</div> </div>
<button <button
onClick={openTerminal} onClick={openTerminal}
@@ -2328,8 +1583,7 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
? privateInfonetReady ? privateInfonetReady
? `→ INFONET${selectedGate ? ` / ${selectedGate}` : ''}${privateInfonetTransportReady ? '' : ' / EXPERIMENTAL ENCRYPTION'}` ? `→ INFONET${selectedGate ? ` / ${selectedGate}` : ''}${privateInfonetTransportReady ? '' : ' / EXPERIMENTAL ENCRYPTION'}`
: '→ PRIVATE LANE LOCKED' : '→ PRIVATE LANE LOCKED'
: activeTab === 'meshtastic' : canUsePublicMeshInput
? canUsePublicMeshInput
? meshDirectTarget ? meshDirectTarget
? `→ MESH / TO ${meshDirectTarget.toUpperCase()} / FROM ${activePublicMeshAddress.toUpperCase()}` ? `→ MESH / TO ${meshDirectTarget.toUpperCase()} / FROM ${activePublicMeshAddress.toUpperCase()}`
: `→ MESH / ${meshRegion} / ${meshChannel} / ${activePublicMeshAddress.toUpperCase()}` : `→ MESH / ${meshRegion} / ${meshChannel} / ${activePublicMeshAddress.toUpperCase()}`
@@ -2337,12 +1591,7 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
? '→ MESH BLOCKED / WORMHOLE ACTIVE' ? '→ MESH BLOCKED / WORMHOLE ACTIVE'
: hasStoredPublicLaneIdentity : hasStoredPublicLaneIdentity
? '→ MESH OFF' ? '→ MESH OFF'
: '→ MESH LOCKED' : '→ MESH LOCKED'}
: activeTab === 'dms' && secureDmBlocked
? '→ DEAD DROP LOCKED'
: dmView === 'chat' && selectedContact
? `→ DEAD DROP / ${selectedContact.slice(0, 14)}`
: '→ SELECT TARGET'}
</span> </span>
)} )}
</div> </div>
@@ -2373,19 +1622,6 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
OPEN PRIVATE LANE BRIEF OPEN PRIVATE LANE BRIEF
</span> </span>
</button> </button>
) : activeTab === 'dms' && secureDmBlocked ? (
<button
onClick={() => setDeadDropUnlockOpen(true)}
className="w-full flex items-center justify-between gap-2 px-3 py-2 border border-cyan-700/40 bg-cyan-950/15 text-cyan-300 hover:bg-cyan-950/25 hover:border-cyan-500/50 transition-colors"
>
<span className="inline-flex items-center gap-2 text-sm font-mono tracking-[0.2em]">
<Lock size={11} />
UNLOCK DEAD DROP
</span>
<span className="text-[12px] font-mono text-cyan-300/70">
NEED WORMHOLE
</span>
</button>
) : activeTab === 'meshtastic' && !canUsePublicMeshInput ? ( ) : activeTab === 'meshtastic' && !canUsePublicMeshInput ? (
<button <button
onClick={handleMeshActivationAction} onClick={handleMeshActivationAction}
@@ -4122,12 +4122,10 @@ export function useMeshChatController({
const dmTrustHint = buildDmTrustHint(selectedContactInfo); const dmTrustHint = buildDmTrustHint(selectedContactInfo);
const dmTrustPrimaryAction = dmTrustPrimaryActionLabel(selectedContactInfo); const dmTrustPrimaryAction = dmTrustPrimaryActionLabel(selectedContactInfo);
const wormholeDescriptor = getWormholeIdentityDescriptor(); const wormholeDescriptor = getWormholeIdentityDescriptor();
const dashboardRestrictedTab: boolean = activeTab === 'infonet' || activeTab === 'dms'; const dashboardRestrictedTab: boolean = activeTab === 'infonet';
const dashboardRestrictedTitle = activeTab === 'infonet' ? 'INFONET RESTRICTED' : 'DEAD DROP RESTRICTED'; const dashboardRestrictedTitle = 'INFONET RESTRICTED';
const dashboardRestrictedDetail = const dashboardRestrictedDetail =
activeTab === 'infonet' 'Private Wormhole gate activity is staying in the terminal for this build. Dashboard integration is coming soon.';
? '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.';
const selectedGateKey = selectedGate.trim().toLowerCase(); const selectedGateKey = selectedGate.trim().toLowerCase();
const selectedGatePersonaList = selectedGateKey ? gatePersonas[selectedGateKey] || [] : []; const selectedGatePersonaList = selectedGateKey ? gatePersonas[selectedGateKey] || [] : [];
const selectedGateActivePersonaId = selectedGateKey ? activeGatePersonaId[selectedGateKey] || '' : ''; const selectedGateActivePersonaId = selectedGateKey ? activeGatePersonaId[selectedGateKey] || '' : '';
+13
View File
@@ -0,0 +1,13 @@
export function resolveAgentShellWsUrl(cwd?: string): string {
if (typeof window === 'undefined') return '';
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
const host = window.location.hostname || '127.0.0.1';
const port =
process.env.NEXT_PUBLIC_BACKEND_PORT ||
(window.location.port === '3000' ? '8000' : window.location.port || '8000');
const params = new URLSearchParams();
const trimmed = String(cwd || '').trim();
if (trimmed) params.set('cwd', trimmed);
const query = params.toString();
return `${protocol}://${host}:${port}/api/agent-shell/ws${query ? `?${query}` : ''}`;
}
+46 -5
View File
@@ -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 earthquakes, fires, GDELT conflict events, prediction markets, and 30+ other data
layers — all with geographic coordinates. 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`**⚡ &lt;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 ## How to Use This Skill
Import the client and call methods: 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 | | `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.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.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("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):** **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. fresh data, pushes alerts instantly, and eliminates blind polling.
2. **Does ShadowBroker have this data already?** 2. **Does ShadowBroker have this data already?**
- **Start with `get_summary()`** to see what layers are populated and their counts. - **Natural language**`await sb.ask(question)` (routes server-side)
- **Known domain** (flight callsign, ship name, keyword) → use the targeted command: - **Batch snapshot**`await sb.run_playbook("hot_snapshot")`
`find_flights`, `find_ships`, `search_news`, `entities_near`, `search_telemetry` - **Known domain**`find_entity`, `find_flights`, `find_ships`, `search_news`, `entities_near`
- **Unknown domain**`search_telemetry` (cross-layer keyword search, ranked results) - **Unknown domain**`find_entity` first; only then `search_telemetry` with `confirm_expensive=true`
- **Need specific layers**`get_layer_slice(["military_flights", "gdelt"])` — only - **Need specific layers**`get_layer_slice(["military_flights", "gdelt"])` — only
fetches layers that changed since your last call (per-layer incremental). 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) - **Near a location**`entities_near()` or `get_near_me()` (scans all layers within radius)
+28 -1
View File
@@ -424,6 +424,27 @@ def query_snapshots(
return results 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 # Main heartbeat handler
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -437,7 +458,13 @@ async def heartbeat(sb_client) -> list[str]:
messages = [] messages = []
try: try:
# 1. Pull fresh telemetry (fast + slow merged for full visibility) # 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() data = await sb_client.get_full_telemetry()
# 2. Run anomaly detection # 2. Run anomaly detection
+76
View File
@@ -270,6 +270,82 @@ class ShadowBrokerClient:
r = await self._get("/api/ai/channel/status") r = await self._get("/api/ai/channel/status")
return r.json() 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: async def send_batch(self, commands: list[dict]) -> dict:
"""Send multiple commands in a single HTTP round-trip. """Send multiple commands in a single HTTP round-trip.
+16
View File
@@ -24,6 +24,22 @@ entry_points:
requirements: requirements:
- httpx>=0.25.0 - 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 declared
capabilities: capabilities:
- live_telemetry # Real-time OSINT data (flights, ships, SIGINT, etc.) - live_telemetry # Real-time OSINT data (flights, ships, SIGINT, etc.)