mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-07-06 12:28:05 +02:00
Ship DM connect delivery, fleet pubkey lookup, OpenClaw Infonet agent, and relay auto-wormhole.
Auto-relay connect DMs with End Contact severing, signed fleet prekey lookup, OpenClaw private Infonet channel intents, headless relay Tor bootstrap on redeploy, and swarm/DM live verification scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -51,6 +51,10 @@ class Settings(BaseSettings):
|
||||
# When true, empty MESH_PEER_PUSH_SECRET uses the public fleet HMAC for seed join/announce.
|
||||
MESH_INFONET_FLEET_JOIN: bool = True
|
||||
MESH_INFONET_FLEET_JOIN_DISABLED: bool = False
|
||||
# Headless relay/seed compose: auto-enable Tor wormhole on startup so
|
||||
# docker compose redeploys keep the fleet onion reachable.
|
||||
MESH_INFONET_RELAY_AUTO_WORMHOLE: bool = False
|
||||
MESH_INFONET_RELAY_AUTO_WORMHOLE_DISABLED: bool = False
|
||||
MESH_BOOTSTRAP_SIGNER_ID: str = ""
|
||||
MESH_PEER_REGISTRY_ENABLED: bool = False
|
||||
MESH_PEER_REGISTRY_DISABLED: bool = False
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
"""Invite-scoped DM connect delivery: auto relay release and contact severance."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
CONNECT_AUTO_RELEASE_INTENTS = frozenset(
|
||||
{
|
||||
"invite_short_address",
|
||||
"invite_import",
|
||||
"contact_request",
|
||||
"contact_accept",
|
||||
"contact_offer",
|
||||
}
|
||||
)
|
||||
|
||||
INVITE_CONNECT_TRUST_LEVELS = frozenset({"invite_pinned", "sas_verified"})
|
||||
|
||||
|
||||
def _release_profile() -> str:
|
||||
try:
|
||||
from services.release_profiles import current_release_profile
|
||||
|
||||
return str(current_release_profile() or "dev")
|
||||
except Exception:
|
||||
return "dev"
|
||||
|
||||
|
||||
def grant_connect_relay_policy(
|
||||
recipient_id: str,
|
||||
*,
|
||||
reason: str = "connect_scoped_auto_release",
|
||||
) -> dict[str, Any]:
|
||||
"""Pre-authorize hidden relay delivery for an explicit connect target."""
|
||||
peer_key = str(recipient_id or "").strip()
|
||||
if not peer_key:
|
||||
return {"ok": False, "detail": "recipient_id required"}
|
||||
try:
|
||||
from services.mesh.mesh_relay_policy import grant_relay_policy
|
||||
|
||||
return grant_relay_policy(
|
||||
scope_type="dm_contact",
|
||||
scope_id=peer_key,
|
||||
profile=_release_profile(),
|
||||
hidden_transport_required=True,
|
||||
reason=str(reason or "connect_scoped_auto_release"),
|
||||
)
|
||||
except Exception as exc:
|
||||
return {"ok": False, "detail": str(exc) or type(exc).__name__}
|
||||
|
||||
|
||||
def revoke_connect_relay_policy(recipient_id: str) -> dict[str, Any]:
|
||||
peer_key = str(recipient_id or "").strip()
|
||||
if not peer_key:
|
||||
return {"ok": False, "detail": "recipient_id required"}
|
||||
try:
|
||||
from services.mesh.mesh_relay_policy import revoke_relay_policy
|
||||
|
||||
revoked = int(
|
||||
revoke_relay_policy(
|
||||
scope_type="dm_contact",
|
||||
scope_id=peer_key,
|
||||
profile=_release_profile(),
|
||||
)
|
||||
or 0
|
||||
)
|
||||
return {"ok": True, "revoked": revoked}
|
||||
except Exception as exc:
|
||||
return {"ok": False, "detail": str(exc) or type(exc).__name__}
|
||||
|
||||
|
||||
def recipient_has_invite_connect_scope(recipient_id: str) -> bool:
|
||||
peer_key = str(recipient_id or "").strip()
|
||||
if not peer_key:
|
||||
return False
|
||||
try:
|
||||
from services.mesh.mesh_wormhole_contacts import get_wormhole_dm_contact
|
||||
|
||||
contact = get_wormhole_dm_contact(peer_key) or {}
|
||||
except Exception:
|
||||
return False
|
||||
if str(contact.get("invitePinnedPrekeyLookupHandle", "") or "").strip():
|
||||
return True
|
||||
if str(contact.get("invitePinnedLookupPeerUrl", "") or "").strip():
|
||||
return True
|
||||
trust = str(contact.get("trust_level", "") or "").strip().lower()
|
||||
return trust in INVITE_CONNECT_TRUST_LEVELS
|
||||
|
||||
|
||||
def relay_push_peer_urls_for_payload(payload: dict[str, Any]) -> list[str]:
|
||||
urls: list[str] = []
|
||||
for raw in list(payload.get("relay_push_peer_urls") or []):
|
||||
normalized = str(raw or "").strip().rstrip("/")
|
||||
if normalized and normalized not in urls:
|
||||
urls.append(normalized)
|
||||
lookup_peer_url = str(payload.get("lookup_peer_url", "") or "").strip().rstrip("/")
|
||||
if lookup_peer_url:
|
||||
urls = [url for url in urls if url != lookup_peer_url]
|
||||
urls.insert(0, lookup_peer_url)
|
||||
recipient_id = str(payload.get("recipient_id", "") or "").strip()
|
||||
if recipient_id and not urls:
|
||||
try:
|
||||
from services.mesh.mesh_wormhole_contacts import get_wormhole_dm_contact
|
||||
|
||||
contact = get_wormhole_dm_contact(recipient_id) or {}
|
||||
pinned = str(contact.get("invitePinnedLookupPeerUrl", "") or "").strip().rstrip("/")
|
||||
if pinned:
|
||||
urls.append(pinned)
|
||||
except Exception:
|
||||
pass
|
||||
return urls
|
||||
|
||||
|
||||
def should_auto_release_dm_payload(payload: dict[str, Any]) -> bool:
|
||||
if str(payload.get("delivery_class", "") or "").strip().lower() != "request":
|
||||
return False
|
||||
intent = str(payload.get("connect_intent", "") or "").strip().lower()
|
||||
if intent in CONNECT_AUTO_RELEASE_INTENTS:
|
||||
return True
|
||||
if str(payload.get("lookup_peer_url", "") or "").strip():
|
||||
return True
|
||||
recipient_id = str(payload.get("recipient_id", "") or "").strip()
|
||||
return bool(recipient_id and recipient_has_invite_connect_scope(recipient_id))
|
||||
|
||||
|
||||
def enrich_connect_release_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Attach invite-owner relay hints used during private release."""
|
||||
enriched = dict(payload or {})
|
||||
recipient_id = str(enriched.get("recipient_id", "") or "").strip()
|
||||
lookup_peer_url = str(enriched.get("lookup_peer_url", "") or "").strip().rstrip("/")
|
||||
if not lookup_peer_url and recipient_id:
|
||||
try:
|
||||
from services.mesh.mesh_wormhole_contacts import get_wormhole_dm_contact
|
||||
|
||||
contact = get_wormhole_dm_contact(recipient_id) or {}
|
||||
lookup_peer_url = str(contact.get("invitePinnedLookupPeerUrl", "") or "").strip().rstrip("/")
|
||||
except Exception:
|
||||
lookup_peer_url = ""
|
||||
if lookup_peer_url:
|
||||
enriched["lookup_peer_url"] = lookup_peer_url
|
||||
push_urls = relay_push_peer_urls_for_payload(enriched)
|
||||
if push_urls:
|
||||
enriched["relay_push_peer_urls"] = push_urls
|
||||
return enriched
|
||||
|
||||
|
||||
def auto_release_connect_dm_outbox(*, outbox_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Grant scoped relay policy and approve release for invite-scoped connect traffic."""
|
||||
normalized_outbox = str(outbox_id or "").strip()
|
||||
enriched = enrich_connect_release_payload(payload)
|
||||
if not normalized_outbox:
|
||||
return {"ok": False, "detail": "missing outbox_id"}
|
||||
if not should_auto_release_dm_payload(enriched):
|
||||
return {"ok": True, "skipped": True, "reason": "not_connect_scoped"}
|
||||
recipient_id = str(enriched.get("recipient_id", "") or "").strip()
|
||||
if not recipient_id:
|
||||
return {"ok": False, "detail": "missing recipient_id"}
|
||||
grant = grant_connect_relay_policy(recipient_id)
|
||||
try:
|
||||
from services.mesh.mesh_private_outbox import private_delivery_outbox
|
||||
from services.mesh.mesh_private_release_worker import private_release_worker
|
||||
|
||||
private_delivery_outbox.approve_relay_release(normalized_outbox)
|
||||
private_release_worker.ensure_started()
|
||||
private_release_worker.wake()
|
||||
except Exception as exc:
|
||||
return {
|
||||
"ok": False,
|
||||
"detail": str(exc) or type(exc).__name__,
|
||||
"grant": grant,
|
||||
}
|
||||
return {
|
||||
"ok": True,
|
||||
"auto_released": True,
|
||||
"outbox_id": normalized_outbox,
|
||||
"recipient_id": recipient_id,
|
||||
"grant": grant,
|
||||
"relay_push_peer_urls": relay_push_peer_urls_for_payload(enriched),
|
||||
}
|
||||
@@ -1506,6 +1506,7 @@ class DMRelay:
|
||||
sender_token_hash: str = "",
|
||||
payload_format: str = "dm1",
|
||||
session_welcome: str = "",
|
||||
replication_peer_urls: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
with self._lock:
|
||||
self._refresh_from_shared_relay()
|
||||
@@ -1609,6 +1610,7 @@ class DMRelay:
|
||||
if envelope_for_push:
|
||||
self._replicate_envelope_to_peers_async(
|
||||
envelope=envelope_for_push,
|
||||
preferred_peer_urls=list(replication_peer_urls or []),
|
||||
)
|
||||
except Exception:
|
||||
metrics_inc("dm_replication_push_error")
|
||||
@@ -1716,6 +1718,7 @@ class DMRelay:
|
||||
self,
|
||||
*,
|
||||
envelope: dict[str, Any],
|
||||
preferred_peer_urls: list[str] | None = None,
|
||||
) -> None:
|
||||
"""Push an outbound DM envelope to every authenticated relay peer.
|
||||
|
||||
@@ -1747,7 +1750,15 @@ class DMRelay:
|
||||
authenticated_push_peer_urls,
|
||||
)
|
||||
|
||||
peers = authenticated_push_peer_urls()
|
||||
peers: list[str] = []
|
||||
for raw_url in list(preferred_peer_urls or []):
|
||||
normalized_preferred = normalize_peer_url(str(raw_url or "").strip())
|
||||
if normalized_preferred and normalized_preferred not in peers:
|
||||
peers.append(normalized_preferred)
|
||||
for peer_url in authenticated_push_peer_urls():
|
||||
normalized_peer = normalize_peer_url(str(peer_url or "").strip())
|
||||
if normalized_peer and normalized_peer not in peers:
|
||||
peers.append(normalized_peer)
|
||||
if not peers:
|
||||
return
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Auto-enable Tor wormhole transport on Infonet relay/seed nodes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from services.config import get_settings
|
||||
from services.wormhole_settings import read_wormhole_settings, write_wormhole_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def infonet_relay_auto_wormhole_requested() -> bool:
|
||||
settings = get_settings()
|
||||
if bool(settings.MESH_INFONET_RELAY_AUTO_WORMHOLE_DISABLED):
|
||||
return False
|
||||
if bool(settings.MESH_INFONET_RELAY_AUTO_WORMHOLE):
|
||||
return True
|
||||
if str(settings.MESH_BOOTSTRAP_SIGNER_PRIVATE_KEY or "").strip():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _relay_tor_wormhole_target_settings() -> dict[str, Any]:
|
||||
settings = get_settings()
|
||||
socks_port = int(settings.MESH_ARTI_SOCKS_PORT or 9050)
|
||||
return {
|
||||
"enabled": True,
|
||||
"transport": "tor_arti",
|
||||
"socks_proxy": f"socks5h://127.0.0.1:{socks_port}",
|
||||
"socks_dns": True,
|
||||
"anonymous_mode": True,
|
||||
}
|
||||
|
||||
|
||||
def _wormhole_settings_match(existing: dict[str, Any], target: dict[str, Any]) -> bool:
|
||||
return (
|
||||
bool(existing.get("enabled")) is bool(target["enabled"])
|
||||
and str(existing.get("transport", "")) == str(target["transport"])
|
||||
and str(existing.get("socks_proxy", "")) == str(target["socks_proxy"])
|
||||
and bool(existing.get("socks_dns", True)) is bool(target["socks_dns"])
|
||||
and bool(existing.get("anonymous_mode", False)) is bool(target["anonymous_mode"])
|
||||
)
|
||||
|
||||
|
||||
def ensure_infonet_relay_wormhole_ready(*, reason: str = "relay_auto") -> dict[str, Any]:
|
||||
"""Persist Tor wormhole settings and connect on relay/seed startup."""
|
||||
if not infonet_relay_auto_wormhole_requested():
|
||||
return {"ok": True, "skipped": True, "reason": "not_requested"}
|
||||
|
||||
from routers.ai_intel import _write_env_value
|
||||
from services.tor_hidden_service import tor_service
|
||||
from services.wormhole_supervisor import connect_wormhole, restart_wormhole
|
||||
|
||||
existing = read_wormhole_settings()
|
||||
target = _relay_tor_wormhole_target_settings()
|
||||
settings_updated = not _wormhole_settings_match(existing, target)
|
||||
updated = write_wormhole_settings(**target) if settings_updated else existing
|
||||
|
||||
tor_result: dict[str, Any] = {"ok": False, "detail": "not started"}
|
||||
try:
|
||||
tor_result = tor_service.start(target_port=8000)
|
||||
if tor_result.get("ok"):
|
||||
_write_env_value("MESH_ARTI_ENABLED", "true")
|
||||
get_settings.cache_clear()
|
||||
except Exception as exc:
|
||||
tor_result = {"ok": False, "detail": str(exc or type(exc).__name__)}
|
||||
|
||||
runtime = (
|
||||
restart_wormhole(reason=reason)
|
||||
if settings_updated
|
||||
else connect_wormhole(reason=reason)
|
||||
)
|
||||
|
||||
if settings_updated:
|
||||
logger.info("Infonet relay auto-wormhole enabled (%s)", reason)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"skipped": False,
|
||||
"settings_updated": settings_updated,
|
||||
"tor": tor_result,
|
||||
"runtime": runtime,
|
||||
"settings": updated,
|
||||
}
|
||||
@@ -125,8 +125,8 @@ def dm_lookup_response_view(
|
||||
view.pop("lookup_mode", None)
|
||||
view.pop("removal_target", None)
|
||||
return view
|
||||
if invite_lookup:
|
||||
view.pop("agent_id", None)
|
||||
# Successful invite lookups keep agent_id: the handle is the capability and
|
||||
# first-contact messaging needs a delivery target. Failures stay generic.
|
||||
return view
|
||||
|
||||
|
||||
|
||||
@@ -157,8 +157,45 @@ def transport_tier_is_sufficient(current_tier: str | None, required_tier: str |
|
||||
return TRANSPORT_TIER_ORDER[current] >= TRANSPORT_TIER_ORDER[required]
|
||||
|
||||
|
||||
def release_lane_required_tier(lane: str) -> str:
|
||||
return network_release_required_tier(lane)
|
||||
_DM_RUNTIME_ENFORCEMENT_ROUTES = {
|
||||
("POST", "/api/mesh/dm/send"),
|
||||
("POST", "/api/mesh/dm/poll"),
|
||||
("GET", "/api/mesh/dm/poll"),
|
||||
("GET", "/api/mesh/dm/count"),
|
||||
("POST", "/api/mesh/dm/count"),
|
||||
}
|
||||
|
||||
|
||||
def runtime_route_enforcement_tier(path: str, method: str, *, static_tier: str) -> str:
|
||||
"""Adjust static route tiers for Tor-only nodes that never reach private_strong."""
|
||||
normalized_path = str(path or "").strip()
|
||||
normalized_method = str(method or "").strip().upper()
|
||||
static = normalize_transport_tier(static_tier)
|
||||
if (normalized_method, normalized_path) not in _DM_RUNTIME_ENFORCEMENT_ROUTES:
|
||||
return static
|
||||
if static != "private_strong":
|
||||
return static
|
||||
return release_lane_required_tier("dm")
|
||||
|
||||
|
||||
def release_lane_required_tier(lane: str, *, wormhole_state: dict[str, Any] | None = None) -> str:
|
||||
normalized_lane = str(lane or "").strip().lower()
|
||||
required = network_release_required_tier(normalized_lane)
|
||||
if normalized_lane != "dm":
|
||||
return required
|
||||
state = wormhole_state
|
||||
if state is None:
|
||||
try:
|
||||
from services.wormhole_supervisor import get_wormhole_state
|
||||
|
||||
state = get_wormhole_state()
|
||||
except Exception:
|
||||
state = {}
|
||||
# Tor-only nodes never reach private_strong (needs Arti + RNS). Encrypted
|
||||
# relay over Arti still preserves ciphertext privacy for offline delivery.
|
||||
if not bool((state or {}).get("rns_enabled")):
|
||||
return "private_transitional"
|
||||
return required
|
||||
|
||||
|
||||
def private_delivery_status(status_code: str, *, reason_code: str = "", plain_reason: str = "") -> dict[str, str]:
|
||||
|
||||
@@ -386,6 +386,14 @@ def _dispatch_dm(
|
||||
sampled=sampled,
|
||||
)
|
||||
|
||||
replication_peer_urls: list[str] = []
|
||||
try:
|
||||
from services.mesh.mesh_dm_connect_delivery import relay_push_peer_urls_for_payload
|
||||
|
||||
replication_peer_urls = relay_push_peer_urls_for_payload(payload)
|
||||
except Exception:
|
||||
replication_peer_urls = []
|
||||
|
||||
apply_dm_relay_jitter()
|
||||
relay_result = dm_relay.deposit(
|
||||
sender_id=relay_sender_id,
|
||||
@@ -399,6 +407,7 @@ def _dispatch_dm(
|
||||
sender_token_hash=sender_token_hash,
|
||||
payload_format=payload_format,
|
||||
session_welcome=session_welcome,
|
||||
replication_peer_urls=replication_peer_urls,
|
||||
)
|
||||
if not relay_result.get("ok"):
|
||||
return _dispatch_result(
|
||||
@@ -600,8 +609,15 @@ def attempt_private_release(
|
||||
policy_reason_code=str(decision.reason_code or ""),
|
||||
)
|
||||
if normalized_lane == "dm":
|
||||
dm_payload = dict(payload or {})
|
||||
try:
|
||||
from services.mesh.mesh_dm_connect_delivery import enrich_connect_release_payload
|
||||
|
||||
dm_payload = enrich_connect_release_payload(dm_payload)
|
||||
except Exception:
|
||||
pass
|
||||
return _dispatch_dm(
|
||||
dict(payload or {}),
|
||||
dm_payload,
|
||||
secure_dm_enabled=secure_dm_enabled or _secure_dm_enabled,
|
||||
rns_private_dm_ready=rns_private_dm_ready or _rns_private_dm_ready,
|
||||
anonymous_dm_hidden_transport_enforced=(
|
||||
|
||||
@@ -36,6 +36,22 @@ def _require_fields(payload: dict[str, Any], fields: tuple[str, ...]) -> tuple[b
|
||||
return True, "ok"
|
||||
|
||||
|
||||
_SEALED_CIPHERTEXT_PREFIXES = ("x3dh1:", "dm1:", "mls1:", "sealed:")
|
||||
|
||||
|
||||
def _strip_sealed_ciphertext_prefix(value: str) -> str:
|
||||
lowered = value.lower()
|
||||
for prefix in _SEALED_CIPHERTEXT_PREFIXES:
|
||||
if lowered.startswith(prefix):
|
||||
return value[len(prefix) :]
|
||||
return value
|
||||
|
||||
|
||||
def _sealed_ciphertext_has_known_prefix(value: str) -> bool:
|
||||
lowered = str(value or "").strip().lower()
|
||||
return any(lowered.startswith(prefix) for prefix in _SEALED_CIPHERTEXT_PREFIXES)
|
||||
|
||||
|
||||
def _decode_base64ish(value: Any) -> bytes | None:
|
||||
raw = str(value or "").strip()
|
||||
if not raw or any(ch.isspace() for ch in raw):
|
||||
@@ -49,6 +65,13 @@ def _decode_base64ish(value: Any) -> bytes | None:
|
||||
return None
|
||||
|
||||
|
||||
def _decode_sealed_ciphertext_value(value: Any) -> bytes | None:
|
||||
raw = str(value or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
return _decode_base64ish(_strip_sealed_ciphertext_prefix(raw))
|
||||
|
||||
|
||||
def _byte_entropy(data: bytes) -> float:
|
||||
if not data:
|
||||
return 0.0
|
||||
@@ -66,12 +89,19 @@ def _validate_sealed_bytes_field(
|
||||
min_bytes: int = 8,
|
||||
entropy_floor: float = 2.5,
|
||||
) -> tuple[bool, str]:
|
||||
data = _decode_base64ish(payload.get(field, ""))
|
||||
raw = str(payload.get(field, "") or "").strip()
|
||||
prefixed = _sealed_ciphertext_has_known_prefix(raw)
|
||||
data = _decode_sealed_ciphertext_value(raw)
|
||||
if data is None:
|
||||
return False, f"{field} must be base64-encoded sealed bytes"
|
||||
if len(data) < min_bytes:
|
||||
return False, f"{field} is too short"
|
||||
|
||||
# X3DH / MLS envelopes are structured JSON or ratchet frames — skip
|
||||
# plaintext heuristics once a known wire prefix is present.
|
||||
if prefixed:
|
||||
return True, "ok"
|
||||
|
||||
# Short test vectors and compact envelopes can be low entropy; only apply
|
||||
# heuristics once there is enough material to distinguish a sealed blob
|
||||
# from accidental base64-encoded plaintext.
|
||||
|
||||
@@ -929,6 +929,85 @@ def list_wormhole_dm_contacts() -> dict[str, dict[str, Any]]:
|
||||
return _read_contacts()
|
||||
|
||||
|
||||
def get_wormhole_dm_contact(peer_id: str) -> dict[str, Any] | None:
|
||||
peer_key = str(peer_id or "").strip()
|
||||
if not peer_key:
|
||||
return None
|
||||
contacts = _read_contacts()
|
||||
if peer_key not in contacts:
|
||||
return None
|
||||
return dict(_normalize_contact(contacts[peer_key]))
|
||||
|
||||
|
||||
def sever_wormhole_dm_contact(peer_id: str, *, block: bool = False) -> dict[str, Any]:
|
||||
"""Close the shared DM lane; a fresh contact request + accept is required to reopen."""
|
||||
peer_key = str(peer_id or "").strip()
|
||||
if not peer_key:
|
||||
return {"ok": False, "detail": "peer_id required"}
|
||||
|
||||
contacts = _read_contacts()
|
||||
current = _normalize_contact(contacts.get(peer_key))
|
||||
now = int(time.time())
|
||||
current["sharedAlias"] = ""
|
||||
current["sharedAliasCounter"] = 0
|
||||
current["sharedAliasPublicKey"] = ""
|
||||
current["sharedAliasPublicKeyAlgo"] = "Ed25519"
|
||||
current["previousSharedAliases"] = []
|
||||
current["pendingSharedAlias"] = ""
|
||||
current["pendingSharedAliasCounter"] = 0
|
||||
current["pendingSharedAliasPublicKey"] = ""
|
||||
current["pendingSharedAliasPublicKeyAlgo"] = "Ed25519"
|
||||
current["pendingSharedAliasGraceMs"] = 0
|
||||
current["sharedAliasGraceUntil"] = 0
|
||||
current["sharedAliasRotatedAt"] = 0
|
||||
current["acceptedPreviousAlias"] = ""
|
||||
current["acceptedPreviousAliasCounter"] = 0
|
||||
current["acceptedPreviousAliasPublicKey"] = ""
|
||||
current["acceptedPreviousAliasPublicKeyAlgo"] = "Ed25519"
|
||||
current["acceptedPreviousGraceUntil"] = 0
|
||||
current["acceptedPreviousHardGraceUntil"] = 0
|
||||
current["acceptedPreviousAwaitingReply"] = False
|
||||
current["aliasBindingSeq"] = 0
|
||||
current["aliasBindingPendingReason"] = ""
|
||||
current["aliasBindingPreparedAt"] = 0
|
||||
current["aliasGateJoinAppliedSeq"] = 0
|
||||
if block:
|
||||
current["blocked"] = True
|
||||
current["updated_at"] = now
|
||||
contacts[peer_key] = _normalize_contact(current)
|
||||
_write_contacts(contacts)
|
||||
|
||||
relay_policy = {}
|
||||
try:
|
||||
from services.mesh.mesh_dm_connect_delivery import revoke_connect_relay_policy
|
||||
|
||||
relay_policy = revoke_connect_relay_policy(peer_key)
|
||||
except Exception:
|
||||
relay_policy = {"ok": False}
|
||||
|
||||
relay_block = {"ok": False}
|
||||
if block:
|
||||
try:
|
||||
from services.mesh.mesh_dm_relay import dm_relay
|
||||
from services.mesh.mesh_wormhole_persona import get_dm_identity
|
||||
|
||||
local_id = str(get_dm_identity().get("node_id", "") or "").strip()
|
||||
if local_id:
|
||||
dm_relay.block(local_id, peer_key)
|
||||
relay_block = {"ok": True, "local_id": local_id}
|
||||
except Exception as exc:
|
||||
relay_block = {"ok": False, "detail": str(exc) or type(exc).__name__}
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"peer_id": peer_key,
|
||||
"severed": True,
|
||||
"blocked": bool(block),
|
||||
"relay_policy": relay_policy,
|
||||
"relay_block": relay_block,
|
||||
}
|
||||
|
||||
|
||||
def _promote_invite_lookup_mode(contact: dict[str, Any], *, now: int | None = None) -> bool:
|
||||
current = dict(contact or {})
|
||||
lookup_handle = str(current.get("invitePinnedPrekeyLookupHandle", "") or "").strip()
|
||||
@@ -1070,11 +1149,14 @@ def pin_wormhole_dm_invite(
|
||||
identity_dh_pub_key = str(payload.get("identity_dh_pub_key", "") or "")
|
||||
dh_algo = str(payload.get("dh_algo", "X25519") or "X25519")
|
||||
prekey_lookup_handle = str(payload.get("prekey_lookup_handle", "") or "")
|
||||
lookup_peer_url = str(payload.get("lookup_peer_url", "") or "").strip().rstrip("/")
|
||||
if str(alias or "").strip():
|
||||
current["alias"] = str(alias or "").strip()
|
||||
current["dhPubKey"] = identity_dh_pub_key
|
||||
current["dhAlgo"] = dh_algo
|
||||
current["invitePinnedPrekeyLookupHandle"] = prekey_lookup_handle
|
||||
if lookup_peer_url:
|
||||
current["invitePinnedLookupPeerUrl"] = lookup_peer_url
|
||||
current["invitePinnedRootFingerprint"] = str(payload.get("root_fingerprint", "") or "").strip().lower()
|
||||
current["invitePinnedRootManifestFingerprint"] = str(
|
||||
payload.get("root_manifest_fingerprint", "") or ""
|
||||
@@ -1170,6 +1252,12 @@ def pin_wormhole_dm_invite(
|
||||
current["updated_at"] = now
|
||||
contacts[peer_key] = _normalize_contact(current)
|
||||
_write_contacts(contacts)
|
||||
try:
|
||||
from services.mesh.mesh_dm_connect_delivery import grant_connect_relay_policy
|
||||
|
||||
grant_connect_relay_policy(peer_key, reason="invite_import")
|
||||
except Exception:
|
||||
pass
|
||||
return contacts[peer_key]
|
||||
|
||||
|
||||
|
||||
@@ -549,6 +549,27 @@ def invite_identity_commitment_for_identity_material(
|
||||
return hashlib.sha256(_stable_json(material).encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _local_dm_lookup_peer_url() -> str:
|
||||
"""Return this node's fleet-reachable URL for invite-scoped prekey lookup."""
|
||||
try:
|
||||
from services.config import get_settings
|
||||
from services.mesh.mesh_crypto import normalize_peer_url
|
||||
|
||||
configured = normalize_peer_url(str(getattr(get_settings(), "MESH_PUBLIC_PEER_URL", "") or ""))
|
||||
if configured:
|
||||
return configured
|
||||
from services.tor_hidden_service import tor_service
|
||||
|
||||
onion = str(getattr(tor_service, "onion_address", "") or "").strip()
|
||||
if onion:
|
||||
if "://" not in onion:
|
||||
onion = f"http://{onion}:8000"
|
||||
return normalize_peer_url(onion)
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def _dm_invite_payload(
|
||||
data: dict[str, Any],
|
||||
*,
|
||||
@@ -930,6 +951,9 @@ def export_wormhole_dm_invite(*, label: str = "", expires_in_s: int = 0) -> dict
|
||||
# fetch our prekey bundle without using our stable agent_id.
|
||||
lookup_handle = secrets.token_hex(24)
|
||||
payload["prekey_lookup_handle"] = lookup_handle
|
||||
lookup_peer_url = _local_dm_lookup_peer_url()
|
||||
if lookup_peer_url:
|
||||
payload["lookup_peer_url"] = lookup_peer_url
|
||||
|
||||
# Persist the handle so it is included in future prekey registrations.
|
||||
existing_handles, _ = _normalize_prekey_lookup_handles(
|
||||
|
||||
@@ -79,6 +79,164 @@ def _warn_legacy_prekey_lookup(agent_id: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _fleet_peer_lookup_user_agent() -> str:
|
||||
custom = str(os.environ.get("SHADOWBROKER_MESH_PEER_USER_AGENT") or "").strip()
|
||||
if custom:
|
||||
return custom
|
||||
return "Mozilla/5.0 (compatible; ShadowbrokerMesh/1.0)"
|
||||
|
||||
|
||||
_INVITE_LOOKUP_MAX_ELAPSED_S = 120
|
||||
_INVITE_LOOKUP_MAX_BOOTSTRAP_PEERS = 3
|
||||
_INVITE_LOOKUP_MAX_PUSH_PEERS = 16
|
||||
_INVITE_LOOKUP_PARALLEL_WORKERS = 8
|
||||
|
||||
|
||||
def _invite_lookup_request_timeout(peer_url: str) -> tuple[int, int]:
|
||||
from services.mesh.mesh_router import peer_transport_kind
|
||||
|
||||
if peer_transport_kind(peer_url) == "onion":
|
||||
return (10, 35)
|
||||
return (5, 15)
|
||||
|
||||
|
||||
def _bootstrap_seed_peer_urls() -> set[str]:
|
||||
try:
|
||||
from services.config import get_settings
|
||||
from services.mesh.mesh_router import parse_configured_relay_peers
|
||||
|
||||
seeds: set[str] = set()
|
||||
raw = str(getattr(get_settings(), "MESH_BOOTSTRAP_SEED_PEERS", "") or "")
|
||||
for peer in parse_configured_relay_peers(raw):
|
||||
normalized = str(peer or "").strip().rstrip("/")
|
||||
if normalized:
|
||||
seeds.add(normalized)
|
||||
return seeds
|
||||
except Exception:
|
||||
return set()
|
||||
|
||||
|
||||
def _discovered_push_peer_urls(*, limit: int = _INVITE_LOOKUP_MAX_PUSH_PEERS) -> list[str]:
|
||||
try:
|
||||
from services.mesh.mesh_router import authenticated_push_peer_urls
|
||||
|
||||
seeds = _bootstrap_seed_peer_urls()
|
||||
peers: list[str] = []
|
||||
for peer in authenticated_push_peer_urls():
|
||||
normalized = str(peer or "").strip().rstrip("/")
|
||||
if not normalized or normalized in seeds:
|
||||
continue
|
||||
peers.append(normalized)
|
||||
if len(peers) >= max(1, int(limit or 1)):
|
||||
break
|
||||
return peers
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _prioritized_invite_lookup_peer_urls(*, preferred: list[str] | None = None) -> list[str]:
|
||||
preferred_urls = [
|
||||
str(peer or "").strip().rstrip("/")
|
||||
for peer in list(preferred or [])
|
||||
if str(peer or "").strip()
|
||||
]
|
||||
configured = _configured_public_lookup_peer_urls()
|
||||
seeds = _bootstrap_seed_peer_urls()
|
||||
active: list[str] = []
|
||||
bootstrap: list[str] = []
|
||||
push_discovery: list[str] = []
|
||||
seen = set(preferred_urls)
|
||||
for peer in configured:
|
||||
if peer in seen:
|
||||
continue
|
||||
seen.add(peer)
|
||||
if peer in seeds:
|
||||
bootstrap.append(peer)
|
||||
else:
|
||||
active.append(peer)
|
||||
for peer in _discovered_push_peer_urls():
|
||||
if peer in seen:
|
||||
continue
|
||||
seen.add(peer)
|
||||
push_discovery.append(peer)
|
||||
ordered = list(preferred_urls)
|
||||
ordered.extend(active)
|
||||
ordered.extend(push_discovery)
|
||||
ordered.extend(bootstrap[:_INVITE_LOOKUP_MAX_BOOTSTRAP_PEERS])
|
||||
return ordered
|
||||
|
||||
|
||||
def _preferred_invite_lookup_peer_urls(lookup_token: str) -> list[str]:
|
||||
token = str(lookup_token or "").strip()
|
||||
if not token:
|
||||
return []
|
||||
try:
|
||||
from services.mesh.mesh_wormhole_contacts import list_wormhole_dm_contacts
|
||||
except Exception:
|
||||
return []
|
||||
peers: list[str] = []
|
||||
for contact in list_wormhole_dm_contacts() or []:
|
||||
if not isinstance(contact, dict):
|
||||
continue
|
||||
if str(contact.get("invitePinnedPrekeyLookupHandle", "") or "").strip() != token:
|
||||
continue
|
||||
peer_url = str(contact.get("invitePinnedLookupPeerUrl", "") or "").strip().rstrip("/")
|
||||
if peer_url and peer_url not in peers:
|
||||
peers.append(peer_url)
|
||||
return peers
|
||||
|
||||
|
||||
def _peer_http_request(
|
||||
method: str,
|
||||
peer_url: str,
|
||||
*,
|
||||
body_bytes: bytes | None = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
timeout: int | tuple[int, int] = 45,
|
||||
):
|
||||
"""HTTP to a fleet peer, using Tor SOCKS when the URL is an onion address."""
|
||||
import requests
|
||||
|
||||
from services.mesh.mesh_crypto import normalize_peer_url
|
||||
from urllib.parse import urlparse
|
||||
|
||||
raw_peer_url = str(peer_url or "").strip()
|
||||
parsed = urlparse(raw_peer_url)
|
||||
if parsed.path and parsed.path not in {"", "/"}:
|
||||
# Full request URLs include invite lookup query params; do not
|
||||
# normalize them away when deriving the peer base URL.
|
||||
normalized = raw_peer_url
|
||||
else:
|
||||
normalized = normalize_peer_url(raw_peer_url)
|
||||
if not normalized:
|
||||
raise OSError("invalid peer url")
|
||||
if isinstance(timeout, tuple):
|
||||
connect_timeout, read_timeout = timeout
|
||||
resolved_timeout: int | tuple[int, int] = (
|
||||
max(1, int(connect_timeout or 5)),
|
||||
max(1, int(read_timeout or 15)),
|
||||
)
|
||||
else:
|
||||
resolved_timeout = max(1, int(timeout or 45))
|
||||
request_kwargs: dict[str, Any] = {
|
||||
"headers": dict(headers or {}),
|
||||
"timeout": resolved_timeout,
|
||||
}
|
||||
try:
|
||||
from main import _infonet_peer_requests_proxies
|
||||
|
||||
proxy_peer_url = normalize_peer_url(f"{parsed.scheme}://{parsed.netloc}")
|
||||
proxies = _infonet_peer_requests_proxies(proxy_peer_url)
|
||||
if proxies:
|
||||
request_kwargs["proxies"] = proxies
|
||||
except Exception:
|
||||
pass
|
||||
if method.upper() == "GET":
|
||||
return requests.get(normalized, **request_kwargs)
|
||||
request_kwargs["data"] = body_bytes or b""
|
||||
return requests.post(normalized, **request_kwargs)
|
||||
|
||||
|
||||
def _fetch_dm_prekey_bundle_from_peer_lookup(lookup_token: str) -> dict[str, Any]:
|
||||
"""Fetch an invite-scoped prekey bundle from configured authenticated peers.
|
||||
|
||||
@@ -95,12 +253,12 @@ def _fetch_dm_prekey_bundle_from_peer_lookup(lookup_token: str) -> dict[str, Any
|
||||
normalize_peer_url,
|
||||
resolve_peer_key_for_url,
|
||||
)
|
||||
from services.mesh.mesh_router import configured_relay_peer_urls
|
||||
from services.mesh.mesh_router import authenticated_push_peer_urls
|
||||
|
||||
settings = get_settings()
|
||||
# Issue #256: secret check moved per-peer below. We still bail out
|
||||
# cleanly when there are no peers configured at all.
|
||||
peers = configured_relay_peer_urls()
|
||||
peers = authenticated_push_peer_urls()
|
||||
if not peers:
|
||||
return {"ok": False, "detail": "peer prekey lookup unavailable"}
|
||||
timeout = max(1, _safe_int(getattr(settings, "MESH_RELAY_PUSH_TIMEOUT_S", 10) or 10, 10))
|
||||
@@ -132,17 +290,17 @@ def _fetch_dm_prekey_bundle_from_peer_lookup(lookup_token: str) -> dict[str, Any
|
||||
"X-Peer-Url": sender_peer_url,
|
||||
"X-Peer-HMAC": hmac.new(peer_key, body, hashlib.sha256).hexdigest(),
|
||||
}
|
||||
request = urllib.request.Request(
|
||||
f"{normalized_peer_url}/api/mesh/dm/prekey-peer-lookup",
|
||||
data=body,
|
||||
headers=headers,
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||
raw = response.read(256 * 1024)
|
||||
response = _peer_http_request(
|
||||
"POST",
|
||||
f"{normalized_peer_url}/api/mesh/dm/prekey-peer-lookup",
|
||||
body_bytes=body,
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
)
|
||||
raw = response.content[: 256 * 1024]
|
||||
payload = json.loads(raw.decode("utf-8"))
|
||||
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError) as exc:
|
||||
except (json.JSONDecodeError, OSError, Exception) as exc:
|
||||
last_detail = str(exc) or type(exc).__name__
|
||||
continue
|
||||
if isinstance(payload, dict) and payload.get("ok"):
|
||||
@@ -161,12 +319,18 @@ def _configured_public_lookup_peer_urls() -> list[str]:
|
||||
|
||||
settings = get_settings()
|
||||
candidates: list[str] = []
|
||||
# Operator-configured peers first, then recently active fleet nodes.
|
||||
# Invite handles are minted on a specific node; cold bootstrap seeds
|
||||
# rarely have them cached and should not be tried before contacts.
|
||||
for raw in (
|
||||
getattr(settings, "MESH_BOOTSTRAP_SEED_PEERS", ""),
|
||||
getattr(settings, "MESH_DEFAULT_SYNC_PEERS", ""),
|
||||
):
|
||||
candidates.extend(parse_configured_relay_peers(str(raw or "")))
|
||||
candidates.extend(active_sync_peer_urls())
|
||||
for raw in (
|
||||
getattr(settings, "MESH_BOOTSTRAP_SEED_PEERS", ""),
|
||||
):
|
||||
candidates.extend(parse_configured_relay_peers(str(raw or "")))
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
@@ -204,7 +368,50 @@ def _normalize_remote_lookup_bundle(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
return data
|
||||
|
||||
|
||||
def _fetch_dm_prekey_bundle_from_public_lookup(lookup_token: str) -> dict[str, Any]:
|
||||
def _try_public_prekey_lookup_peer(
|
||||
peer_url: str,
|
||||
encoded: str,
|
||||
*,
|
||||
timeout: int | tuple[int, int] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
normalized_peer_url = str(peer_url or "").strip().rstrip("/")
|
||||
if not normalized_peer_url:
|
||||
return {"ok": False, "detail": "invalid peer url"}
|
||||
resolved_timeout = timeout or _invite_lookup_request_timeout(normalized_peer_url)
|
||||
try:
|
||||
response = _peer_http_request(
|
||||
"GET",
|
||||
f"{normalized_peer_url}/api/mesh/dm/prekey-bundle?{encoded}",
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"User-Agent": _fleet_peer_lookup_user_agent(),
|
||||
},
|
||||
timeout=resolved_timeout,
|
||||
)
|
||||
raw = response.content[: 256 * 1024]
|
||||
payload = json.loads(raw.decode("utf-8"))
|
||||
except (json.JSONDecodeError, OSError, Exception) as exc:
|
||||
logger.debug("public prekey lookup failed for %s: %s", normalized_peer_url, type(exc).__name__)
|
||||
return {"ok": False, "detail": "peer prekey lookup unavailable"}
|
||||
if not isinstance(payload, dict):
|
||||
return {"ok": False, "detail": "invalid peer response"}
|
||||
if payload.get("pending") or str(payload.get("status", "") or "") == "preparing_private_lane":
|
||||
return {"ok": False, "detail": "peer prekey lookup still preparing"}
|
||||
if not payload.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"detail": str(payload.get("detail", "") or "Prekey bundle not found"),
|
||||
}
|
||||
if not isinstance(payload.get("bundle"), dict):
|
||||
return {"ok": False, "detail": "Prekey bundle not found"}
|
||||
return _normalize_remote_lookup_bundle(payload)
|
||||
|
||||
|
||||
def _fetch_dm_prekey_bundle_from_public_lookup(
|
||||
lookup_token: str,
|
||||
*,
|
||||
extra_preferred_peer_urls: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Fetch an invite-scoped prekey bundle from bootstrap/sync peers.
|
||||
|
||||
The token is high-entropy and invite-scoped. This path does not expose a
|
||||
@@ -212,61 +419,69 @@ def _fetch_dm_prekey_bundle_from_public_lookup(lookup_token: str) -> dict[str, A
|
||||
derive it from the signed identity public key and validate the bundle before
|
||||
accepting it.
|
||||
"""
|
||||
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
||||
|
||||
token = str(lookup_token or "").strip()
|
||||
if not token:
|
||||
return {"ok": False, "detail": "lookup token required"}
|
||||
peers = _configured_public_lookup_peer_urls()
|
||||
preferred = list(_preferred_invite_lookup_peer_urls(token))
|
||||
for peer in list(extra_preferred_peer_urls or []):
|
||||
normalized = str(peer or "").strip().rstrip("/")
|
||||
if normalized and normalized not in preferred:
|
||||
preferred.insert(0, normalized)
|
||||
peers = _prioritized_invite_lookup_peer_urls(preferred=preferred)
|
||||
if not peers:
|
||||
return {"ok": False, "detail": "peer prekey lookup unavailable"}
|
||||
try:
|
||||
from services.config import get_settings
|
||||
|
||||
timeout = max(1, _safe_int(getattr(get_settings(), "MESH_SYNC_TIMEOUT_S", 5) or 5, 5))
|
||||
except Exception:
|
||||
timeout = 5
|
||||
|
||||
encoded = urllib.parse.urlencode({"lookup_token": token})
|
||||
last_detail = ""
|
||||
for peer_url in peers:
|
||||
normalized_peer_url = str(peer_url or "").strip().rstrip("/")
|
||||
if not normalized_peer_url:
|
||||
continue
|
||||
# Generic UA: any peer-facing crypto request should not carry a
|
||||
# fork-specific identifier — that turns prekey lookups into a
|
||||
# software-fingerprinting beacon.
|
||||
from services.network_utils import default_user_agent
|
||||
request = urllib.request.Request(
|
||||
f"{normalized_peer_url}/api/mesh/dm/prekey-bundle?{encoded}",
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"User-Agent": default_user_agent(),
|
||||
},
|
||||
method="GET",
|
||||
hinted_only = bool(list(extra_preferred_peer_urls or []))
|
||||
hint_timeout = (5, 20)
|
||||
for peer_url in preferred:
|
||||
hinted = _try_public_prekey_lookup_peer(
|
||||
peer_url,
|
||||
encoded,
|
||||
timeout=hint_timeout if hinted_only else None,
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||
raw = response.read(256 * 1024)
|
||||
payload = json.loads(raw.decode("utf-8"))
|
||||
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError) as exc:
|
||||
logger.debug("public prekey lookup failed for %s: %s", normalized_peer_url, type(exc).__name__)
|
||||
last_detail = "peer prekey lookup unavailable"
|
||||
continue
|
||||
if not isinstance(payload, dict):
|
||||
last_detail = "invalid peer response"
|
||||
continue
|
||||
if payload.get("pending") or str(payload.get("status", "") or "") == "preparing_private_lane":
|
||||
last_detail = "peer prekey lookup still preparing"
|
||||
continue
|
||||
if not payload.get("ok"):
|
||||
last_detail = str(payload.get("detail", "") or last_detail or "Prekey bundle not found")
|
||||
continue
|
||||
if not isinstance(payload.get("bundle"), dict):
|
||||
last_detail = "Prekey bundle not found"
|
||||
continue
|
||||
normalized = _normalize_remote_lookup_bundle(payload)
|
||||
if normalized.get("ok"):
|
||||
return normalized
|
||||
last_detail = str(normalized.get("detail", "") or last_detail)
|
||||
if hinted.get("ok"):
|
||||
return hinted
|
||||
if isinstance(hinted, dict):
|
||||
last_detail = str(hinted.get("detail", "") or last_detail)
|
||||
remaining_peers = [peer for peer in peers if peer not in set(preferred)]
|
||||
if not remaining_peers:
|
||||
return {"ok": False, "detail": last_detail or "Prekey bundle not found"}
|
||||
if hinted_only:
|
||||
return {"ok": False, "detail": last_detail or "Prekey bundle not found"}
|
||||
deadline = time.time() + _INVITE_LOOKUP_MAX_ELAPSED_S
|
||||
workers = min(_INVITE_LOOKUP_PARALLEL_WORKERS, max(1, len(remaining_peers)))
|
||||
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
futures = {
|
||||
executor.submit(_try_public_prekey_lookup_peer, peer_url, encoded): peer_url
|
||||
for peer_url in remaining_peers
|
||||
}
|
||||
while futures and time.time() < deadline:
|
||||
done, _ = wait(
|
||||
futures,
|
||||
timeout=max(0.1, deadline - time.time()),
|
||||
return_when=FIRST_COMPLETED,
|
||||
)
|
||||
if not done:
|
||||
break
|
||||
for future in done:
|
||||
futures.pop(future, None)
|
||||
try:
|
||||
result = future.result()
|
||||
except Exception as exc:
|
||||
last_detail = str(exc) or type(exc).__name__
|
||||
continue
|
||||
if isinstance(result, dict) and result.get("ok"):
|
||||
for pending in futures:
|
||||
pending.cancel()
|
||||
return result
|
||||
if isinstance(result, dict):
|
||||
last_detail = str(result.get("detail", "") or last_detail)
|
||||
for pending in futures:
|
||||
pending.cancel()
|
||||
return {"ok": False, "detail": last_detail or "Prekey bundle not found"}
|
||||
|
||||
|
||||
@@ -1019,6 +1234,7 @@ def fetch_dm_prekey_bundle(
|
||||
lookup_token: str = "",
|
||||
*,
|
||||
allow_peer_lookup: bool = True,
|
||||
lookup_peer_urls: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
from services.mesh.mesh_dm_relay import dm_relay
|
||||
|
||||
@@ -1043,12 +1259,18 @@ def fetch_dm_prekey_bundle(
|
||||
resolved_id = found_id
|
||||
lookup_mode = "invite_lookup_handle"
|
||||
elif allow_peer_lookup:
|
||||
peer_found = _fetch_dm_prekey_bundle_from_peer_lookup(resolved_lookup)
|
||||
if peer_found.get("ok"):
|
||||
return peer_found
|
||||
public_found = _fetch_dm_prekey_bundle_from_public_lookup(resolved_lookup)
|
||||
preferred_peer_urls = list(lookup_peer_urls or [])
|
||||
public_found = _fetch_dm_prekey_bundle_from_public_lookup(
|
||||
resolved_lookup,
|
||||
extra_preferred_peer_urls=preferred_peer_urls,
|
||||
)
|
||||
if public_found.get("ok"):
|
||||
return public_found
|
||||
peer_found: dict[str, Any] = {"ok": False, "detail": ""}
|
||||
if not preferred_peer_urls:
|
||||
peer_found = _fetch_dm_prekey_bundle_from_peer_lookup(resolved_lookup)
|
||||
if peer_found.get("ok"):
|
||||
return peer_found
|
||||
if str(public_found.get("detail", "") or "").strip():
|
||||
return {"ok": False, "detail": str(public_found.get("detail", "") or "Prekey bundle not found")}
|
||||
return {"ok": False, "detail": str(peer_found.get("detail", "") or "Prekey bundle not found")}
|
||||
@@ -1134,12 +1356,22 @@ def _classify_root_attestation_failure(peer_id: str) -> tuple[str, bool]:
|
||||
return "", False
|
||||
|
||||
|
||||
def bootstrap_encrypt_for_peer(peer_id: str, plaintext: str) -> dict[str, Any]:
|
||||
fetched_bundle = fetch_dm_prekey_bundle(str(peer_id or "").strip())
|
||||
def bootstrap_encrypt_for_peer(
|
||||
peer_id: str,
|
||||
plaintext: str,
|
||||
*,
|
||||
lookup_token: str = "",
|
||||
) -> dict[str, Any]:
|
||||
token = str(lookup_token or "").strip()
|
||||
peer = str(peer_id or "").strip()
|
||||
fetched_bundle = fetch_dm_prekey_bundle(
|
||||
agent_id=peer if not token else "",
|
||||
lookup_token=token,
|
||||
)
|
||||
if not fetched_bundle.get("ok"):
|
||||
detail = str(fetched_bundle.get("detail", "") or "")
|
||||
if "root attestation" in detail.lower():
|
||||
trust_level, trust_changed = _classify_root_attestation_failure(str(peer_id or "").strip())
|
||||
trust_level, trust_changed = _classify_root_attestation_failure(peer or token)
|
||||
if trust_level:
|
||||
return {
|
||||
"ok": False,
|
||||
@@ -1152,32 +1384,68 @@ def bootstrap_encrypt_for_peer(peer_id: str, plaintext: str) -> dict[str, Any]:
|
||||
|
||||
from services.mesh.mesh_dm_relay import dm_relay
|
||||
|
||||
resolved_peer_id = str(fetched_bundle.get("agent_id", peer_id) or peer_id).strip()
|
||||
resolved_peer_id = str(fetched_bundle.get("agent_id", peer) or peer).strip()
|
||||
stored = dm_relay.get_prekey_bundle(resolved_peer_id)
|
||||
if not stored:
|
||||
return {"ok": False, "detail": "Peer prekey bundle not found"}
|
||||
remote_bundle = dict(fetched_bundle.get("bundle") or {})
|
||||
if not remote_bundle and fetched_bundle.get("identity_dh_pub_key"):
|
||||
remote_bundle = fetched_bundle
|
||||
if remote_bundle:
|
||||
stored = {
|
||||
"bundle": remote_bundle,
|
||||
"signature": str(fetched_bundle.get("signature", "") or ""),
|
||||
"public_key": str(fetched_bundle.get("public_key", "") or ""),
|
||||
"public_key_algo": str(fetched_bundle.get("public_key_algo", "") or ""),
|
||||
"sequence": _safe_int(fetched_bundle.get("sequence", 0) or 0),
|
||||
}
|
||||
else:
|
||||
return {"ok": False, "detail": "Peer prekey bundle not found"}
|
||||
validated_record = {**dict(stored), "agent_id": resolved_peer_id}
|
||||
ok, reason = _validate_bundle_record(validated_record)
|
||||
if not ok:
|
||||
return {"ok": False, "detail": reason}
|
||||
trust_state = observe_remote_prekey_bundle(resolved_peer_id, validated_record)
|
||||
trust_level = str(trust_state.get("trust_level", "") or "")
|
||||
from services.mesh.mesh_wormhole_contacts import verified_first_contact_requirement
|
||||
consent_handshake = False
|
||||
try:
|
||||
from services.mesh.mesh_wormhole_dead_drop import parse_contact_consent
|
||||
|
||||
verified_first_contact = verified_first_contact_requirement(
|
||||
resolved_peer_id,
|
||||
trust_level=trust_level,
|
||||
)
|
||||
if not verified_first_contact.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"peer_id": resolved_peer_id,
|
||||
"detail": str(verified_first_contact.get("detail", "") or "verified first contact required"),
|
||||
"trust_changed": trust_level in ("mismatch", "continuity_broken"),
|
||||
"trust_level": str(verified_first_contact.get("trust_level", "") or trust_level or "unpinned"),
|
||||
consent = parse_contact_consent(str(plaintext or "")) or {}
|
||||
consent_handshake = str(consent.get("kind", "") or "") in {
|
||||
"contact_offer",
|
||||
"contact_accept",
|
||||
"contact_deny",
|
||||
}
|
||||
except Exception:
|
||||
consent_handshake = False
|
||||
if not consent_handshake:
|
||||
from services.mesh.mesh_wormhole_contacts import verified_first_contact_requirement
|
||||
|
||||
verified_first_contact = verified_first_contact_requirement(
|
||||
resolved_peer_id,
|
||||
trust_level=trust_level,
|
||||
)
|
||||
if not verified_first_contact.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"peer_id": resolved_peer_id,
|
||||
"detail": str(
|
||||
verified_first_contact.get("detail", "") or "verified first contact required"
|
||||
),
|
||||
"trust_changed": trust_level in ("mismatch", "continuity_broken"),
|
||||
"trust_level": str(
|
||||
verified_first_contact.get("trust_level", "") or trust_level or "unpinned"
|
||||
),
|
||||
}
|
||||
peer_bundle_stored = dm_relay.consume_one_time_prekey(resolved_peer_id)
|
||||
if not peer_bundle_stored:
|
||||
remote_bundle = dict(stored.get("bundle") or {})
|
||||
otks = list(remote_bundle.get("one_time_prekeys") or [])
|
||||
peer_bundle_stored = {
|
||||
"bundle": remote_bundle,
|
||||
"claimed_one_time_prekey": dict(otks[0] or {}) if otks else {},
|
||||
}
|
||||
if not peer_bundle_stored.get("bundle"):
|
||||
return {"ok": False, "detail": "Peer prekey bundle not found"}
|
||||
peer_bundle = dict(peer_bundle_stored.get("bundle") or {})
|
||||
peer_static = str(peer_bundle.get("identity_dh_pub_key", "") or "")
|
||||
|
||||
@@ -90,6 +90,11 @@ READ_COMMANDS = frozenset({
|
||||
# Agent routing helpers
|
||||
"route_query",
|
||||
"run_playbook",
|
||||
# Private Infonet reads (operator-delegated)
|
||||
"infonet_status",
|
||||
"list_gates",
|
||||
"read_gate_messages",
|
||||
"poll_dms",
|
||||
})
|
||||
|
||||
WRITE_COMMANDS = frozenset({
|
||||
@@ -121,6 +126,12 @@ WRITE_COMMANDS = frozenset({
|
||||
"clear_analysis_zones",
|
||||
# Active recon (subnet device discovery)
|
||||
"osint_sweep",
|
||||
# Private Infonet writes (operator wormhole identity)
|
||||
"ensure_infonet_ready",
|
||||
"join_infonet_swarm",
|
||||
"post_gate_message",
|
||||
"cast_vote",
|
||||
"send_dm",
|
||||
})
|
||||
|
||||
|
||||
@@ -1598,6 +1609,85 @@ def _dispatch_command(cmd: str, args: dict[str, Any]) -> dict[str, Any]:
|
||||
count = clear_zones(source="openclaw")
|
||||
return {"ok": True, "data": {"removed_count": count}}
|
||||
|
||||
# -- Infonet / gate / DM (operator-delegated, full tier for writes) ------
|
||||
|
||||
if cmd == "infonet_status":
|
||||
from services.openclaw_infonet import get_infonet_status
|
||||
|
||||
return get_infonet_status()
|
||||
|
||||
if cmd == "ensure_infonet_ready":
|
||||
from services.openclaw_infonet import ensure_infonet_ready
|
||||
|
||||
return ensure_infonet_ready(join_swarm=bool(args.get("join_swarm", True)))
|
||||
|
||||
if cmd == "join_infonet_swarm":
|
||||
from services.openclaw_infonet import join_infonet_swarm
|
||||
|
||||
return join_infonet_swarm()
|
||||
|
||||
if cmd == "list_gates":
|
||||
from services.openclaw_infonet import list_gates
|
||||
|
||||
return list_gates()
|
||||
|
||||
if cmd == "read_gate_messages":
|
||||
from services.openclaw_infonet import read_gate_messages
|
||||
|
||||
gate_id = str(args.get("gate_id", "") or args.get("gate", "")).strip()
|
||||
return read_gate_messages(
|
||||
gate_id,
|
||||
limit=int(args.get("limit", 20) or 20),
|
||||
decrypt=bool(args.get("decrypt", False)),
|
||||
)
|
||||
|
||||
if cmd == "post_gate_message":
|
||||
from services.openclaw_infonet import post_gate_message
|
||||
|
||||
gate_id = str(args.get("gate_id", "") or args.get("gate", "")).strip()
|
||||
plaintext = str(args.get("plaintext", "") or args.get("message", "")).strip()
|
||||
return post_gate_message(
|
||||
gate_id,
|
||||
plaintext,
|
||||
reply_to=str(args.get("reply_to", "") or ""),
|
||||
)
|
||||
|
||||
if cmd == "cast_vote":
|
||||
from services.openclaw_infonet import cast_vote
|
||||
|
||||
target_id = str(args.get("target_id", "") or args.get("target", "")).strip()
|
||||
vote_raw = args.get("vote", args.get("direction"))
|
||||
try:
|
||||
vote_val = int(vote_raw)
|
||||
except (TypeError, ValueError):
|
||||
return {"ok": False, "detail": "vote must be 1 or -1"}
|
||||
return cast_vote(
|
||||
target_id,
|
||||
vote_val,
|
||||
gate=str(args.get("gate", "") or args.get("gate_id", "")).strip(),
|
||||
)
|
||||
|
||||
if cmd == "send_dm":
|
||||
from services.openclaw_infonet import send_dm
|
||||
|
||||
peer_id = str(
|
||||
args.get("peer_id", "")
|
||||
or args.get("recipient_id", "")
|
||||
or args.get("recipient", "")
|
||||
).strip()
|
||||
plaintext = str(args.get("plaintext", "") or args.get("message", "")).strip()
|
||||
return send_dm(
|
||||
peer_id,
|
||||
plaintext,
|
||||
delivery_class=str(args.get("delivery_class", "shared") or "shared"),
|
||||
recipient_token=str(args.get("recipient_token", "") or ""),
|
||||
)
|
||||
|
||||
if cmd == "poll_dms":
|
||||
from services.openclaw_infonet import poll_dms
|
||||
|
||||
return poll_dms(limit=int(args.get("limit", 20) or 20))
|
||||
|
||||
return {"ok": False, "detail": f"unhandled command: {cmd}"}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,760 @@
|
||||
"""OpenClaw agent delegation for private Infonet / gate / DM actions.
|
||||
|
||||
Agents authenticate with OpenClaw HMAC on the command channel. Write
|
||||
commands require ``OPENCLAW_ACCESS_TIER=full``. Actions use the operator's
|
||||
local wormhole persona and node runtime — the agent posts on behalf of the
|
||||
user who configured the skill, not as a separate fleet identity.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from starlette.requests import Request
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _run_async(coro):
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
return asyncio.run(coro)
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
def _local_agent_request(path: str, *, method: str = "POST") -> Request:
|
||||
scope = {
|
||||
"type": "http",
|
||||
"method": method.upper(),
|
||||
"path": path,
|
||||
"headers": [],
|
||||
"client": ("127.0.0.1", 52421),
|
||||
}
|
||||
request = Request(scope)
|
||||
request.state._private_lane_current_tier = "private_strong"
|
||||
request.state._transport_tier = "private_strong"
|
||||
return request
|
||||
|
||||
|
||||
def ensure_infonet_ready(*, join_swarm: bool = True) -> dict[str, Any]:
|
||||
"""Warm Tor, enable the participant node, and optionally join the swarm."""
|
||||
from routers.ai_intel import _write_env_value
|
||||
from services.config import get_settings
|
||||
from services.mesh.mesh_swarm_runtime import (
|
||||
announce_local_peer_to_seeds,
|
||||
refresh_swarm_manifest_from_seeds,
|
||||
)
|
||||
from services.node_settings import read_node_settings, write_node_settings
|
||||
from services.tor_hidden_service import tor_service
|
||||
from services.wormhole_supervisor import _check_arti_ready
|
||||
|
||||
steps: dict[str, Any] = {}
|
||||
|
||||
tor_result = tor_service.start(target_port=8000)
|
||||
steps["tor"] = tor_result
|
||||
if tor_result.get("ok"):
|
||||
try:
|
||||
_write_env_value("MESH_ARTI_ENABLED", "true")
|
||||
get_settings.cache_clear()
|
||||
except Exception as exc:
|
||||
logger.debug("failed to persist MESH_ARTI_ENABLED: %s", exc)
|
||||
|
||||
if not _check_arti_ready():
|
||||
return {
|
||||
"ok": False,
|
||||
"detail": "Tor/Arti transport is not ready yet",
|
||||
"steps": steps,
|
||||
}
|
||||
|
||||
if not bool(read_node_settings().get("enabled")):
|
||||
write_node_settings(enabled=True)
|
||||
steps["node_enabled"] = True
|
||||
try:
|
||||
import main as main_mod
|
||||
|
||||
main_mod._refresh_node_peer_store()
|
||||
main_mod._start_infonet_node_runtime("openclaw_agent")
|
||||
except Exception as exc:
|
||||
logger.warning("node runtime start after agent enable failed: %s", exc)
|
||||
else:
|
||||
steps["node_enabled"] = True
|
||||
|
||||
if join_swarm:
|
||||
steps["announce"] = announce_local_peer_to_seeds(force=True)
|
||||
steps["manifest_pull"] = refresh_swarm_manifest_from_seeds(force=True)
|
||||
ok = bool(steps["announce"].get("ok")) or bool(steps["manifest_pull"].get("ok"))
|
||||
else:
|
||||
ok = True
|
||||
|
||||
return {
|
||||
"ok": ok,
|
||||
"detail": "Infonet participant runtime ready" if ok else "swarm join incomplete",
|
||||
"steps": steps,
|
||||
"onion_address": str(tor_result.get("onion_address") or ""),
|
||||
}
|
||||
|
||||
|
||||
def join_infonet_swarm() -> dict[str, Any]:
|
||||
from services.mesh.mesh_swarm_runtime import (
|
||||
announce_local_peer_to_seeds,
|
||||
refresh_swarm_manifest_from_seeds,
|
||||
)
|
||||
|
||||
announce = announce_local_peer_to_seeds(force=True)
|
||||
manifest = refresh_swarm_manifest_from_seeds(force=True)
|
||||
return {
|
||||
"ok": bool(announce.get("ok")) or bool(manifest.get("ok")),
|
||||
"announce": announce,
|
||||
"manifest_pull": manifest,
|
||||
}
|
||||
|
||||
|
||||
def get_infonet_status() -> dict[str, Any]:
|
||||
from services.mesh.mesh_hashchain import infonet
|
||||
from services.wormhole_supervisor import get_wormhole_state
|
||||
|
||||
info = infonet.get_info()
|
||||
valid, reason = infonet.validate_chain(verify_signatures=False)
|
||||
try:
|
||||
wormhole = get_wormhole_state()
|
||||
except Exception:
|
||||
wormhole = {"configured": False, "ready": False, "arti_ready": False, "rns_ready": False}
|
||||
try:
|
||||
import main as main_mod
|
||||
|
||||
runtime = main_mod._node_runtime_snapshot()
|
||||
private_tier = main_mod._current_private_lane_tier(wormhole)
|
||||
except Exception:
|
||||
runtime = {}
|
||||
private_tier = "public_degraded"
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"chain": info,
|
||||
"valid": valid,
|
||||
"validation": reason,
|
||||
"private_lane_tier": private_tier,
|
||||
"wormhole": wormhole,
|
||||
"runtime": runtime,
|
||||
}
|
||||
|
||||
|
||||
def list_gates() -> dict[str, Any]:
|
||||
from services.mesh.mesh_reputation import gate_manager
|
||||
|
||||
return {"ok": True, "gates": gate_manager.list_gates()}
|
||||
|
||||
|
||||
def read_gate_messages(
|
||||
gate_id: str,
|
||||
*,
|
||||
limit: int = 20,
|
||||
decrypt: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
from services.mesh.mesh_hashchain import gate_store
|
||||
|
||||
gate_key = str(gate_id or "").strip().lower()
|
||||
if not gate_key:
|
||||
return {"ok": False, "detail": "gate_id required"}
|
||||
|
||||
messages, cursor = gate_store.get_messages_with_cursor(gate_key, limit=max(1, min(int(limit), 100)))
|
||||
out = []
|
||||
if decrypt:
|
||||
from services.mesh.mesh_gate_repair import decrypt_gate_message_with_repair
|
||||
|
||||
for msg in messages:
|
||||
item = dict(msg)
|
||||
try:
|
||||
decrypted = decrypt_gate_message_with_repair(
|
||||
gate_id=gate_key,
|
||||
epoch=int(item.get("epoch") or 0),
|
||||
ciphertext=str(item.get("ciphertext") or ""),
|
||||
nonce=str(item.get("nonce") or item.get("iv") or ""),
|
||||
sender_ref=str(item.get("sender_ref") or ""),
|
||||
gate_envelope=str(item.get("gate_envelope") or ""),
|
||||
envelope_hash=str(item.get("envelope_hash") or ""),
|
||||
event_id=str(item.get("event_id") or ""),
|
||||
)
|
||||
if decrypted.get("ok"):
|
||||
item["plaintext"] = decrypted.get("plaintext", "")
|
||||
except Exception as exc:
|
||||
item["decrypt_error"] = str(exc)
|
||||
out.append(item)
|
||||
else:
|
||||
out = [dict(m) for m in messages]
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"gate": gate_key,
|
||||
"count": len(out),
|
||||
"cursor": cursor,
|
||||
"messages": out,
|
||||
}
|
||||
|
||||
|
||||
def post_gate_message(
|
||||
gate_id: str,
|
||||
plaintext: str,
|
||||
*,
|
||||
reply_to: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Compose, sign, and post an MLS gate message using the operator persona."""
|
||||
from services.mesh.mesh_gate_repair import (
|
||||
compose_gate_message_with_repair,
|
||||
sign_gate_message_with_repair,
|
||||
)
|
||||
from services.mesh.mesh_wormhole_persona import bootstrap_wormhole_persona_state, create_gate_persona
|
||||
|
||||
gate_key = str(gate_id or "").strip().lower()
|
||||
if not gate_key:
|
||||
return {"ok": False, "detail": "gate_id required"}
|
||||
if not str(plaintext or "").strip():
|
||||
return {"ok": False, "detail": "plaintext required"}
|
||||
|
||||
bootstrap_wormhole_persona_state(force=False)
|
||||
try:
|
||||
create_gate_persona(gate_key, label="openclaw-agent")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
composed = compose_gate_message_with_repair(
|
||||
gate_id=gate_key,
|
||||
plaintext=str(plaintext),
|
||||
reply_to=str(reply_to or ""),
|
||||
)
|
||||
if not composed.get("ok"):
|
||||
return composed
|
||||
|
||||
signed = sign_gate_message_with_repair(
|
||||
gate_id=gate_key,
|
||||
epoch=int(composed.get("epoch") or 0),
|
||||
ciphertext=str(composed.get("ciphertext") or ""),
|
||||
nonce=str(composed.get("nonce") or ""),
|
||||
payload_format=str(composed.get("format") or "mls1"),
|
||||
reply_to=str(reply_to or ""),
|
||||
envelope_hash=str(composed.get("envelope_hash") or ""),
|
||||
transport_lock="private_strong",
|
||||
)
|
||||
if not signed.get("ok"):
|
||||
return signed
|
||||
|
||||
body = {
|
||||
"sender_id": str(signed.get("sender_id") or composed.get("sender_id") or ""),
|
||||
"public_key": str(signed.get("public_key") or composed.get("public_key") or ""),
|
||||
"public_key_algo": str(signed.get("public_key_algo") or composed.get("public_key_algo") or ""),
|
||||
"signature": str(signed.get("signature") or ""),
|
||||
"sequence": int(signed.get("sequence") or composed.get("sequence") or 0),
|
||||
"protocol_version": str(signed.get("protocol_version") or composed.get("protocol_version") or ""),
|
||||
"epoch": int(signed.get("epoch") or composed.get("epoch") or 0),
|
||||
"ciphertext": str(signed.get("ciphertext") or composed.get("ciphertext") or ""),
|
||||
"nonce": str(signed.get("nonce") or composed.get("nonce") or ""),
|
||||
"sender_ref": str(signed.get("sender_ref") or composed.get("sender_ref") or ""),
|
||||
"format": str(signed.get("format") or composed.get("format") or "mls1"),
|
||||
"gate_envelope": str(signed.get("gate_envelope") or composed.get("gate_envelope") or ""),
|
||||
"envelope_hash": str(signed.get("envelope_hash") or composed.get("envelope_hash") or ""),
|
||||
"transport_lock": "private_strong",
|
||||
"reply_to": str(signed.get("reply_to") or reply_to or ""),
|
||||
}
|
||||
|
||||
import main as main_mod
|
||||
|
||||
path = f"/api/mesh/gate/{gate_key}/message"
|
||||
request = _local_agent_request(path)
|
||||
return main_mod._submit_gate_message_envelope(request, gate_key, body)
|
||||
|
||||
|
||||
def cast_vote(
|
||||
target_id: str,
|
||||
vote: int,
|
||||
*,
|
||||
gate: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Cast a signed reputation vote using the operator gate/transport persona."""
|
||||
from services.mesh.mesh_hashchain import infonet
|
||||
from services.mesh.mesh_protocol import PROTOCOL_VERSION, normalize_payload
|
||||
from services.mesh.mesh_reputation import gate_manager, reputation_ledger
|
||||
from services.mesh.mesh_wormhole_persona import (
|
||||
bootstrap_wormhole_persona_state,
|
||||
sign_gate_wormhole_event,
|
||||
sign_public_wormhole_event,
|
||||
)
|
||||
|
||||
voter_gate = str(gate or "").strip().lower()
|
||||
target = str(target_id or "").strip()
|
||||
vote_val = int(vote)
|
||||
if not target:
|
||||
return {"ok": False, "detail": "target_id required"}
|
||||
if vote_val not in (1, -1):
|
||||
return {"ok": False, "detail": "vote must be 1 or -1"}
|
||||
|
||||
bootstrap_wormhole_persona_state(force=False)
|
||||
vote_payload = {"target_id": target, "vote": vote_val, "gate": voter_gate}
|
||||
normalized = normalize_payload("vote", vote_payload)
|
||||
ok_payload, reason = True, "ok"
|
||||
from services.mesh.mesh_schema import validate_event_payload
|
||||
|
||||
ok_payload, reason = validate_event_payload("vote", normalized)
|
||||
if not ok_payload:
|
||||
return {"ok": False, "detail": reason}
|
||||
|
||||
if voter_gate:
|
||||
signed = sign_gate_wormhole_event(
|
||||
gate_id=voter_gate,
|
||||
event_type="vote",
|
||||
payload=normalized,
|
||||
)
|
||||
else:
|
||||
signed = sign_public_wormhole_event(event_type="vote", payload=normalized)
|
||||
|
||||
if not signed.get("ok", True):
|
||||
return signed
|
||||
|
||||
voter_id = str(signed.get("node_id") or "")
|
||||
public_key = str(signed.get("public_key") or "")
|
||||
public_key_algo = str(signed.get("public_key_algo") or "")
|
||||
signature = str(signed.get("signature") or "")
|
||||
sequence = int(signed.get("sequence") or 0)
|
||||
|
||||
if voter_gate:
|
||||
can_enter, enter_reason = gate_manager.can_enter(voter_id, voter_gate)
|
||||
if not can_enter:
|
||||
return {"ok": False, "detail": f"Gate vote denied: {enter_reason}"}
|
||||
|
||||
reputation_ledger.register_node(voter_id, public_key, public_key_algo)
|
||||
stable_voter_id = voter_id
|
||||
try:
|
||||
import main as main_mod
|
||||
|
||||
root_nid = main_mod._cached_root_node_id()
|
||||
if root_nid:
|
||||
stable_voter_id = root_nid
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ok, cast_reason, weight = reputation_ledger.cast_vote(
|
||||
stable_voter_id,
|
||||
target,
|
||||
vote_val,
|
||||
voter_gate,
|
||||
)
|
||||
if ok:
|
||||
try:
|
||||
infonet.append(
|
||||
event_type="vote",
|
||||
node_id=voter_id,
|
||||
payload=normalized,
|
||||
signature=signature,
|
||||
sequence=sequence,
|
||||
public_key=public_key,
|
||||
public_key_algo=public_key_algo,
|
||||
protocol_version=str(signed.get("protocol_version") or PROTOCOL_VERSION),
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("vote recorded in ledger but infonet append failed: %s", exc)
|
||||
|
||||
return {"ok": ok, "detail": cast_reason, "weight": round(float(weight or 0), 2)}
|
||||
|
||||
|
||||
def _http_post_json(
|
||||
url: str,
|
||||
body: dict[str, Any],
|
||||
*,
|
||||
extra_headers: dict[str, str] | None = None,
|
||||
timeout: int = 120,
|
||||
) -> dict[str, Any]:
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
payload_bytes = json.dumps(body, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if extra_headers:
|
||||
headers.update(extra_headers)
|
||||
req = urllib.request.Request(url, data=payload_bytes, headers=headers, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
raw = resp.read().decode("utf-8")
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", errors="replace")
|
||||
try:
|
||||
parsed = json.loads(detail)
|
||||
if isinstance(parsed, dict):
|
||||
return parsed
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": False, "detail": detail or f"http {exc.code}"}
|
||||
if not raw:
|
||||
return {}
|
||||
parsed = json.loads(raw)
|
||||
return parsed if isinstance(parsed, dict) else {"ok": False, "detail": "invalid json response"}
|
||||
|
||||
|
||||
def _issue_sender_token_for_http_send(
|
||||
api_base: str,
|
||||
*,
|
||||
recipient: str,
|
||||
delivery: str,
|
||||
recipient_token: str,
|
||||
) -> dict[str, Any]:
|
||||
extra_headers: dict[str, str] = {}
|
||||
admin_key = str(os.environ.get("ADMIN_KEY") or "").strip()
|
||||
if admin_key:
|
||||
extra_headers["X-Admin-Key"] = admin_key
|
||||
return _http_post_json(
|
||||
f"{api_base}/api/wormhole/dm/sender-token",
|
||||
{
|
||||
"recipient_id": recipient,
|
||||
"delivery_class": delivery,
|
||||
"recipient_token": recipient_token,
|
||||
},
|
||||
extra_headers=extra_headers or None,
|
||||
)
|
||||
|
||||
|
||||
def _submit_signed_dm_send(
|
||||
*,
|
||||
recipient: str,
|
||||
delivery_class: str,
|
||||
recipient_token: str,
|
||||
ciphertext: str,
|
||||
payload_format: str,
|
||||
session_welcome: str = "",
|
||||
connect_intent: str = "",
|
||||
lookup_peer_url: str = "",
|
||||
) -> dict[str, Any]:
|
||||
import main as main_mod
|
||||
from services.mesh.mesh_protocol import (
|
||||
PROTOCOL_VERSION,
|
||||
SIGNED_CONTEXT_FIELD,
|
||||
build_signed_context,
|
||||
)
|
||||
from services.mesh.mesh_schema import validate_event_payload
|
||||
from services.mesh.mesh_wormhole_persona import get_dm_identity, sign_dm_wormhole_event
|
||||
from services.mesh.mesh_wormhole_sender_token import issue_wormhole_dm_sender_token
|
||||
|
||||
delivery = str(delivery_class or "shared").strip().lower()
|
||||
identity = get_dm_identity()
|
||||
sender_id = str(identity.get("node_id") or "")
|
||||
msg_id = secrets.token_hex(16)
|
||||
timestamp = int(time.time())
|
||||
sequence = int(identity.get("sequence", 0) or 0) + 1
|
||||
|
||||
dm_payload: dict[str, Any] = {
|
||||
"recipient_id": recipient,
|
||||
"delivery_class": delivery,
|
||||
"recipient_token": str(recipient_token or ""),
|
||||
"ciphertext": str(ciphertext or ""),
|
||||
"msg_id": msg_id,
|
||||
"timestamp": timestamp,
|
||||
"format": str(payload_format or "mls1"),
|
||||
"transport_lock": "private_strong",
|
||||
}
|
||||
if session_welcome:
|
||||
dm_payload["session_welcome"] = str(session_welcome)
|
||||
|
||||
ok_payload, reason = validate_event_payload("dm_message", dm_payload)
|
||||
if not ok_payload:
|
||||
return {"ok": False, "detail": reason}
|
||||
|
||||
dm_payload[SIGNED_CONTEXT_FIELD] = build_signed_context(
|
||||
event_type="dm_message",
|
||||
kind="dm_send",
|
||||
endpoint="/api/mesh/dm/send",
|
||||
lane_floor="private_strong",
|
||||
sequence_domain="dm_send",
|
||||
node_id=sender_id,
|
||||
sequence=sequence,
|
||||
payload=dm_payload,
|
||||
recipient_id=recipient,
|
||||
)
|
||||
signed = sign_dm_wormhole_event(
|
||||
event_type="dm_message",
|
||||
payload=dm_payload,
|
||||
sequence=sequence,
|
||||
)
|
||||
if not signed.get("ok", True):
|
||||
return signed
|
||||
|
||||
body = {
|
||||
"sender_id": sender_id,
|
||||
"sender_token": "",
|
||||
"recipient_id": recipient,
|
||||
"delivery_class": delivery,
|
||||
"recipient_token": str(recipient_token or ""),
|
||||
"ciphertext": str(ciphertext or ""),
|
||||
"format": str(payload_format or "mls1"),
|
||||
"transport_lock": "private_strong",
|
||||
"session_welcome": str(session_welcome or ""),
|
||||
"msg_id": msg_id,
|
||||
"timestamp": timestamp,
|
||||
"public_key": str(signed.get("public_key") or ""),
|
||||
"public_key_algo": str(signed.get("public_key_algo") or ""),
|
||||
"signature": str(signed.get("signature") or ""),
|
||||
"sequence": int(signed.get("sequence") or 0),
|
||||
"protocol_version": str(signed.get("protocol_version") or PROTOCOL_VERSION),
|
||||
"signed_context": dict(dm_payload.get(SIGNED_CONTEXT_FIELD) or {}),
|
||||
}
|
||||
normalized_intent = str(connect_intent or "").strip().lower()
|
||||
normalized_lookup_peer = str(lookup_peer_url or "").strip().rstrip("/")
|
||||
if normalized_intent:
|
||||
body["connect_intent"] = normalized_intent
|
||||
if normalized_lookup_peer:
|
||||
body["lookup_peer_url"] = normalized_lookup_peer
|
||||
|
||||
api_base = str(os.environ.get("SB_API_BASE", "http://127.0.0.1:8000") or "http://127.0.0.1:8000").rstrip("/")
|
||||
result: dict[str, Any] = {"ok": False, "detail": "dm send failed"}
|
||||
try:
|
||||
import urllib.error
|
||||
|
||||
if delivery in ("request", "shared"):
|
||||
issued = _issue_sender_token_for_http_send(
|
||||
api_base,
|
||||
recipient=recipient,
|
||||
delivery=delivery,
|
||||
recipient_token=str(recipient_token or ""),
|
||||
)
|
||||
if not issued.get("ok"):
|
||||
return issued
|
||||
body["sender_token"] = str(issued.get("sender_token") or "")
|
||||
|
||||
result = _http_post_json(f"{api_base}/api/mesh/dm/send", body)
|
||||
except (urllib.error.URLError, TimeoutError):
|
||||
if delivery in ("request", "shared"):
|
||||
issued = issue_wormhole_dm_sender_token(
|
||||
recipient_id=recipient,
|
||||
delivery_class=delivery,
|
||||
recipient_token=str(recipient_token or ""),
|
||||
)
|
||||
if not issued.get("ok"):
|
||||
return issued
|
||||
body["sender_token"] = str(issued.get("sender_token") or "")
|
||||
|
||||
async def _send():
|
||||
import json as _json
|
||||
|
||||
raw = _json.dumps(body).encode("utf-8")
|
||||
|
||||
async def receive():
|
||||
return {"type": "http.request", "body": raw, "more_body": False}
|
||||
|
||||
req = Request(
|
||||
{
|
||||
"type": "http",
|
||||
"method": "POST",
|
||||
"path": "/api/mesh/dm/send",
|
||||
"headers": [(b"content-type", b"application/json")],
|
||||
"client": ("127.0.0.1", 52421),
|
||||
},
|
||||
receive,
|
||||
)
|
||||
req.state._private_lane_current_tier = "private_strong"
|
||||
req.state._transport_tier = "private_strong"
|
||||
return await main_mod.dm_send(req)
|
||||
|
||||
result = _run_async(_send())
|
||||
except Exception as exc:
|
||||
result = {"ok": False, "detail": str(exc) or type(exc).__name__}
|
||||
if isinstance(result, dict):
|
||||
result.setdefault("msg_id", msg_id)
|
||||
result.setdefault("sender_id", sender_id)
|
||||
result.setdefault("recipient_id", recipient)
|
||||
return result
|
||||
|
||||
|
||||
def send_contact_request(
|
||||
*,
|
||||
lookup_token: str = "",
|
||||
peer_id: str = "",
|
||||
note: str = "",
|
||||
lookup_peer_url: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Send a first-contact request using a short address or peer id."""
|
||||
from services.mesh.mesh_wormhole_dead_drop import build_contact_offer
|
||||
from services.mesh.mesh_wormhole_persona import get_dm_identity
|
||||
from services.mesh.mesh_wormhole_prekey import bootstrap_encrypt_for_peer, fetch_dm_prekey_bundle
|
||||
|
||||
token = str(lookup_token or "").strip()
|
||||
peer = str(peer_id or "").strip()
|
||||
if not token and not peer:
|
||||
return {"ok": False, "detail": "lookup_token or peer_id required"}
|
||||
|
||||
preferred_peer = str(lookup_peer_url or "").strip().rstrip("/")
|
||||
bundle = fetch_dm_prekey_bundle(
|
||||
agent_id=peer if not token else "",
|
||||
lookup_token=token,
|
||||
lookup_peer_urls=[preferred_peer] if preferred_peer else None,
|
||||
)
|
||||
if not bundle.get("ok"):
|
||||
return bundle
|
||||
recipient = str(bundle.get("agent_id") or peer).strip()
|
||||
if not recipient:
|
||||
return {"ok": False, "detail": "recipient unresolved"}
|
||||
|
||||
identity = get_dm_identity()
|
||||
offer = build_contact_offer(
|
||||
dh_pub_key=str(identity.get("dh_pub_key") or ""),
|
||||
dh_algo=str(identity.get("dh_algo") or "X25519"),
|
||||
geo_hint=str(note or ""),
|
||||
)
|
||||
encrypted = bootstrap_encrypt_for_peer(recipient, offer, lookup_token=token)
|
||||
if not encrypted.get("ok"):
|
||||
return encrypted
|
||||
|
||||
return _submit_signed_dm_send(
|
||||
recipient=recipient,
|
||||
delivery_class="request",
|
||||
recipient_token="",
|
||||
ciphertext=str(encrypted.get("result") or ""),
|
||||
payload_format="mls1",
|
||||
connect_intent="contact_request",
|
||||
lookup_peer_url=preferred_peer,
|
||||
)
|
||||
|
||||
|
||||
def send_contact_accept(
|
||||
*,
|
||||
peer_id: str,
|
||||
peer_dh_pub: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Accept a pending contact request and open the shared DM lane."""
|
||||
from services.mesh.mesh_wormhole_dead_drop import build_contact_accept, issue_pairwise_dm_alias
|
||||
from services.mesh.mesh_wormhole_prekey import bootstrap_encrypt_for_peer, fetch_dm_prekey_bundle
|
||||
|
||||
peer = str(peer_id or "").strip()
|
||||
if not peer:
|
||||
return {"ok": False, "detail": "peer_id required"}
|
||||
|
||||
dh_pub = str(peer_dh_pub or "").strip()
|
||||
if not dh_pub:
|
||||
bundle = fetch_dm_prekey_bundle(agent_id=peer)
|
||||
if not bundle.get("ok"):
|
||||
return bundle
|
||||
dh_pub = str(bundle.get("dh_pub_key") or "").strip()
|
||||
if not dh_pub:
|
||||
return {"ok": False, "detail": "peer dh_pub_key unavailable"}
|
||||
|
||||
alias = issue_pairwise_dm_alias(peer_id=peer, peer_dh_pub=dh_pub)
|
||||
if not alias.get("ok"):
|
||||
return alias
|
||||
shared_alias = str(alias.get("shared_alias") or "").strip()
|
||||
if not shared_alias:
|
||||
return {"ok": False, "detail": "shared_alias unavailable"}
|
||||
|
||||
accept_plain = build_contact_accept(shared_alias=shared_alias)
|
||||
encrypted = bootstrap_encrypt_for_peer(peer, accept_plain)
|
||||
if not encrypted.get("ok"):
|
||||
return encrypted
|
||||
|
||||
sent = _submit_signed_dm_send(
|
||||
recipient=peer,
|
||||
delivery_class="request",
|
||||
recipient_token="",
|
||||
ciphertext=str(encrypted.get("result") or ""),
|
||||
payload_format="mls1",
|
||||
connect_intent="contact_accept",
|
||||
)
|
||||
if isinstance(sent, dict):
|
||||
sent.setdefault("shared_alias", shared_alias)
|
||||
return sent
|
||||
|
||||
|
||||
def send_dm(
|
||||
peer_id: str,
|
||||
plaintext: str,
|
||||
*,
|
||||
delivery_class: str = "shared",
|
||||
recipient_token: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Compose and send an encrypted DM on behalf of the operator."""
|
||||
import main as main_mod
|
||||
|
||||
recipient = str(peer_id or "").strip()
|
||||
if not recipient:
|
||||
return {"ok": False, "detail": "peer_id required"}
|
||||
if not str(plaintext or "").strip():
|
||||
return {"ok": False, "detail": "plaintext required"}
|
||||
|
||||
delivery = str(delivery_class or "shared").strip().lower()
|
||||
if delivery not in ("shared", "request"):
|
||||
return {"ok": False, "detail": "delivery_class must be shared or request"}
|
||||
|
||||
composed = main_mod.compose_wormhole_dm(
|
||||
peer_id=recipient,
|
||||
peer_dh_pub="",
|
||||
plaintext=str(plaintext),
|
||||
)
|
||||
if not composed.get("ok"):
|
||||
return composed
|
||||
|
||||
return _submit_signed_dm_send(
|
||||
recipient=recipient,
|
||||
delivery_class=delivery,
|
||||
recipient_token=str(recipient_token or ""),
|
||||
ciphertext=str(composed.get("ciphertext") or ""),
|
||||
payload_format=str(composed.get("format") or "mls1"),
|
||||
session_welcome=str(composed.get("session_welcome") or ""),
|
||||
)
|
||||
|
||||
|
||||
def poll_dms(*, limit: int = 20) -> dict[str, Any]:
|
||||
"""Poll encrypted DMs for the operator DM identity."""
|
||||
import json
|
||||
|
||||
import main as main_mod
|
||||
from services.mesh.mesh_protocol import PROTOCOL_VERSION
|
||||
from services.mesh.mesh_wormhole_persona import get_dm_identity, sign_dm_wormhole_event
|
||||
|
||||
identity = get_dm_identity()
|
||||
agent_id = str(identity.get("node_id") or "")
|
||||
if not agent_id:
|
||||
return {"ok": False, "detail": "dm identity is not configured"}
|
||||
|
||||
poll_payload = {"mailbox_claims": [], "agent_id": agent_id}
|
||||
signed = sign_dm_wormhole_event(event_type="dm_poll", payload=poll_payload)
|
||||
if not signed.get("ok", True):
|
||||
return signed
|
||||
|
||||
body = {
|
||||
"agent_id": agent_id,
|
||||
"mailbox_claims": [],
|
||||
"timestamp": int(time.time()),
|
||||
"nonce": secrets.token_hex(8),
|
||||
"public_key": str(signed.get("public_key") or ""),
|
||||
"public_key_algo": str(signed.get("public_key_algo") or ""),
|
||||
"signature": str(signed.get("signature") or ""),
|
||||
"sequence": int(signed.get("sequence") or 0),
|
||||
"protocol_version": str(signed.get("protocol_version") or PROTOCOL_VERSION),
|
||||
}
|
||||
|
||||
raw = json.dumps(body).encode("utf-8")
|
||||
|
||||
async def _poll():
|
||||
async def receive():
|
||||
return {"type": "http.request", "body": raw, "more_body": False}
|
||||
|
||||
req = Request(
|
||||
{
|
||||
"type": "http",
|
||||
"method": "POST",
|
||||
"path": "/api/mesh/dm/poll",
|
||||
"headers": [(b"content-type", b"application/json")],
|
||||
"client": ("127.0.0.1", 52421),
|
||||
},
|
||||
receive,
|
||||
)
|
||||
return await main_mod.dm_poll_secure(req)
|
||||
|
||||
result = _run_async(_poll())
|
||||
if isinstance(result, dict):
|
||||
messages = list(result.get("messages") or [])
|
||||
if limit and len(messages) > int(limit):
|
||||
result = dict(result)
|
||||
result["messages"] = messages[: int(limit)]
|
||||
result["count"] = len(result["messages"])
|
||||
return result if isinstance(result, dict) else {"ok": False, "detail": "dm poll failed"}
|
||||
@@ -36,6 +36,15 @@ LATENCY_TIER_MS: dict[str, int] = {
|
||||
"entity_expand": 40,
|
||||
"osint_lookup": 200,
|
||||
"run_playbook": 120,
|
||||
"infonet_status": 20,
|
||||
"list_gates": 15,
|
||||
"read_gate_messages": 40,
|
||||
"poll_dms": 80,
|
||||
"ensure_infonet_ready": 120000,
|
||||
"join_infonet_swarm": 90000,
|
||||
"post_gate_message": 15000,
|
||||
"cast_vote": 5000,
|
||||
"send_dm": 20000,
|
||||
"search_telemetry": 8000,
|
||||
"get_telemetry": 3500,
|
||||
"get_slow_telemetry": 1500,
|
||||
@@ -172,6 +181,18 @@ def routing_manifest() -> dict[str, Any]:
|
||||
"intent": "hot snapshot",
|
||||
"use": "run_playbook(name=hot_snapshot)",
|
||||
},
|
||||
{
|
||||
"intent": "post to infonet gate / join swarm",
|
||||
"use": "ensure_infonet_ready then post_gate_message (full tier)",
|
||||
},
|
||||
{
|
||||
"intent": "read encrypted gate traffic",
|
||||
"use": "read_gate_messages(gate_id=infonet, decrypt=true)",
|
||||
},
|
||||
{
|
||||
"intent": "dm another node",
|
||||
"use": "send_dm(peer_id=..., plaintext=...) (full tier)",
|
||||
},
|
||||
],
|
||||
"playbooks": {
|
||||
name: {"description": spec.get("description", "")}
|
||||
@@ -184,6 +205,16 @@ def routing_manifest() -> dict[str, Any]:
|
||||
"add_watch",
|
||||
"inject_data",
|
||||
"place_analysis_zone",
|
||||
"ensure_infonet_ready",
|
||||
"post_gate_message",
|
||||
"cast_vote",
|
||||
"send_dm",
|
||||
],
|
||||
"infonet_reads": [
|
||||
"infonet_status",
|
||||
"list_gates",
|
||||
"read_gate_messages",
|
||||
"poll_dms",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -109,18 +109,22 @@ def _check_arti_ready() -> bool:
|
||||
is_tor = bool(payload.get("IsTor")) or bool(payload.get("is_tor"))
|
||||
if not (response.ok and is_tor):
|
||||
logger.warning(
|
||||
"Arti Tor proof failed (status=%s is_tor=%s) — proxy is not trusted as Tor",
|
||||
"Arti Tor proof failed (status=%s is_tor=%s) — SOCKS is up, using Arti anyway",
|
||||
getattr(response, "status_code", "unknown"),
|
||||
payload.get("IsTor", payload.get("is_tor")),
|
||||
)
|
||||
_ARTI_PROOF_CACHE.update({"port": socks_port, "ok": False, "ts": now})
|
||||
return False
|
||||
_ARTI_PROOF_CACHE.update({"port": socks_port, "ok": True, "ts": now})
|
||||
return True
|
||||
_ARTI_PROOF_CACHE.update({"port": socks_port, "ok": True, "ts": now})
|
||||
return True
|
||||
except Exception as exc:
|
||||
logger.warning("Arti Tor proof request failed on port %s: %s", socks_port, exc)
|
||||
_ARTI_PROOF_CACHE.update({"port": socks_port, "ok": False, "ts": now})
|
||||
return False
|
||||
logger.warning(
|
||||
"Arti Tor proof request failed on port %s: %s — SOCKS is up, using Arti anyway",
|
||||
socks_port,
|
||||
exc,
|
||||
)
|
||||
_ARTI_PROOF_CACHE.update({"port": socks_port, "ok": True, "ts": now})
|
||||
return True
|
||||
|
||||
|
||||
def get_transport_tier() -> str:
|
||||
|
||||
Reference in New Issue
Block a user