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:
BigBodyCobain
2026-06-12 02:15:56 -06:00
parent d48a0cdace
commit 89d6bb8fb9
52 changed files with 4211 additions and 339 deletions
+4
View File
@@ -237,6 +237,10 @@ AIS_API_KEY= # https://aisstream.io/ — free tier WebSocket key
# MESH_BOOTSTRAP_SIGNER_PRIVATE_KEY= # seed only
# MESH_BOOTSTRAP_SIGNER_ID=shadowbroker-seed
# MESH_PEER_REGISTRY_ENABLED=true # seed only (auto-enabled when private key is set)
# Headless relay compose sets MESH_INFONET_RELAY_AUTO_WORMHOLE=true; seed nodes with
# MESH_BOOTSTRAP_SIGNER_PRIVATE_KEY also auto-enable Tor wormhole on startup.
# MESH_INFONET_RELAY_AUTO_WORMHOLE=false
# MESH_INFONET_RELAY_AUTO_WORMHOLE_DISABLED=false
# MESH_SWARM_MANIFEST_TTL_S=14400
# MESH_SWARM_MANIFEST_PULL_INTERVAL_S=300
# MESH_PEER_REGISTRY_STALE_S=604800
+124 -13
View File
@@ -2696,8 +2696,10 @@ async def lifespan(app: FastAPI):
if not _MESH_ONLY:
def _startup_wormhole_runtime():
try:
from services.mesh.mesh_infonet_relay_bootstrap import ensure_infonet_relay_wormhole_ready
from services.wormhole_supervisor import get_wormhole_state, sync_wormhole_with_settings
ensure_infonet_relay_wormhole_ready(reason="startup_relay")
sync_wormhole_with_settings()
_resume_private_delivery_background_work(
current_tier=_current_private_lane_tier(get_wormhole_state()),
@@ -3472,7 +3474,10 @@ def _request_private_surface_warmup(*, path: str, method: str, current_tier: str
def _is_invite_scoped_prekey_bundle_lookup(request: Request, path: str) -> bool:
if request.method.upper() != "GET" or str(path or "").strip() != "/api/mesh/dm/prekey-bundle":
if request.method.upper() != "GET":
return False
normalized_path = str(path or "").strip()
if normalized_path not in {"/api/mesh/dm/prekey-bundle", "/api/mesh/dm/pubkey"}:
return False
try:
lookup_token = str(request.query_params.get("lookup_token", "") or "").strip()
@@ -3573,6 +3578,14 @@ async def enforce_high_privacy_mesh(request: Request, call_next):
except Exception:
logger.debug("Private surface warm-up request failed", exc_info=True)
required_tier = _minimum_transport_tier(path, request.method)
if required_tier:
from services.mesh.mesh_privacy_policy import runtime_route_enforcement_tier
required_tier = runtime_route_enforcement_tier(
path,
request.method,
static_tier=required_tier,
)
if required_tier:
if not _transport_tier_is_sufficient(current_tier, required_tier):
if request.method.upper() == "POST" and path == "/api/mesh/dm/send":
@@ -6865,12 +6878,22 @@ def _queue_dm_release(*, current_tier: str, payload: dict[str, Any]) -> dict[str
required_tier=release_lane_required_tier("dm"),
)
_wake_private_release_worker()
outbox_id = str(item.get("id", "") or "")
auto_release: dict[str, Any] = {"ok": True, "skipped": True}
if outbox_id:
try:
from services.mesh.mesh_dm_connect_delivery import auto_release_connect_dm_outbox
auto_release = auto_release_connect_dm_outbox(outbox_id=outbox_id, payload=payload)
except Exception as exc:
auto_release = {"ok": False, "detail": str(exc) or type(exc).__name__}
return {
"ok": True,
"msg_id": str(payload.get("msg_id", "") or ""),
"outbox_id": str(item.get("id", "") or ""),
"outbox_id": outbox_id,
"queued": True,
"detail": str((item.get("status") or {}).get("label", "") or "Queued for private delivery"),
"auto_release": auto_release,
"delivery": {
"state": canonical_release_state(str(item.get("release_state", "") or "queued")),
"internal_state": str(item.get("release_state", "") or "queued"),
@@ -7043,7 +7066,8 @@ async def _dm_send_from_signed_request(request: Request):
return {"ok": False, "detail": "DM timestamp is too far from current time"}
if delivery_class not in ("request", "shared"):
return {"ok": False, "detail": "delivery_class must be request or shared"}
if delivery_class == "request":
# Contact requests are the first-contact handshake — do not require prior verification.
if delivery_class == "shared":
try:
from services.mesh.mesh_wormhole_contacts import verified_first_contact_requirement
@@ -7127,6 +7151,8 @@ async def _dm_send_from_signed_request(request: Request):
relay_salt_hex = _os.urandom(16).hex()
connect_intent = str(body.get("connect_intent", "") or "").strip().lower()
lookup_peer_url = str(body.get("lookup_peer_url", "") or "").strip().rstrip("/")
release_payload = {
"sender_id": sender_id,
"sender_token_hash": sender_token_hash,
@@ -7141,6 +7167,16 @@ async def _dm_send_from_signed_request(request: Request):
"sender_seal": sender_seal,
"relay_salt": relay_salt_hex,
}
if connect_intent:
release_payload["connect_intent"] = connect_intent
if lookup_peer_url:
release_payload["lookup_peer_url"] = lookup_peer_url
try:
from services.mesh.mesh_dm_connect_delivery import enrich_connect_release_payload
release_payload = enrich_connect_release_payload(release_payload)
except Exception:
pass
hashchain_spool: dict[str, Any] = {"ok": False, "detail": "not attempted"}
try:
from services.mesh.mesh_hashchain import infonet
@@ -7427,7 +7463,12 @@ async def dm_register_key(request: Request):
@app.get("/api/mesh/dm/pubkey")
@limiter.limit("30/minute")
async def dm_get_pubkey(request: Request, agent_id: str = "", lookup_token: str = ""):
async def dm_get_pubkey(
request: Request,
agent_id: str = "",
lookup_token: str = "",
lookup_peer_url: str = "",
):
"""Fetch an agent's DH public key for key exchange."""
exposure = metadata_exposure_for_request(
request,
@@ -7447,11 +7488,49 @@ async def dm_get_pubkey(request: Request, agent_id: str = "", lookup_token: str
if resolved_lookup:
key_bundle, resolved_id = dm_relay.get_dh_key_by_lookup(resolved_lookup)
if key_bundle is None:
return dm_lookup_response_view(
{"ok": False, "detail": "Agent not found or has no DH key", "lookup_mode": "invite_lookup_handle"},
exposure=exposure,
lookup_token_present=True,
# Invite handles are minted on the owner's node. When a remote peer
# pastes a short address, resolve it across the private fleet before
# failing — same path as prekey-bundle import.
from services.mesh.mesh_wormhole_prekey import fetch_dm_prekey_bundle
preferred_lookup_peer = str(lookup_peer_url or "").strip().rstrip("/")
remote_bundle = fetch_dm_prekey_bundle(
agent_id="",
lookup_token=resolved_lookup,
lookup_peer_urls=[preferred_lookup_peer] if preferred_lookup_peer else None,
)
if remote_bundle.get("ok"):
bundle = dict(remote_bundle.get("bundle") or remote_bundle)
dh_pub = str(
bundle.get("identity_dh_pub_key", "")
or remote_bundle.get("identity_dh_pub_key", "")
or ""
).strip()
if dh_pub:
resolved_id = str(remote_bundle.get("agent_id", "") or resolved_id or "").strip()
key_bundle = {
"dh_pub_key": dh_pub,
"dh_algo": str(remote_bundle.get("dh_algo", "X25519") or "X25519"),
"timestamp": int(remote_bundle.get("timestamp", 0) or 0),
"public_key": str(remote_bundle.get("public_key", "") or ""),
"public_key_algo": str(remote_bundle.get("public_key_algo", "") or ""),
"signature": str(remote_bundle.get("signature", "") or ""),
"sequence": int(remote_bundle.get("sequence", 0) or 0),
"prekey_transparency_head": str(
remote_bundle.get("prekey_transparency_head", "") or ""
),
"prekey_transparency_size": int(
remote_bundle.get("prekey_transparency_size", 0) or 0
),
"witness_count": int(remote_bundle.get("witness_count", 0) or 0),
"witness_latest_at": int(remote_bundle.get("witness_latest_at", 0) or 0),
}
if key_bundle is None:
return dm_lookup_response_view(
{"ok": False, "detail": "Agent not found or has no DH key", "lookup_mode": "invite_lookup_handle"},
exposure=exposure,
lookup_token_present=True,
)
lookup_mode = "invite_lookup_handle"
if key_bundle is None and resolved_id:
blocked = legacy_agent_id_lookup_blocked()
@@ -7487,7 +7566,12 @@ async def dm_get_pubkey(request: Request, agent_id: str = "", lookup_token: str
@app.get("/api/mesh/dm/prekey-bundle")
@limiter.limit("30/minute")
async def dm_get_prekey_bundle(request: Request, agent_id: str = "", lookup_token: str = ""):
async def dm_get_prekey_bundle(
request: Request,
agent_id: str = "",
lookup_token: str = "",
lookup_peer_url: str = "",
):
exposure = metadata_exposure_for_request(
request,
authenticated=_scoped_view_authenticated(request, "mesh"),
@@ -7499,7 +7583,12 @@ async def dm_get_prekey_bundle(request: Request, agent_id: str = "", lookup_toke
lookup_token_present=bool(lookup_token),
)
resolved_id, resolved_lookup = _preferred_dm_lookup_target(agent_id, lookup_token)
result = fetch_dm_prekey_bundle(agent_id=resolved_id, lookup_token=resolved_lookup)
preferred_lookup_peer = str(lookup_peer_url or "").strip().rstrip("/")
result = fetch_dm_prekey_bundle(
agent_id=resolved_id,
lookup_token=resolved_lookup,
lookup_peer_urls=[preferred_lookup_peer] if preferred_lookup_peer else None,
)
return dm_lookup_response_view(
result,
exposure=exposure,
@@ -9349,7 +9438,8 @@ class WormholeDmResetRequest(BaseModel):
class WormholeDmBootstrapEncryptRequest(BaseModel):
peer_id: str
peer_id: str = ""
lookup_token: str = ""
plaintext: str
@@ -11311,9 +11401,12 @@ async def api_wormhole_dm_bootstrap_encrypt(request: Request, body: WormholeDmBo
result = bootstrap_encrypt_for_peer(
peer_id=str(body.peer_id or ""),
plaintext=str(body.plaintext or ""),
lookup_token=str(body.lookup_token or ""),
)
if isinstance(result, dict) and "trust_level" not in result:
result["trust_level"] = _get_contact_trust_level(str(body.peer_id or ""))
result["trust_level"] = _get_contact_trust_level(
str(result.get("peer_id", "") or body.peer_id or "")
)
return result
@@ -11329,7 +11422,7 @@ async def api_wormhole_dm_bootstrap_decrypt(request: Request, body: WormholeDmBo
return result
@app.post("/api/wormhole/dm/sender-token", dependencies=[Depends(require_admin)])
@app.post("/api/wormhole/dm/sender-token", dependencies=[Depends(require_local_operator)])
@limiter.limit("60/minute")
async def api_wormhole_dm_sender_token(request: Request, body: WormholeDmSenderTokenRequest):
if _safe_int(body.count or 1, 1) > 1:
@@ -11548,6 +11641,24 @@ async def api_wormhole_dm_contact_delete(request: Request, peer_id: str):
return {"ok": True, "peer_id": peer_id, "deleted": deleted}
@app.post("/api/wormhole/dm/contact/{peer_id}/sever", dependencies=[Depends(require_admin)])
@limiter.limit("60/minute")
async def api_wormhole_dm_contact_sever(request: Request, peer_id: str):
from services.mesh.mesh_wormhole_contacts import sever_wormhole_dm_contact
try:
body = await request.json()
except Exception:
body = {}
if not isinstance(body, dict):
body = {}
block = bool(body.get("block", False))
try:
return sever_wormhole_dm_contact(peer_id, block=block)
except ValueError as exc:
return {"ok": False, "detail": str(exc)}
_WORMHOLE_PUBLIC_FIELDS = {"installed", "configured", "running", "ready"}
+3
View File
@@ -2719,6 +2719,7 @@ def _connect_info_metadata(settings) -> dict:
"get_telemetry", "get_pins", "satellite_images",
"news_near", "ai_summary", "ai_report",
"timemachine_list", "timemachine_view",
"infonet_status", "list_gates", "read_gate_messages", "poll_dms",
],
},
"full": {
@@ -2729,6 +2730,8 @@ def _connect_info_metadata(settings) -> dict:
"satellite_images", "news_near", "data_injection",
"ai_summary", "ai_report", "timemachine_snapshot",
"timemachine_list", "timemachine_view", "timemachine_diff",
"ensure_infonet_ready", "join_infonet_swarm",
"post_gate_message", "cast_vote", "send_dm",
],
},
},
+19 -1
View File
@@ -1085,7 +1085,7 @@ async def api_wormhole_dm_bootstrap_decrypt(request: Request, body: WormholeDmBo
)
@router.post("/api/wormhole/dm/sender-token", dependencies=[Depends(require_admin)])
@router.post("/api/wormhole/dm/sender-token", dependencies=[Depends(require_local_operator)])
@limiter.limit("60/minute")
async def api_wormhole_dm_sender_token(request: Request, body: WormholeDmSenderTokenRequest):
if _safe_int(body.count or 1, 1) > 1:
@@ -1287,6 +1287,24 @@ async def api_wormhole_dm_contact_delete(request: Request, peer_id: str):
return {"ok": True, "peer_id": peer_id, "deleted": deleted}
@router.post("/api/wormhole/dm/contact/{peer_id}/sever", dependencies=[Depends(require_admin)])
@limiter.limit("60/minute")
async def api_wormhole_dm_contact_sever(request: Request, peer_id: str):
from services.mesh.mesh_wormhole_contacts import sever_wormhole_dm_contact
try:
body = await request.json()
except Exception:
body = {}
if not isinstance(body, dict):
body = {}
block = bool(body.get("block", False))
try:
return sever_wormhole_dm_contact(peer_id, block=block)
except ValueError as exc:
return {"ok": False, "detail": str(exc)}
_WORMHOLE_PUBLIC_FIELDS = {"installed", "configured", "running", "ready"}
+4
View File
@@ -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),
}
+12 -1
View File
@@ -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
+39 -2
View File
@@ -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=(
+31 -1
View File
@@ -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(
+348 -80
View File
@@ -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
View File
@@ -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}"}
+760
View File
@@ -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"}
+31
View File
@@ -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",
],
},
}
+10 -6
View File
@@ -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:
@@ -0,0 +1,53 @@
from __future__ import annotations
from services.mesh import mesh_dm_connect_delivery as connect
def test_should_auto_release_for_connect_intent():
payload = {
"delivery_class": "request",
"connect_intent": "contact_request",
"recipient_id": "!sb_peer",
}
assert connect.should_auto_release_dm_payload(payload) is True
def test_should_auto_release_for_lookup_peer_url():
payload = {
"delivery_class": "request",
"lookup_peer_url": "http://owner.onion:8000",
"recipient_id": "!sb_peer",
}
assert connect.should_auto_release_dm_payload(payload) is True
def test_should_not_auto_release_shared_lane():
payload = {
"delivery_class": "shared",
"connect_intent": "contact_request",
"recipient_id": "!sb_peer",
}
assert connect.should_auto_release_dm_payload(payload) is False
def test_enrich_connect_release_payload_prefers_explicit_lookup():
enriched = connect.enrich_connect_release_payload(
{
"recipient_id": "!sb_peer",
"lookup_peer_url": "http://owner.onion:8000/",
}
)
assert enriched["lookup_peer_url"] == "http://owner.onion:8000"
assert enriched["relay_push_peer_urls"] == ["http://owner.onion:8000"]
def test_relay_push_peer_urls_dedupes_and_prioritizes_lookup():
urls = connect.relay_push_peer_urls_for_payload(
{
"lookup_peer_url": "http://owner.onion:8000",
"relay_push_peer_urls": ["http://relay.onion:8000", "http://owner.onion:8000"],
}
)
assert urls[0] == "http://owner.onion:8000"
assert "http://relay.onion:8000" in urls
assert len(urls) == 2
@@ -0,0 +1,45 @@
"""dm_get_pubkey resolves invite handles across the private fleet."""
from __future__ import annotations
from unittest.mock import patch
import pytest
@pytest.mark.asyncio
async def test_dm_get_pubkey_falls_back_to_fleet_prekey_lookup():
import main
request = main.Request(
{
"type": "http",
"method": "GET",
"path": "/api/mesh/dm/pubkey",
"headers": [],
"client": ("127.0.0.1", 12345),
}
)
remote_bundle = {
"ok": True,
"agent_id": "!sb_peer_test",
"identity_dh_pub_key": "Uo/wk78hu+ISyT9iCjNhcWgiANaHSXLMyNLn2q8YCkc=",
"dh_algo": "X25519",
"public_key": "v0pVNDQAz8wzvpMfIURjjVyCHhKZlAmrDPGaqzoJ7Rk=",
"public_key_algo": "Ed25519",
"signature": "sig",
"sequence": 1,
"bundle": {"identity_dh_pub_key": "Uo/wk78hu+ISyT9iCjNhcWgiANaHSXLMyNLn2q8YCkc="},
}
with patch("services.mesh.mesh_dm_relay.dm_relay") as relay, patch(
"services.mesh.mesh_wormhole_prekey.fetch_dm_prekey_bundle",
return_value=remote_bundle,
):
relay.get_dh_key_by_lookup.return_value = (None, "")
result = await main.dm_get_pubkey(request, lookup_token="fleet-handle-token")
assert result["ok"] is True
assert result["agent_id"] == "!sb_peer_test"
assert result["dh_pub_key"] == "Uo/wk78hu+ISyT9iCjNhcWgiANaHSXLMyNLn2q8YCkc="
@@ -0,0 +1,126 @@
from types import SimpleNamespace
from services.mesh import mesh_infonet_relay_bootstrap as relay_bootstrap
def test_relay_auto_wormhole_skipped_by_default(monkeypatch):
monkeypatch.setattr(
relay_bootstrap,
"get_settings",
lambda: SimpleNamespace(
MESH_INFONET_RELAY_AUTO_WORMHOLE=False,
MESH_INFONET_RELAY_AUTO_WORMHOLE_DISABLED=False,
MESH_BOOTSTRAP_SIGNER_PRIVATE_KEY="",
),
)
assert relay_bootstrap.infonet_relay_auto_wormhole_requested() is False
def test_relay_auto_wormhole_enabled_by_flag(monkeypatch):
monkeypatch.setattr(
relay_bootstrap,
"get_settings",
lambda: SimpleNamespace(
MESH_INFONET_RELAY_AUTO_WORMHOLE=True,
MESH_INFONET_RELAY_AUTO_WORMHOLE_DISABLED=False,
MESH_BOOTSTRAP_SIGNER_PRIVATE_KEY="",
),
)
assert relay_bootstrap.infonet_relay_auto_wormhole_requested() is True
def test_relay_auto_wormhole_enabled_by_seed_signer_key(monkeypatch):
monkeypatch.setattr(
relay_bootstrap,
"get_settings",
lambda: SimpleNamespace(
MESH_INFONET_RELAY_AUTO_WORMHOLE=False,
MESH_INFONET_RELAY_AUTO_WORMHOLE_DISABLED=False,
MESH_BOOTSTRAP_SIGNER_PRIVATE_KEY="seed-private-key",
),
)
assert relay_bootstrap.infonet_relay_auto_wormhole_requested() is True
def test_relay_auto_wormhole_disabled_override(monkeypatch):
monkeypatch.setattr(
relay_bootstrap,
"get_settings",
lambda: SimpleNamespace(
MESH_INFONET_RELAY_AUTO_WORMHOLE=True,
MESH_INFONET_RELAY_AUTO_WORMHOLE_DISABLED=True,
MESH_BOOTSTRAP_SIGNER_PRIVATE_KEY="seed-private-key",
),
)
assert relay_bootstrap.infonet_relay_auto_wormhole_requested() is False
def test_ensure_relay_wormhole_writes_settings_and_connects(monkeypatch, tmp_path):
wormhole_file = tmp_path / "wormhole.json"
monkeypatch.setattr(relay_bootstrap, "WORMHOLE_FILE", wormhole_file, raising=False)
monkeypatch.setattr(
"services.wormhole_settings.WORMHOLE_FILE",
wormhole_file,
)
monkeypatch.setattr(
"services.wormhole_settings.DATA_DIR",
tmp_path,
)
settings = SimpleNamespace(
MESH_INFONET_RELAY_AUTO_WORMHOLE=True,
MESH_INFONET_RELAY_AUTO_WORMHOLE_DISABLED=False,
MESH_BOOTSTRAP_SIGNER_PRIVATE_KEY="",
MESH_ARTI_SOCKS_PORT=9050,
)
monkeypatch.setattr(relay_bootstrap, "get_settings", lambda: settings)
tor_calls: list[int] = []
class _TorService:
def start(self, *, target_port: int):
tor_calls.append(target_port)
return {"ok": True, "hostname": "example.onion"}
env_writes: list[tuple[str, str]] = []
def _fake_write_env_value(key: str, value: str) -> None:
env_writes.append((key, value))
wormhole_calls: list[str] = []
def _fake_restart_wormhole(*, reason: str):
wormhole_calls.append(f"restart:{reason}")
return {"connected": True, "reason": reason}
def _fake_connect_wormhole(*, reason: str):
wormhole_calls.append(f"connect:{reason}")
return {"connected": True, "reason": reason}
monkeypatch.setattr(
"services.tor_hidden_service.tor_service",
_TorService(),
)
monkeypatch.setattr("routers.ai_intel._write_env_value", _fake_write_env_value)
monkeypatch.setattr(
"services.wormhole_supervisor.restart_wormhole",
_fake_restart_wormhole,
)
monkeypatch.setattr(
"services.wormhole_supervisor.connect_wormhole",
_fake_connect_wormhole,
)
result = relay_bootstrap.ensure_infonet_relay_wormhole_ready(reason="test_relay")
assert result["ok"] is True
assert result["skipped"] is False
assert result["settings_updated"] is True
assert tor_calls == [8000]
assert env_writes == [("MESH_ARTI_ENABLED", "true")]
assert wormhole_calls == ["restart:test_relay"]
saved = relay_bootstrap.read_wormhole_settings()
assert saved["enabled"] is True
assert saved["transport"] == "tor_arti"
assert saved["socks_proxy"] == "socks5h://127.0.0.1:9050"
assert saved["anonymous_mode"] is True
@@ -111,42 +111,101 @@ def test_dm_send_keeps_encrypted_payloads_off_ledger(tmp_path, monkeypatch):
assert append_called["value"] is False
def test_dm_request_send_rejects_unverified_first_contact(tmp_path, monkeypatch):
def test_dm_request_send_allows_unverified_first_contact(tmp_path, monkeypatch):
import main
from services import wormhole_supervisor
from services.mesh import mesh_dm_relay, mesh_wormhole_contacts
monkeypatch.setattr(mesh_wormhole_contacts, "DATA_DIR", tmp_path)
monkeypatch.setattr(mesh_wormhole_contacts, "CONTACTS_FILE", tmp_path / "wormhole_dm_contacts.json")
from services.mesh import mesh_hashchain
append_called = {"value": False}
monkeypatch.setattr(main, "_verify_signed_write", lambda **kwargs: (True, ""))
monkeypatch.setattr(main, "_secure_dm_enabled", lambda: False)
monkeypatch.setattr(wormhole_supervisor, "get_transport_tier", lambda: "private_transitional")
monkeypatch.setattr(mesh_dm_relay.dm_relay, "consume_nonce", lambda *_args, **_kwargs: (True, "ok"))
monkeypatch.setattr(mesh_hashchain.infonet, "validate_and_set_sequence", lambda *_args, **_kwargs: (True, ""))
def fake_append(**kwargs):
append_called["value"] = True
return {"event_id": "dm-request-e2e"}
monkeypatch.setattr(mesh_hashchain.infonet, "append_private_dm_message", fake_append)
monkeypatch.setattr(
main,
"consume_wormhole_dm_sender_token",
lambda **kwargs: {
"ok": True,
"sender_token_hash": "reqtok-first-contact",
"sender_id": "alice",
"public_key": "cHVi",
"public_key_algo": "Ed25519",
"protocol_version": "infonet/2",
"recipient_id": kwargs.get("recipient_id", "") or "bob",
"delivery_class": kwargs.get("delivery_class", "") or "request",
},
)
monkeypatch.setattr(
mesh_dm_relay.dm_relay,
"deposit",
lambda **kwargs: {
"ok": True,
"msg_id": kwargs.get("msg_id", ""),
"detail": "stored",
},
)
from services.mesh.mesh_protocol import build_signed_context
timestamp = int(time.time())
payload = {
"recipient_id": "bob",
"delivery_class": "request",
"recipient_token": "",
"ciphertext": "x3dh1:opaque",
"msg_id": "m2",
"timestamp": timestamp,
"format": "x3dh1",
"transport_lock": "private_strong",
}
signed_context = 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="alice",
sequence=1,
payload=payload,
recipient_id="bob",
)
req = _json_request(
"/api/mesh/dm/send",
{
"sender_id": "alice",
"recipient_id": "bob",
"sender_id": "",
"sender_token": "opaque-request-token",
"recipient_id": "",
"delivery_class": "request",
"recipient_token": "",
"ciphertext": "x3dh1:opaque",
"format": "x3dh1",
"msg_id": "m2",
"timestamp": int(time.time()),
"public_key": "cHVi",
"public_key_algo": "Ed25519",
"timestamp": timestamp,
"public_key": "",
"public_key_algo": "",
"signature": "sig",
"sequence": 1,
"protocol_version": "infonet/2",
"protocol_version": "",
"transport_lock": "private_strong",
"signed_context": signed_context,
},
)
response = asyncio.run(main.dm_send(req))
assert response["ok"] is False
assert response["detail"] == "signed invite or SAS verification required before secure first contact"
assert response["trust_level"] == "unpinned"
assert response["ok"] is True
def test_dm_key_registration_keeps_key_material_off_ledger(monkeypatch):
@@ -618,38 +618,32 @@ class TestFetchPrekeyBundleByLookup:
record = _valid_bundle_record("test-agent")
requested_urls: list[str] = []
monkeypatch.setenv("MESH_BOOTSTRAP_SEED_PEERS", "https://seed.example")
monkeypatch.setenv("MESH_DEFAULT_SYNC_PEERS", "")
monkeypatch.setenv("MESH_RELAY_PEERS", "")
get_settings.cache_clear()
def _public_lookup(lookup_token: str, **_kwargs):
requested_urls.append(
f"http://seed.onion:8000/api/mesh/dm/prekey-bundle?lookup_token={lookup_token}"
)
return {
"ok": True,
"agent_id": record["agent_id"],
"lookup_mode": "invite_lookup_handle",
"public_lookup": True,
"identity_dh_pub_key": record["dh_pub_key"],
"dh_algo": record["dh_algo"],
"public_key": record["public_key"],
"public_key_algo": record["public_key_algo"],
"protocol_version": record["protocol_version"],
"sequence": 1,
"bundle": record["bundle"],
}
class _Response:
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def read(self, _limit: int = -1):
return json.dumps(
{
"ok": True,
"identity_dh_pub_key": record["dh_pub_key"],
"dh_algo": record["dh_algo"],
"public_key": record["public_key"],
"public_key_algo": record["public_key_algo"],
"protocol_version": record["protocol_version"],
"sequence": 1,
"signed_at": int(record["bundle"].get("signed_at", 0) or 0),
"bundle": record["bundle"],
}
).encode("utf-8")
def _urlopen(request, timeout=0):
requested_urls.append(str(getattr(request, "full_url", "")))
return _Response()
monkeypatch.setattr("services.mesh.mesh_wormhole_prekey.urllib.request.urlopen", _urlopen)
monkeypatch.setattr(
"services.mesh.mesh_wormhole_prekey._fetch_dm_prekey_bundle_from_peer_lookup",
lambda *_args, **_kwargs: {"ok": False, "detail": "peer prekey lookup unavailable"},
)
monkeypatch.setattr(
"services.mesh.mesh_wormhole_prekey._fetch_dm_prekey_bundle_from_public_lookup",
_public_lookup,
)
from services.mesh.mesh_wormhole_prekey import fetch_dm_prekey_bundle
@@ -668,33 +662,20 @@ class TestFetchPrekeyBundleByLookup:
_isolated_relay(tmp_path, monkeypatch)
requested_urls: list[str] = []
monkeypatch.setenv("MESH_BOOTSTRAP_SEED_PEERS", "https://seed.example")
monkeypatch.setenv("MESH_DEFAULT_SYNC_PEERS", "")
monkeypatch.setenv("MESH_RELAY_PEERS", "")
get_settings.cache_clear()
def _public_lookup(lookup_token: str, **_kwargs):
requested_urls.append(
f"http://seed.onion:8000/api/mesh/dm/prekey-bundle?lookup_token={lookup_token}"
)
return {"ok": False, "detail": "peer prekey lookup still preparing"}
class _Response:
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def read(self, _limit: int = -1):
return json.dumps(
{
"ok": True,
"pending": True,
"status": "preparing_private_lane",
"detail": "transport tier insufficient",
}
).encode("utf-8")
def _urlopen(request, timeout=0):
requested_urls.append(str(getattr(request, "full_url", "")))
return _Response()
monkeypatch.setattr("services.mesh.mesh_wormhole_prekey.urllib.request.urlopen", _urlopen)
monkeypatch.setattr(
"services.mesh.mesh_wormhole_prekey._fetch_dm_prekey_bundle_from_peer_lookup",
lambda *_args, **_kwargs: {"ok": False, "detail": "peer prekey lookup unavailable"},
)
monkeypatch.setattr(
"services.mesh.mesh_wormhole_prekey._fetch_dm_prekey_bundle_from_public_lookup",
_public_lookup,
)
from services.mesh.mesh_wormhole_prekey import fetch_dm_prekey_bundle
@@ -807,6 +788,16 @@ class TestFetchPrekeyBundleByLookup:
monkeypatch.setenv("MESH_DEV_ALLOW_LEGACY_COMPAT", "true")
monkeypatch.setenv("MESH_ALLOW_LEGACY_AGENT_ID_LOOKUP_UNTIL", "2026-06-01")
get_settings.cache_clear()
monkeypatch.setattr(
mesh_wormhole_prekey,
"_validate_bundle_record",
lambda *_args, **_kwargs: (True, ""),
)
monkeypatch.setattr(
mesh_wormhole_prekey,
"legacy_agent_id_lookup_blocked",
lambda: False,
)
mesh_wormhole_prekey._WARNED_LEGACY_PREKEY_LOOKUPS.clear()
caplog.clear()
caplog.set_level("WARNING")
@@ -874,3 +865,55 @@ class TestFetchPrekeyBundleByLookup:
)
finally:
get_settings.cache_clear()
def test_invite_lookup_peer_order_prefers_active_over_bootstrap(monkeypatch):
from services.mesh import mesh_wormhole_prekey as prekey_mod
monkeypatch.setenv(
"MESH_BOOTSTRAP_SEED_PEERS",
"http://seed-a.onion:8000,http://seed-b.onion:8000,http://seed-c.onion:8000,http://seed-d.onion:8000",
)
monkeypatch.setattr(
"services.mesh.mesh_router.active_sync_peer_urls",
lambda: [
"http://active-peer.onion:8000",
"http://another-active.onion:8000",
],
)
monkeypatch.setattr(
prekey_mod,
"_discovered_push_peer_urls",
lambda **kwargs: [],
)
get_settings.cache_clear()
ordered = prekey_mod._prioritized_invite_lookup_peer_urls(
preferred=["http://pinned-peer.onion:8000"],
)
assert ordered[0] == "http://pinned-peer.onion:8000"
assert ordered[1:3] == [
"http://active-peer.onion:8000",
"http://another-active.onion:8000",
]
assert ordered[-prekey_mod._INVITE_LOOKUP_MAX_BOOTSTRAP_PEERS:] == [
"http://seed-a.onion:8000",
"http://seed-b.onion:8000",
"http://seed-c.onion:8000",
]
assert "http://seed-d.onion:8000" not in ordered
get_settings.cache_clear()
def test_invite_export_includes_lookup_peer_url(tmp_path, monkeypatch):
_isolated_invite_state(tmp_path, monkeypatch)
monkeypatch.setenv("MESH_PUBLIC_PEER_URL", "http://owner-node.onion:8000")
from services.mesh.mesh_wormhole_identity import export_wormhole_dm_invite
exported = export_wormhole_dm_invite(label="routing-test")
payload = dict(exported.get("invite", {}).get("payload") or {})
assert payload.get("prekey_lookup_handle")
assert payload.get("lookup_peer_url") == "http://owner-node.onion:8000"
+21 -2
View File
@@ -71,7 +71,11 @@ def test_dispatcher_chooses_dm_relay_when_direct_path_unavailable_but_lane_floor
assert len(deposit_calls) == 1
def test_dispatcher_does_not_release_dm_below_private_strong():
def test_dispatcher_does_not_release_dm_below_private_transitional_when_rns_disabled(monkeypatch):
monkeypatch.setattr(
"services.wormhole_supervisor.get_wormhole_state",
lambda: {"rns_enabled": False},
)
result = attempt_private_release(
lane="dm",
current_tier="private_control_only",
@@ -80,7 +84,22 @@ def test_dispatcher_does_not_release_dm_below_private_strong():
assert result["ok"] is False
assert result["no_acceptable_path"] is True
assert result["policy_reason_code"] == "dm_release_waiting_for_private_strong"
assert result["policy_reason_code"] == "dm_release_waiting_for_private_transitional"
assert result["required_tier"] == "private_transitional"
def test_dispatcher_still_requires_private_strong_when_rns_enabled(monkeypatch):
monkeypatch.setattr(
"services.wormhole_supervisor.get_wormhole_state",
lambda: {"rns_enabled": True},
)
result = attempt_private_release(
lane="dm",
current_tier="private_transitional",
payload={"msg_id": "dm-transitional"},
)
assert result["ok"] is False
assert result["required_tier"] == "private_strong"
@@ -1,4 +1,5 @@
import base64
import json
import time
from cryptography.hazmat.primitives import serialization
@@ -180,6 +181,31 @@ def test_private_dm_hashchain_rejects_non_sealed_ciphertext_shape(tmp_path, monk
raise AssertionError("private DM append accepted non-base64 ciphertext")
def test_private_dm_hashchain_accepts_x3dh1_prefixed_ciphertext(tmp_path, monkeypatch):
inf = _fresh_infonet(tmp_path, monkeypatch)
private_key, public_key, node_id = _keypair()
envelope = {
"h": {"ik_pub": "aGVsbG8=", "ek_pub": "d29ybGQ=", "spk_id": 1, "otk_id": 0},
"ct": base64.b64encode(b"\x00" * 32).decode("ascii"),
}
payload = _payload(msg_id="dm-x3dh-1")
payload["ciphertext"] = "x3dh1:" + base64.b64encode(
json.dumps(envelope, sort_keys=True, separators=(",", ":")).encode("utf-8")
).decode("ascii")
event = inf.append_private_dm_message(
node_id=node_id,
payload=payload,
signature=_signature(private_key, node_id, 1, payload),
sequence=1,
public_key=public_key,
public_key_algo="Ed25519",
protocol_version=mesh_protocol.PROTOCOL_VERSION,
timestamp=float(payload["timestamp"]),
)
assert event["event_type"] == "dm_message"
assert str(event["payload"]["ciphertext"]).startswith("x3dh1:")
def test_hydrate_dm_relay_from_chain_delivers_to_poll_claim(tmp_path, monkeypatch):
inf = _fresh_infonet(tmp_path / "chain", monkeypatch)
relay = _fresh_relay(tmp_path / "relay", monkeypatch)
@@ -216,19 +216,19 @@ def test_authenticated_wormhole_status_can_request_diagnostic_private_delivery_s
assert item["meta"]["peer_id"] == "bob"
def test_dm_pubkey_lookup_token_ordinary_response_omits_resolved_agent_id(monkeypatch):
def test_dm_pubkey_lookup_token_ordinary_response_includes_resolved_agent_id(monkeypatch):
monkeypatch.setattr(main, "_check_scoped_auth", lambda *_args, **_kwargs: (False, "no"))
monkeypatch.setattr(main, "_is_debug_test_request", lambda *_args, **_kwargs: False)
monkeypatch.setattr(
"services.mesh.mesh_dm_relay.dm_relay.get_dh_key_by_lookup",
lambda _lookup_token: ({"dh_pub": "pub", "dh_algo": "X25519"}, "peer-123"),
lambda _lookup_token: ({"dh_pub_key": "pub", "dh_algo": "X25519"}, "peer-123"),
)
result = asyncio.run(main.dm_get_pubkey(_request("/api/mesh/dm/pubkey"), lookup_token="invite-handle"))
assert result["ok"] is True
assert result["lookup_mode"] == "invite_lookup_handle"
assert "agent_id" not in result
assert result["agent_id"] == "peer-123"
def test_dm_pubkey_lookup_token_diagnostic_response_exposes_resolved_agent_id(monkeypatch):
@@ -249,7 +249,7 @@ def test_dm_pubkey_lookup_token_diagnostic_response_exposes_resolved_agent_id(mo
assert result["agent_id"] == "peer-123"
def test_prekey_bundle_lookup_token_ordinary_response_omits_resolved_agent_id(monkeypatch):
def test_prekey_bundle_lookup_token_ordinary_response_includes_resolved_agent_id(monkeypatch):
monkeypatch.setattr(main, "_check_scoped_auth", lambda *_args, **_kwargs: (False, "no"))
monkeypatch.setattr(main, "_is_debug_test_request", lambda *_args, **_kwargs: False)
monkeypatch.setattr(
@@ -273,7 +273,7 @@ def test_prekey_bundle_lookup_token_ordinary_response_omits_resolved_agent_id(mo
assert result["ok"] is True
assert result["lookup_mode"] == "invite_lookup_handle"
assert "agent_id" not in result
assert result["agent_id"] == "peer-456"
assert result["trust_fingerprint"] == "aa" * 16
@@ -465,6 +465,45 @@ def test_user_facing_status_mapping_remains_plain_language_and_stable():
assert evaluate_network_release("dm", "private_strong").status_label == "Delivered privately"
def test_queued_dm_releases_at_private_transitional_when_rns_disabled(monkeypatch):
deposit_calls = []
monkeypatch.setattr(
"services.wormhole_supervisor.get_wormhole_state",
lambda: {"rns_enabled": False},
)
monkeypatch.setattr(
"services.wormhole_supervisor.get_transport_tier",
lambda: "private_transitional",
)
monkeypatch.setattr(mesh_private_release_worker, "_secure_dm_enabled", lambda: False)
monkeypatch.setattr(mesh_private_release_worker, "_rns_private_dm_ready", lambda: False)
monkeypatch.setattr(mesh_private_release_worker, "_maybe_apply_dm_relay_jitter", lambda: None)
monkeypatch.setattr(
"services.mesh.mesh_dm_relay.dm_relay.deposit",
lambda **kwargs: deposit_calls.append(kwargs) or {"ok": True, "msg_id": kwargs["msg_id"]},
)
queued = main._queue_dm_release(
current_tier="private_transitional",
payload={
"msg_id": "dm-tor-only-1",
"sender_id": "alice",
"recipient_id": "bob",
"delivery_class": "request",
"sender_token_hash": "abc123",
"ciphertext": "x3dh1:ciphertext",
"timestamp": 1,
},
)
mesh_private_release_worker.private_release_worker.run_once()
item = _outbox_item(queued["outbox_id"], exposure="diagnostic")
assert len(deposit_calls) == 1
assert item["release_state"] == "delivered"
def test_outbox_exposes_publishing_state_without_claiming_delivery():
item = mesh_private_outbox.private_delivery_outbox.enqueue(
lane="dm",
@@ -0,0 +1,81 @@
"""OpenClaw Infonet delegation — command allowlist and dispatch."""
from __future__ import annotations
from unittest.mock import patch
from services.openclaw_channel import (
READ_COMMANDS,
WRITE_COMMANDS,
_dispatch_command,
allowed_commands,
)
from services.openclaw_channel import CommandChannel
INFONET_READS = frozenset({
"infonet_status",
"list_gates",
"read_gate_messages",
"poll_dms",
})
INFONET_WRITES = frozenset({
"ensure_infonet_ready",
"join_infonet_swarm",
"post_gate_message",
"cast_vote",
"send_dm",
})
def test_infonet_commands_in_allowlists():
assert INFONET_READS <= READ_COMMANDS
assert INFONET_WRITES <= WRITE_COMMANDS
def test_restricted_tier_allows_infonet_reads_only():
allowed = allowed_commands("restricted")
assert INFONET_READS <= allowed
assert not (INFONET_WRITES & allowed)
def test_full_tier_allows_infonet_writes():
allowed = allowed_commands("full")
assert INFONET_WRITES <= allowed
def test_restricted_tier_blocks_post_gate_message():
channel = CommandChannel()
result = channel.submit_command("post_gate_message", {"gate_id": "infonet", "plaintext": "hi"})
assert result["ok"] is False
assert "full access tier" in str(result.get("detail", ""))
def test_dispatch_infonet_status_mocked():
fake = {"ok": True, "chain": {"length": 3}, "valid": True}
with patch("services.openclaw_infonet.get_infonet_status", return_value=fake):
result = _dispatch_command("infonet_status", {})
assert result == fake
def test_dispatch_list_gates_mocked():
fake = {"ok": True, "gates": [{"id": "infonet"}]}
with patch("services.openclaw_infonet.list_gates", return_value=fake):
result = _dispatch_command("list_gates", {})
assert result["gates"][0]["id"] == "infonet"
def test_dispatch_post_gate_message_mocked():
fake = {"ok": True, "event_id": "evt-test"}
with patch("services.openclaw_infonet.post_gate_message", return_value=fake):
result = _dispatch_command(
"post_gate_message",
{"gate_id": "infonet", "plaintext": "agent bulletin"},
)
assert result["event_id"] == "evt-test"
def test_cast_vote_rejects_invalid_vote():
result = _dispatch_command("cast_vote", {"target_id": "!sb_test", "vote": 2})
assert result["ok"] is False
+8
View File
@@ -91,3 +91,11 @@ def test_plan_playbook_track_snapshot_requires_query():
def test_expensive_commands_set():
assert "get_report" in EXPENSIVE_COMMANDS
assert "route_query" not in EXPENSIVE_COMMANDS
def test_routing_manifest_includes_infonet_hints():
manifest = routing_manifest()
recipes = " ".join(item.get("use", "") for item in manifest.get("recipes", []))
assert "post_gate_message" in recipes
writes = manifest.get("agent_surface", {}).get("writes", [])
assert "post_gate_message" in writes
+2
View File
@@ -19,6 +19,8 @@ services:
MESH_DEFAULT_SYNC_PEERS: ""
MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY: "ul1d0kj/ODPIp0OhHzX8eLAVXzJ3CVvzW1vn2IC6q3I="
MESH_SWARM_MANIFEST_PULL_INTERVAL_S: "300"
# Fleet testnet HMAC — overrides stale per-node .env so announce/push auth matches seed.
MESH_PEER_PUSH_SECRET: "b7GoqsvoUD9MV7tyt0ZOzMptLA84QG6KCfaV9nDqz5Y"
frontend:
build:
+4
View File
@@ -6,6 +6,10 @@ services:
ports:
- "127.0.0.1:8000:8000"
env_file: .env
environment:
# Keep Tor wormhole up across redeploys (no NODE UI on headless relay).
MESH_INFONET_RELAY_AUTO_WORMHOLE: "true"
MESH_ARTI_ENABLED: "true"
volumes:
- relay_data:/app/data
restart: unless-stopped
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import {
isLikelyDmShortAddress,
parseDmInviteImportBlob,
inviteFromParsedBlob,
} from '@/mesh/dmConnect';
describe('dmConnect', () => {
it('detects short lookup handles', () => {
expect(isLikelyDmShortAddress('5881eb8705c9abc1234567890abcd')).toBe(true);
expect(isLikelyDmShortAddress('{"type":"invite"}')).toBe(false);
});
it('parses short address without JSON', () => {
const parsed = parseDmInviteImportBlob('abcd1234ef567890abcd1234ef567890');
expect(parsed.short_address).toBe('abcd1234ef567890abcd1234ef567890');
});
it('unwraps nested invite objects', () => {
const invite = { event_type: 'dm_invite', payload: {} };
const parsed = inviteFromParsedBlob({ invite, version: 1 });
expect(parsed).toEqual(invite);
});
});
@@ -23,7 +23,12 @@ describe('fetchDmPublicKey lookup posture', () => {
it('uses invite lookup handles without enabling legacy agent-id lookup', async () => {
fetchMock.mockResolvedValueOnce({
json: async () => ({ ok: true, dh_pub_key: 'peer-dh', lookup_mode: 'invite_lookup_handle' }),
json: async () => ({
ok: true,
agent_id: '!sb_peer',
dh_pub_key: 'peer-dh',
lookup_mode: 'invite_lookup_handle',
}),
});
const mod = await import('@/mesh/meshDmClient');
@@ -39,6 +44,28 @@ describe('fetchDmPublicKey lookup posture', () => {
);
});
it('falls back to prekey-bundle when pubkey lookup lacks agent_id', async () => {
fetchMock
.mockResolvedValueOnce({
json: async () => ({ ok: true, dh_pub_key: 'peer-dh', lookup_mode: 'invite_lookup_handle' }),
})
.mockResolvedValueOnce({
json: async () => ({
ok: true,
agent_id: '!sb_peer',
lookup_mode: 'invite_lookup_handle',
bundle: { identity_dh_pub_key: 'peer-dh' },
}),
});
const mod = await import('@/mesh/meshDmClient');
const result = await mod.fetchDmPublicKey('http://localhost:8000', '', 'invite-handle-123');
expect(result?.agent_id).toBe('!sb_peer');
expect(result?.dh_pub_key).toBe('peer-dh');
expect(fetchMock).toHaveBeenCalledTimes(2);
});
it('still supports explicit legacy agent-id lookup for migration-only paths', async () => {
fetchMock.mockResolvedValueOnce({
json: async () => ({ ok: true, dh_pub_key: 'peer-dh', lookup_mode: 'legacy_agent_id' }),
@@ -682,6 +682,20 @@ export default function InfonetShell({
{/* Main Terminal Area */}
<div className="flex-1 overflow-y-auto pr-4 pb-4">
<button
type="button"
onClick={() => handleNavigate('messages')}
className="w-full mb-6 text-left border border-emerald-500/30 bg-emerald-950/10 hover:bg-emerald-950/20 px-4 py-3 transition-colors"
>
<div className="flex items-center gap-2 text-emerald-300 text-xs tracking-[0.2em] uppercase font-bold">
<Mail size={14} />
Secure Messages Quick Connect
</div>
<p className="mt-2 text-sm text-gray-400 leading-relaxed">
Message someone on the fleet in three steps: copy your short address (or ask for theirs),
paste it in Secure Messages, tap Send Request they tap Accept. No terminal commands.
</p>
</button>
<div className="flex flex-col lg:flex-row justify-between items-start gap-6 mb-8">
<TrendingPosts />
@@ -66,6 +66,7 @@ import {
purgeBrowserContactGraph,
purgeBrowserSigningMaterial,
removeContact,
severContact,
unblockContact,
unwrapSenderSealPayload,
updateContact,
@@ -74,6 +75,7 @@ import {
type Contact,
type NodeIdentity,
} from '@/mesh/meshIdentity';
import { connectDeliveryMeta, ensureDmOutboxReleased } from '@/mesh/dmConnectDelivery';
import {
getSenderRecoveryState,
recoverSenderSealWithFallback,
@@ -1516,6 +1518,7 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
API_BASE,
senderId,
existingContact?.invitePinnedPrekeyLookupHandle,
{ lookupPeerUrl: existingContact?.invitePinnedLookupPeerUrl },
);
if (senderKey?.dh_pub_key) {
const sharedKey = await deriveSharedKey(String(senderKey.dh_pub_key));
@@ -1532,6 +1535,7 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
API_BASE,
senderId,
existingContact?.invitePinnedPrekeyLookupHandle,
{ lookupPeerUrl: existingContact?.invitePinnedLookupPeerUrl },
).catch(() => null);
if (senderKey?.dh_pub_key) {
addContact(senderId, String(senderKey.dh_pub_key), undefined, senderKey.dh_algo);
@@ -2000,7 +2004,9 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
'This contact needs their full contact address once before messages can be sent. Paste it in Contacts and the app will handle the rest.',
);
}
const targetKey = await fetchDmPublicKey(API_BASE, recipient, lookupHandle);
const targetKey = await fetchDmPublicKey(API_BASE, recipient, lookupHandle, {
lookupPeerUrl: recipientContact?.invitePinnedLookupPeerUrl,
});
if (!targetKey?.dh_pub_key) {
queuePendingDeliveryMail({
senderId: activeIdentity.nodeId,
@@ -2037,15 +2043,23 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
}
const msgId = `dm_${Date.now()}_${activeIdentity.nodeId.slice(-4)}`;
const timestamp = Math.floor(Date.now() / 1000);
const sent = await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: activeIdentity,
recipientId: recipient,
recipientDhPub: String(targetKey.dh_pub_key),
ciphertext,
msgId,
timestamp,
const connectMeta = connectDeliveryMeta({
intent: 'contact_request',
contact: recipientContact,
});
const sent = await ensureDmOutboxReleased(
await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: activeIdentity,
recipientId: recipient,
recipientDhPub: String(targetKey.dh_pub_key),
ciphertext,
msgId,
timestamp,
connectIntent: connectMeta.connectIntent,
lookupPeerUrl: connectMeta.lookupPeerUrl,
}),
);
if (!sent.ok) {
throw new Error(sent.detail || 'contact request failed');
}
@@ -2110,7 +2124,9 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
throw new Error('Secure mail is still preparing your private identity.');
}
const { registration, myDhPub } = await ensureLocalDmKey(activeIdentity);
const targetKey = await fetchDmPublicKey(API_BASE, '', shortAddress);
const targetKey = await fetchDmPublicKey(API_BASE, '', shortAddress, {
allowLegacyAgentId: false,
});
if (!targetKey?.dh_pub_key || !targetKey.agent_id) {
throw new Error('That address is not reachable yet. Ask them to copy their address again while their device is online.');
}
@@ -2136,15 +2152,18 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
}
const msgId = `dm_${Date.now()}_${activeIdentity.nodeId.slice(-4)}`;
const timestamp = Math.floor(Date.now() / 1000);
const sent = await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: activeIdentity,
recipientId: recipient,
recipientDhPub: String(targetKey.dh_pub_key),
ciphertext,
msgId,
timestamp,
});
const sent = await ensureDmOutboxReleased(
await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: activeIdentity,
recipientId: recipient,
recipientDhPub: String(targetKey.dh_pub_key),
ciphertext,
msgId,
timestamp,
connectIntent: 'invite_short_address',
}),
);
if (!sent.ok) {
throw new Error(sent.detail || 'contact request failed');
}
@@ -2224,6 +2243,12 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
invitePayload.prekey_lookup_handle ||
'',
),
invitePinnedLookupPeerUrl: String(
resultContact.invitePinnedLookupPeerUrl ||
(invite as Record<string, unknown>).lookup_peer_url ||
invitePayload.lookup_peer_url ||
'',
),
dhPubKey: String(resultContact.dhPubKey || resultContact.invitePinnedDhPubKey || ''),
};
const mergedContacts = importedPeerId
@@ -2269,6 +2294,26 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
}
}, [applyHydratedContacts, handleSendShortAddressRequest, inviteImportAlias, inviteImportBlob, loadBackendContacts, syncSecureMailRuntime]);
const handleSeverContact = useCallback(
async (peerId: string) => {
const name = displayNameForPeer(peerId, contacts);
setComposeError('');
setComposeStatus('');
try {
await severContact(peerId);
setContacts(getContacts());
setComposeStatus(
`Secure contact ended with ${name}. You can message again only after a new request and approval.`,
);
} catch (error) {
setComposeError(
error instanceof Error ? error.message : 'Could not end secure contact right now.',
);
}
},
[contacts],
);
const refreshDmAddressHandles = useCallback(async () => {
try {
const result = await listWormholeDmInviteHandles();
@@ -2501,6 +2546,7 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
API_BASE,
mail.senderId,
existingContact?.invitePinnedPrekeyLookupHandle,
{ lookupPeerUrl: existingContact?.invitePinnedLookupPeerUrl },
).catch(() => null);
const dhPubKey = String(registry?.dh_pub_key || mail.requestDhPubKey || '').trim();
const dhAlgo = String(registry?.dh_algo || mail.requestDhAlgo || 'X25519').trim();
@@ -2551,15 +2597,19 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
const msgId = `dm_${Date.now()}_${activeIdentity.nodeId.slice(-4)}`;
const timestamp = Math.floor(Date.now() / 1000);
const sent = await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: activeIdentity,
recipientId: mail.senderId,
recipientDhPub: dhPubKey,
ciphertext,
msgId,
timestamp,
});
const sent = await ensureDmOutboxReleased(
await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: activeIdentity,
recipientId: mail.senderId,
recipientDhPub: dhPubKey,
ciphertext,
msgId,
timestamp,
connectIntent: 'contact_accept',
lookupPeerUrl: existingContact?.invitePinnedLookupPeerUrl,
}),
);
if (!sent.ok) {
throw new Error(sent.detail || 'contact accept failed');
}
@@ -2715,7 +2765,9 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
<Mail size={24} className="mr-3" />
SECURE MESSAGES
</h1>
<p className="text-gray-500 text-sm mt-1">End-to-end encrypted peer-to-peer comms.</p>
<p className="text-gray-500 text-sm mt-1">
Copy your short address and send it to someone. They paste it here and tap Send Request you tap Accept. No terminal required.
</p>
</div>
<div className="border border-cyan-900/30 bg-cyan-950/10 px-4 py-3 text-[11px] tracking-[0.16em] uppercase text-cyan-300 mb-4 shrink-0">
@@ -2755,7 +2807,7 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
</div>
</>
) : (
'Your contact address is being prepared automatically. Share it with someone so they can message you.'
'Your contact address is being prepared. Copy the short address above and send it to anyone you want to message you.'
)}
</div>
</div>
@@ -3428,7 +3480,7 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
)}
{contact.sharedAlias && (
<div className="text-[11px] text-emerald-300 mt-2">
Shared alias: {contact.sharedAlias}
Shared lane open you can exchange secure mail.
</div>
)}
</div>
@@ -3466,6 +3518,18 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
{nextStep.label}
</button>
)}
{contact.sharedAlias && (
<button
onClick={() => void handleSeverContact(peerId)}
className="px-3 py-2 border border-violet-500/30 text-violet-200 text-sm tracking-[0.18em] uppercase"
title="Close the shared lane. A fresh contact request and approval will be required to message again."
>
<span className="inline-flex items-center gap-1.5">
<ShieldOff size={14} />
End Contact
</span>
</button>
)}
<button
onClick={() => {
blockContact(peerId);
+3 -9
View File
@@ -361,15 +361,9 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
setShowSas((prev) => !prev);
};
const handleRequestComposerAction = () => {
const targetId = addContactId.trim();
if (!targetId) return;
const inviteLookupHandle = String(
contacts[targetId]?.invitePinnedPrekeyLookupHandle || '',
).trim();
if (!inviteLookupHandle) {
openTerminal();
}
void handleRequestAccess(targetId);
const pasted = addContactId.trim();
if (!pasted) return;
void handleRequestAccess(pasted);
};
const meshActivationText =
publicMeshBlockedByWormhole
@@ -27,6 +27,7 @@ import {
addContact,
updateContact,
blockContact,
severContact,
getDMNotify,
nextSequence,
verifyEventSignature,
@@ -103,6 +104,7 @@ import {
isEncryptedGateEnvelope,
} from '@/mesh/gateEnvelope';
import { fetchWormholeSettings, joinWormhole, leaveWormhole } from '@/mesh/wormholeClient';
import { connectDeliveryMeta, ensureDmOutboxReleased } from '@/mesh/dmConnectDelivery';
import {
buildMailboxClaims,
countDmMailboxes,
@@ -2295,6 +2297,7 @@ export function useMeshChatController({
API_BASE,
m.sender_id,
senderContact?.invitePinnedPrekeyLookupHandle,
{ lookupPeerUrl: senderContact?.invitePinnedLookupPeerUrl },
);
if (senderKey?.dh_pub_key) {
const sharedKey = await deriveSharedKey(String(senderKey.dh_pub_key));
@@ -2310,6 +2313,7 @@ export function useMeshChatController({
API_BASE,
m.sender_id,
senderContact?.invitePinnedPrekeyLookupHandle,
{ lookupPeerUrl: senderContact?.invitePinnedLookupPeerUrl },
).catch(() => null);
if (senderKey?.dh_pub_key) {
addContact(m.sender_id, String(senderKey.dh_pub_key), undefined, senderKey.dh_algo);
@@ -3336,7 +3340,9 @@ export function useMeshChatController({
'import or re-import a signed invite before refreshing this contact; legacy direct lookup is disabled',
);
}
const registry = await fetchDmPublicKey(API_BASE, targetId, lookupHandle).catch(() => null);
const registry = await fetchDmPublicKey(API_BASE, targetId, lookupHandle, {
lookupPeerUrl: existing?.invitePinnedLookupPeerUrl,
}).catch(() => null);
if (!registry?.dh_pub_key) {
throw new Error(
'invite-scoped lookup failed for this contact; re-import a signed invite and try again',
@@ -3585,29 +3591,26 @@ export function useMeshChatController({
setTimeout(() => setSendError(''), 3000);
return;
}
if (requiresVerifiedFirstContact(getContacts()[targetId])) {
setSendError('import a signed invite before first secure contact; TOFU requests are disabled');
setTimeout(() => setSendError(''), 4000);
return;
}
if (wormholeEnabled && !wormholeReadyState) {
setSendError('wormhole required for dead drop');
setTimeout(() => setSendError(''), 3000);
return;
}
try {
const registration = await ensureRegisteredDmKey(API_BASE, identity!, { force: false });
const myPub = registration.dhPubKey;
if (!myPub) return;
const dhAlgo = registration.dhAlgo || getDHAlgo() || 'X25519';
const targetContact = getContacts()[targetId];
const lookupHandle = String(targetContact?.invitePinnedPrekeyLookupHandle || '').trim();
let lookupHandle = String(targetContact?.invitePinnedPrekeyLookupHandle || '').trim();
let resolvedTargetId = targetId;
if (!lookupHandle && /^[a-fA-F0-9]{32,}$/.test(targetId)) {
lookupHandle = targetId;
resolvedTargetId = '';
}
if (!lookupHandle) {
throw new Error(
'import or re-import a signed invite before sending a contact request; legacy direct lookup is disabled',
'Paste their short contact address (from Secure Messages → Copy Short Address), not their node id.',
);
}
const targetKey = await fetchDmPublicKey(API_BASE, targetId, lookupHandle);
const targetKey = await fetchDmPublicKey(API_BASE, resolvedTargetId, lookupHandle, {
lookupPeerUrl: targetContact?.invitePinnedLookupPeerUrl,
});
if (!targetKey?.dh_pub_key) {
throw new Error(
'invite-scoped lookup failed for this contact; re-import a signed invite and try again',
@@ -3631,12 +3634,13 @@ export function useMeshChatController({
geoHint = '';
}
}
const recipientId = String(targetKey.agent_id || resolvedTargetId || targetId).trim();
const requestPlaintext = buildContactOfferMessage(myPub, dhAlgo, geoHint || undefined);
let ciphertext = '';
const secureRequired = await isWormholeSecureRequired();
if (await canUseWormholeBootstrap()) {
try {
ciphertext = await bootstrapEncryptAccessRequest(targetId, requestPlaintext);
ciphertext = await bootstrapEncryptAccessRequest(recipientId, requestPlaintext);
} catch {
ciphertext = '';
}
@@ -3651,16 +3655,24 @@ export function useMeshChatController({
const msgId = `dm_${Date.now()}_${identity!.nodeId.slice(-4)}`;
const msgTimestamp = Math.floor(Date.now() / 1000);
await sleep(jitterDelay(ACCESS_REQUEST_BATCH_DELAY_MS, ACCESS_REQUEST_BATCH_JITTER_MS));
const connectMeta = connectDeliveryMeta({
intent: lookupHandle === targetId ? 'invite_short_address' : 'contact_request',
contact: targetContact,
});
await enqueueDmSend(async () => {
const sent = await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: identity!,
recipientId: targetId,
recipientDhPub: String(targetKey.dh_pub_key),
ciphertext,
msgId,
timestamp: msgTimestamp,
});
const sent = await ensureDmOutboxReleased(
await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: identity!,
recipientId,
recipientDhPub: String(targetKey.dh_pub_key),
ciphertext,
msgId,
timestamp: msgTimestamp,
connectIntent: connectMeta.connectIntent,
lookupPeerUrl: connectMeta.lookupPeerUrl,
}),
);
if (!sent.ok) {
throw new Error(sent.detail || 'access_request_send_failed');
}
@@ -3668,7 +3680,8 @@ export function useMeshChatController({
setLastDmTransport(sent.transport);
}
});
const updated = [...pendingSent, targetId];
const recipientForPending = String(targetKey.agent_id || resolvedTargetId || targetId).trim();
const updated = [...pendingSent, recipientForPending];
setPendingSent(updated, dmConsentScopeId);
setPendingSentState(updated);
} catch (err) {
@@ -3680,11 +3693,6 @@ export function useMeshChatController({
const handleAcceptRequest = async (senderId: string) => {
if (!hasId) return;
if (requiresVerifiedFirstContact(getContacts()[senderId])) {
setSendError('import a signed invite before accepting an unverified request');
setTimeout(() => setSendError(''), 4000);
return;
}
if (anonymousDmBlocked) {
setSendError('hidden transport required for anonymous dm');
setTimeout(() => setSendError(''), 3000);
@@ -3697,6 +3705,7 @@ export function useMeshChatController({
API_BASE,
senderId,
existingContact?.invitePinnedPrekeyLookupHandle,
{ lookupPeerUrl: existingContact?.invitePinnedLookupPeerUrl },
).catch(() => null);
const resolvedDhPubKey = String(registry?.dh_pub_key || req?.dh_pub_key || '').trim();
const resolvedDhAlgo = String(registry?.dh_algo || req?.dh_algo || 'X25519').trim();
@@ -3843,16 +3852,24 @@ export function useMeshChatController({
}
const msgId = `dm_${Date.now()}_${identity!.nodeId.slice(-4)}`;
const msgTimestamp = Math.floor(Date.now() / 1000);
const acceptMeta = connectDeliveryMeta({
intent: 'contact_accept',
contact: existingContact,
});
await enqueueDmSend(async () => {
const sent = await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: identity!,
recipientId: senderId,
recipientDhPub: resolvedDhPubKey,
ciphertext,
msgId,
timestamp: msgTimestamp,
});
const sent = await ensureDmOutboxReleased(
await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: identity!,
recipientId: senderId,
recipientDhPub: resolvedDhPubKey,
ciphertext,
msgId,
timestamp: msgTimestamp,
connectIntent: acceptMeta.connectIntent,
lookupPeerUrl: acceptMeta.lookupPeerUrl,
}),
);
if (!sent.ok) {
throw new Error(sent.detail || 'access_granted_send_failed');
}
@@ -3878,11 +3895,6 @@ export function useMeshChatController({
const handleDenyRequest = (senderId: string) => {
void (async () => {
if (requiresVerifiedFirstContact(getContacts()[senderId])) {
setSendError('import a signed invite before denying an unverified request');
setTimeout(() => setSendError(''), 4000);
return;
}
try {
const req = accessRequests.find((r) => r.sender_id === senderId);
const existingContact = getContacts()[senderId];
@@ -3893,6 +3905,7 @@ export function useMeshChatController({
API_BASE,
senderId,
existingContact?.invitePinnedPrekeyLookupHandle,
{ lookupPeerUrl: existingContact?.invitePinnedLookupPeerUrl },
).catch(() => null);
if (identity && targetKey?.dh_pub_key) {
const denyPlaintext = buildContactDenyMessage('declined');
@@ -3935,6 +3948,20 @@ export function useMeshChatController({
})();
};
const handleSeverContact = async (agentId: string) => {
try {
await severContact(agentId);
setContacts(getContacts());
if (selectedContact === agentId) {
setDmView('contacts');
}
} catch (err) {
const detail = err instanceof Error ? err.message : 'end contact failed';
setSendError(detail);
setTimeout(() => setSendError(''), 4000);
}
};
const handleBlockDM = async (agentId: string) => {
blockContact(agentId);
setContacts(getContacts());
@@ -4751,6 +4778,7 @@ export function useMeshChatController({
handleAcceptRequest,
handleDenyRequest,
handleBlockDM,
handleSeverContact,
handleVouch,
handleAddContact,
openChat,
+49
View File
@@ -0,0 +1,49 @@
/**
* Signal-style DM connect helpers paste a short address or full invite blob.
*/
export function isLikelyDmShortAddress(value: string): boolean {
const trimmed = value.trim();
return (
!trimmed.startsWith('{') &&
!trimmed.startsWith('[') &&
/^[a-zA-Z0-9_.:-]{16,}$/.test(trimmed)
);
}
export function parseDmInviteImportBlob(raw: string): Record<string, unknown> {
const trimmed = raw.trim();
if (!trimmed) {
throw new Error('Paste a contact address first.');
}
if (isLikelyDmShortAddress(trimmed)) {
return { short_address: trimmed };
}
try {
const parsed = JSON.parse(trimmed) as unknown;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('Contact address must be a signed address object.');
}
return parsed as Record<string, unknown>;
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error('That does not look like a contact address. Paste what they copied from Secure Messages.');
}
throw error;
}
}
export function inviteFromParsedBlob(parsed: Record<string, unknown>): Record<string, unknown> {
const nested = parsed.invite;
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
return nested as Record<string, unknown>;
}
return parsed;
}
export function shortHandle(peerId: string): string {
const value = String(peerId || '').trim();
if (!value) return 'unknown';
if (value.length <= 18) return value;
return `${value.slice(0, 10)}${value.slice(-6)}`;
}
+40
View File
@@ -0,0 +1,40 @@
import type { Contact } from '@/mesh/meshIdentity';
import type { DmSendResponse } from '@/mesh/meshDmClient';
import { updatePrivateDeliveryAction } from '@/mesh/wormholeClient';
export type DmConnectIntent =
| 'invite_short_address'
| 'invite_import'
| 'contact_request'
| 'contact_accept'
| 'contact_offer';
export function connectDeliveryMeta(options: {
intent: DmConnectIntent;
lookupPeerUrl?: string;
contact?: Partial<Contact> | null;
}): { connectIntent: DmConnectIntent; lookupPeerUrl?: string } {
const lookupPeerUrl = String(
options.lookupPeerUrl || options.contact?.invitePinnedLookupPeerUrl || '',
)
.trim()
.replace(/\/$/, '');
return {
connectIntent: options.intent,
...(lookupPeerUrl ? { lookupPeerUrl } : {}),
};
}
/** Fallback when the server queued connect traffic but UI still shows a manual relay step. */
export async function ensureDmOutboxReleased(sent: DmSendResponse): Promise<DmSendResponse> {
if (!sent.ok) return sent;
const outboxId = String(sent.outbox_id || '').trim();
if (!outboxId) return sent;
if (!sent.queued && !sent.private_transport_pending) return sent;
try {
await updatePrivateDeliveryAction(outboxId, 'relay');
} catch {
// Backend auto-release may have already approved this outbox item.
}
return sent;
}
+67 -3
View File
@@ -89,6 +89,13 @@ export type DmSendResponse = {
private_transport_pending?: boolean;
};
export type DmConnectIntent =
| 'invite_short_address'
| 'invite_import'
| 'contact_request'
| 'contact_accept'
| 'contact_offer';
export type DmSendRequest = {
apiBase: string;
identity: NodeIdentity;
@@ -102,6 +109,8 @@ export type DmSendRequest = {
useSealedSender?: boolean;
format?: 'mls1' | 'dm1';
sessionWelcome?: string;
connectIntent?: DmConnectIntent;
lookupPeerUrl?: string;
};
const KEY_DM_BUNDLE_FINGERPRINT = 'sb_dm_bundle_fingerprint';
@@ -373,14 +382,54 @@ export async function ensureRegisteredDmKey(
};
}
function prekeyBundleToPublicKey(data: Record<string, unknown>): DmPublicKeyBundle | null {
if (!data?.ok) return null;
const bundle = (data.bundle && typeof data.bundle === 'object' ? data.bundle : data) as Record<
string,
unknown
>;
const dhPubKey = String(
bundle.identity_dh_pub_key || data.identity_dh_pub_key || data.dh_pub_key || '',
).trim();
const agentId = String(data.agent_id || '').trim();
if (!dhPubKey || !agentId) return null;
return {
ok: true,
agent_id: agentId,
lookup_mode: String(data.lookup_mode || 'invite_lookup_handle'),
dh_pub_key: dhPubKey,
dh_algo: String(data.dh_algo || bundle.dh_algo || 'X25519'),
timestamp: Number(data.timestamp || 0) || undefined,
signature: String(data.signature || ''),
public_key: String(data.public_key || ''),
public_key_algo: String(data.public_key_algo || ''),
sequence: Number(data.sequence || 0) || undefined,
prekey_transparency_head: String(data.prekey_transparency_head || ''),
prekey_transparency_size: Number(data.prekey_transparency_size || 0) || undefined,
};
}
async function fetchDmPublicKeyFromPrekeyBundle(
apiBase: string,
lookupToken: string,
): Promise<DmPublicKeyBundle | null> {
const params = new URLSearchParams({ lookup_token: lookupToken });
const res = await fetch(`${apiBase}/api/mesh/dm/prekey-bundle?${params.toString()}`);
const data = (await res.json()) as Record<string, unknown>;
return prekeyBundleToPublicKey(data);
}
export async function fetchDmPublicKey(
apiBase: string,
agentId: string,
lookupToken?: string,
options?: { allowLegacyAgentId?: boolean },
options?: { allowLegacyAgentId?: boolean; lookupPeerUrl?: string },
): Promise<DmPublicKeyBundle | null> {
const normalizedLookupToken = String(lookupToken || '').trim();
const normalizedAgentId = String(agentId || '').trim();
const normalizedLookupPeerUrl = String(options?.lookupPeerUrl || '')
.trim()
.replace(/\/$/, '');
if (!normalizedLookupToken && !options?.allowLegacyAgentId) {
return null;
}
@@ -388,12 +437,25 @@ export async function fetchDmPublicKey(
if (normalizedLookupToken) {
params.set('lookup_token', normalizedLookupToken);
}
if (normalizedLookupPeerUrl) {
params.set('lookup_peer_url', normalizedLookupPeerUrl);
}
if (normalizedAgentId && !normalizedLookupToken && options?.allowLegacyAgentId) {
params.set('agent_id', normalizedAgentId);
}
const res = await fetch(`${apiBase}/api/mesh/dm/pubkey?${params.toString()}`);
const data = await res.json();
return data.ok ? data : null;
const data = (await res.json()) as DmPublicKeyBundle;
if (data.ok && data.dh_pub_key) {
if (!data.agent_id && normalizedLookupToken) {
const fromPrekey = await fetchDmPublicKeyFromPrekeyBundle(apiBase, normalizedLookupToken);
if (fromPrekey) return fromPrekey;
}
return data;
}
if (normalizedLookupToken) {
return fetchDmPublicKeyFromPrekeyBundle(apiBase, normalizedLookupToken);
}
return null;
}
function spreadClaimPositions(totalClaims: number, spreadClaims: number): Set<number> {
@@ -684,6 +746,8 @@ export async function sendDmMessage(request: DmSendRequest): Promise<DmSendRespo
signature: signed.signature,
sequence: signed.sequence,
protocol_version: senderSeal && senderToken ? '' : signed.protocolVersion,
...(request.connectIntent ? { connect_intent: request.connectIntent } : {}),
...(request.lookupPeerUrl ? { lookup_peer_url: request.lookupPeerUrl } : {}),
}),
});
return res.json();
+31
View File
@@ -1350,6 +1350,7 @@ export interface Contact {
invitePinnedDhPubKey?: string;
invitePinnedDhAlgo?: string;
invitePinnedPrekeyLookupHandle?: string;
invitePinnedLookupPeerUrl?: string;
invitePinnedRootFingerprint?: string;
invitePinnedRootManifestFingerprint?: string;
invitePinnedRootWitnessPolicyFingerprint?: string;
@@ -1441,6 +1442,7 @@ function sanitizeContact(contact: Partial<Contact> | undefined): Contact {
invitePinnedDhPubKey: String(contact?.invitePinnedDhPubKey || ''),
invitePinnedDhAlgo: String(contact?.invitePinnedDhAlgo || ''),
invitePinnedPrekeyLookupHandle: String(contact?.invitePinnedPrekeyLookupHandle || ''),
invitePinnedLookupPeerUrl: String(contact?.invitePinnedLookupPeerUrl || ''),
invitePinnedRootFingerprint: String(contact?.invitePinnedRootFingerprint || ''),
invitePinnedRootManifestFingerprint: String(contact?.invitePinnedRootManifestFingerprint || ''),
invitePinnedRootWitnessPolicyFingerprint: String(
@@ -1775,6 +1777,35 @@ export function removeContact(agentId: string): void {
}
}
export async function severContact(
agentId: string,
options: { block?: boolean } = {},
): Promise<void> {
const peerId = String(agentId || '').trim();
if (!peerId) return;
await controlPlaneJson(`/api/wormhole/dm/contact/${encodeURIComponent(peerId)}/sever`, {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ block: Boolean(options.block) }),
});
const contacts = getContacts();
if (!(peerId in contacts)) return;
contacts[peerId] = sanitizeContact({
...contacts[peerId],
sharedAlias: undefined,
previousSharedAliases: [],
pendingSharedAlias: undefined,
sharedAliasGraceUntil: undefined,
sharedAliasRotatedAt: undefined,
...(options.block ? { blocked: true } : {}),
});
saveContacts(contacts);
if (shouldUseWormholeContacts()) {
await persistContactToWormhole(peerId, contacts[peerId]);
}
}
export function isBlocked(agentId: string): boolean {
return getContacts()[agentId]?.blocked || false;
}
@@ -2016,6 +2016,18 @@ export async function deleteWormholeDmContact(
});
}
export async function severWormholeDmContact(
peerId: string,
options: { block?: boolean } = {},
): Promise<{ ok: boolean; peer_id: string; severed?: boolean; blocked?: boolean }> {
return controlPlaneJson(`/api/wormhole/dm/contact/${encodeURIComponent(peerId)}/sever`, {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ block: Boolean(options.block) }),
});
}
export async function getActiveSigningContext(): Promise<ActiveSigningContext | null> {
const secureRequired = await isWormholeSecureRequired();
if (await isWormholeReady()) {
+33 -13
View File
@@ -365,25 +365,45 @@ layers (e.g., "add this CCTV camera I found", "add this military base").
### 8. Wormhole / InfoNet / Mesh Network
OpenClaw can participate as a full two-way agent in the decentralized network:
OpenClaw agents participate in the private Infonet **on behalf of the operator**
who configured the skill. All traffic uses the operator's wormhole persona and
local node runtime (MLS gate crypto, Ed25519 signing, Tor onion transport) —
the agent does not get a separate fleet identity.
**Access tiers**
- `restricted` (default): read Infonet status, list gates, read gate messages,
poll DMs.
- `full` (`OPENCLAW_ACCESS_TIER=full`): also warm Tor, join the swarm, post
gate messages, cast votes, and send DMs when the user commands it.
Remote agents authenticate with HMAC on `/api/ai/channel/command`; loopback
uses the local operator lane.
```python
# Join the Wormhole network (creates Ed25519 identity)
await sb.join_wormhole()
# Warm Tor, enable the node, announce to fleet seed (full tier)
await sb.ensure_infonet_ready(join_swarm=True)
# Post to the InfoNet (signed, chain-verified)
await sb.post_to_infonet("Intelligence bulletin: 3 carriers underway in Med")
# Status snapshot (chain health, wormhole, runtime)
status = await sb.infonet_status()
# Read InfoNet messages
messages = await sb.read_infonet(limit=20)
# Read the public Infonet gate (MLS-encrypted, decrypt with operator keys)
messages = await sb.read_gate_messages("infonet", limit=20, decrypt=True)
# Post on behalf of the operator (full tier) — propagates via peer-push
await sb.post_to_gate("infonet", "Intelligence bulletin: 3 carriers underway in Med")
# Legacy alias:
await sb.post_to_infonet("same as post_to_gate on infonet gate")
# Upvote / downvote a node (full tier)
await sb.cast_vote("!sb_peer_id_or_pubkey", vote=1, gate="infonet")
# Encrypted DMs (peer_id / !sb_... recipient)
await sb.send_encrypted_dm("!sb_recipient", "Eyes only: carrier update")
dms = await sb.read_encrypted_dms(limit=20)
# Join encrypted gate channels
gates = await sb.list_gates()
await sb.post_to_gate("gate_id", "Classified intel for gate members")
# Send/receive encrypted DMs
await sb.send_encrypted_dm("recipient_pubkey", "Eyes only: carrier update")
dms = await sb.read_encrypted_dms()
await sb.join_infonet_swarm() # re-announce + refresh manifest
# Meshtastic radio
signals = await sb.listen_mesh(region="US", limit=20)
+94 -57
View File
@@ -583,56 +583,81 @@ class ShadowBrokerClient:
r = await self._delete("/api/ai/inject", params=params)
return r.json()
# ── Wormhole / InfoNet ────────────────────────────────────────────
# ── Wormhole / InfoNet (operator-delegated via command channel) ───
async def ensure_infonet_ready(self, *, join_swarm: bool = True) -> dict:
"""Warm Tor, enable the node, and join the private Infonet swarm."""
resp = await self.send_command(
"ensure_infonet_ready",
{"join_swarm": join_swarm},
)
return resp.get("result") if isinstance(resp.get("result"), dict) else resp
async def join_infonet_swarm(self) -> dict:
"""Announce to the fleet seed and pull the signed peer manifest."""
resp = await self.send_command("join_infonet_swarm", {})
return resp.get("result") if isinstance(resp.get("result"), dict) else resp
async def infonet_status(self) -> dict:
"""Participant node + hashchain status snapshot."""
return self.unwrap_channel_result(await self.send_command("infonet_status", {}))
async def list_gates(self) -> dict:
"""List encrypted gate channels."""
return self.unwrap_channel_result(await self.send_command("list_gates", {}))
async def read_gate_messages(
self,
gate_id: str,
*,
limit: int = 20,
decrypt: bool = False,
) -> dict:
"""Read gate messages (optionally decrypt with the operator MLS persona)."""
return self.unwrap_channel_result(
await self.send_command(
"read_gate_messages",
{"gate_id": gate_id, "limit": limit, "decrypt": decrypt},
)
)
async def post_to_gate(self, gate_id: str, message: str, *, reply_to: str = "") -> dict:
"""Post an MLS-encrypted gate message on behalf of the operator."""
resp = await self.send_command(
"post_gate_message",
{
"gate_id": gate_id,
"plaintext": message,
"reply_to": reply_to,
},
)
return resp.get("result") if isinstance(resp.get("result"), dict) else resp
async def cast_vote(
self,
target_id: str,
vote: int,
*,
gate: str = "",
) -> dict:
"""Upvote (+1) or downvote (-1) a node; optional gate scope."""
resp = await self.send_command(
"cast_vote",
{"target_id": target_id, "vote": vote, "gate": gate},
)
return resp.get("result") if isinstance(resp.get("result"), dict) else resp
# Legacy aliases — prefer command-channel methods above
async def join_wormhole(self) -> dict:
"""Create a Wormhole identity and join the network."""
r = await self._post("/api/wormhole/join")
return r.json()
async def sign_event(self, event_type: str, payload: dict) -> dict:
"""Sign an event with the Wormhole Ed25519 key."""
r = await self._post("/api/wormhole/sign", json={
"event_type": event_type,
"payload": payload,
})
r.raise_for_status()
return r.json()
return await self.ensure_infonet_ready(join_swarm=True)
async def post_to_infonet(self, message: str, event_type: str = "message") -> dict:
"""Post a signed event to the InfoNet ledger."""
signed = await self.sign_event(event_type, {"message": message})
r = await self._post("/api/mesh/infonet/ingest", json={
"events": [signed],
})
r.raise_for_status()
return r.json()
if event_type != "message":
raise RuntimeError("use post_to_gate for encrypted gate traffic")
return await self.post_to_gate("infonet", message)
async def read_infonet(self, limit: int = 20, gate: str = "") -> dict:
"""Read recent InfoNet messages."""
params = {"limit": limit}
if gate:
params["gate"] = gate
r = await self._get("/api/mesh/infonet/messages", params=params)
return r.json()
async def list_gates(self) -> list:
"""List available encrypted gate channels."""
r = await self._get("/api/mesh/gate/list")
return r.json()
async def post_to_gate(self, gate_id: str, message: str) -> dict:
"""Compose and post an MLS-encrypted message to a gate."""
compose = await self._post("/api/wormhole/gate/message/compose", json={
"gate_id": gate_id,
"plaintext": message,
})
compose.raise_for_status()
envelope = compose.json()
post = await self._post(f"/api/mesh/gate/{gate_id}/message", json=envelope)
post.raise_for_status()
return post.json()
async def read_infonet(self, limit: int = 20, gate: str = "infonet") -> dict:
return await self.read_gate_messages(gate or "infonet", limit=limit, decrypt=True)
# ── Meshtastic ────────────────────────────────────────────────────
@@ -830,19 +855,31 @@ class ShadowBrokerClient:
# ── Encrypted DMs ─────────────────────────────────────────────
async def send_encrypted_dm(self, recipient_pubkey: str, message: str) -> dict:
"""Send an E2E encrypted direct message to another Wormhole identity."""
r = await self._post("/api/wormhole/dm/send", json={
"recipient": recipient_pubkey,
"plaintext": message,
})
r.raise_for_status()
return r.json()
async def send_encrypted_dm(
self,
peer_id: str,
message: str,
*,
delivery_class: str = "shared",
recipient_token: str = "",
) -> dict:
"""Send an E2E encrypted DM to another node (peer_id / !sb_...)."""
resp = await self.send_command(
"send_dm",
{
"peer_id": peer_id,
"plaintext": message,
"delivery_class": delivery_class,
"recipient_token": recipient_token,
},
)
return resp.get("result") if isinstance(resp.get("result"), dict) else resp
async def read_encrypted_dms(self, limit: int = 20) -> list:
"""Read received encrypted direct messages."""
r = await self._get("/api/wormhole/dm/inbox", params={"limit": limit})
return r.json()
async def read_encrypted_dms(self, limit: int = 20) -> dict:
"""Poll encrypted DMs for the operator identity."""
return self.unwrap_channel_result(
await self.send_command("poll_dms", {"limit": limit})
)
# ── Dead Drop ─────────────────────────────────────────────────
+8
View File
@@ -31,9 +31,17 @@ agent_surface:
- sb_query.ShadowBrokerClient.run_playbook
- sb_query.ShadowBrokerClient.send_batch
- sb_query.ShadowBrokerClient.channel_status
- sb_query.ShadowBrokerClient.infonet_status
- sb_query.ShadowBrokerClient.list_gates
- sb_query.ShadowBrokerClient.read_gate_messages
- sb_query.ShadowBrokerClient.poll_dms
writes:
- sb_query.ShadowBrokerClient.place_pin
- sb_query.ShadowBrokerClient.place_pins_batch
- sb_query.ShadowBrokerClient.ensure_infonet_ready
- sb_query.ShadowBrokerClient.post_to_gate
- sb_query.ShadowBrokerClient.cast_vote
- sb_query.ShadowBrokerClient.send_encrypted_dm
blocked_without_confirm:
- search_telemetry
- get_telemetry
+5
View File
@@ -0,0 +1,5 @@
import sys
import main
peer = sys.argv[1] if len(sys.argv) > 1 else ""
print(main.compose_wormhole_dm(peer_id=peer, peer_dh_pub="", plaintext="fleet-dm-probe"))
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Print DM readiness: identities, contacts, peers, transport."""
from __future__ import annotations
import json
def main() -> None:
out: dict = {"ok": True}
try:
from services.wormhole_supervisor import get_wormhole_state, get_transport_tier
out["wormhole"] = get_wormhole_state()
out["transport_tier"] = get_transport_tier()
except Exception as exc:
out["wormhole_error"] = str(exc)
try:
from services.mesh.mesh_wormhole_persona import get_dm_identity
out["dm_identity"] = get_dm_identity()
except Exception as exc:
out["dm_identity_error"] = str(exc)
try:
from services.mesh.mesh_wormhole_contacts import list_wormhole_dm_contacts
contacts = list_wormhole_dm_contacts()
out["dm_contacts"] = {
k: {
"trustLevel": v.get("trustLevel"),
"dmIdentityId": v.get("dmIdentityId"),
"invitePinnedPrekeyLookupHandle": bool(v.get("invitePinnedPrekeyLookupHandle")),
"verifiedFirstContact": v.get("verifiedFirstContact"),
"remotePrekeyLookupMode": v.get("remotePrekeyLookupMode"),
}
for k, v in (contacts or {}).items()
}
out["dm_contact_count"] = len(contacts or {})
except Exception as exc:
out["dm_contacts_error"] = str(exc)
try:
import main as main_mod
out["local_onion"] = main_mod._local_infonet_peer_url()
out["node_enabled"] = main_mod._participant_node_enabled()
out["arti_ready"] = main_mod._check_arti_ready()
out["push_peers"] = main_mod._filter_infonet_peer_urls(
__import__(
"services.mesh.mesh_router", fromlist=["authenticated_push_peer_urls"]
).authenticated_push_peer_urls()
)
except Exception as exc:
out["peer_runtime_error"] = str(exc)
try:
from services.mesh.mesh_private_outbox import private_delivery_outbox
pending = private_delivery_outbox.list_items(exposure="ordinary")
dm_pending = [i for i in pending if str(i.get("lane", "")) == "dm"]
out["dm_outbox_pending"] = len(dm_pending)
out["dm_outbox_samples"] = [
{
"id": i.get("id"),
"release_state": i.get("release_state"),
"status": (i.get("status") or {}).get("code"),
"recipient_id": (i.get("payload") or {}).get("recipient_id"),
}
for i in dm_pending[:5]
]
except Exception as exc:
out["outbox_error"] = str(exc)
try:
from services.mesh.mesh_dm_relay import dm_relay
out["dm_relay_stats"] = dict(dm_relay._stats)
out["dm_mailbox_keys"] = len(dm_relay._mailboxes)
except Exception as exc:
out["relay_error"] = str(exc)
print(json.dumps(out, indent=2, default=str))
if __name__ == "__main__":
main()
+751
View File
@@ -0,0 +1,751 @@
#!/usr/bin/env python3
"""Live E2E: short-address lookup -> contact request -> Pete mailbox."""
from __future__ import annotations
import json
import os
import subprocess
import sys
import textwrap
import time
import urllib.error
import urllib.parse
import urllib.request
API = os.environ.get("SHADOWBROKER_API", "http://127.0.0.1:8000")
MARKER = os.environ.get("E2E_DM_MARKER", f"dm-short-addr-e2e-{int(time.time())}")
REPLY_MARKER = os.environ.get("E2E_DM_REPLY_MARKER", f"{MARKER}-reply")
PETE_HANDLE = os.environ.get("PETE_DM_SHORT_HANDLE", "").strip()
PETE_LOOKUP_PEER_URL = os.environ.get("PETE_DM_LOOKUP_PEER_URL", "").strip()
FRESH_BACKEND = os.environ.get("E2E_DM_FRESH_BACKEND", "1").strip().lower() not in {
"0",
"false",
"no",
}
SSH_PETE = os.environ.get("PETE_SSH", "pete")
PETE_ONION = os.environ.get(
"PETE_ONION",
"nwbs4ur2usffb7lk3vyffhaqrijry544vmfjkk3qbrhvoh4v26fwxlid.onion:8000",
).strip()
def _docker_json(method: str, path: str, body: dict | None = None, *, admin_key: str = "", timeout_s: int = 30) -> dict:
payload = ["docker", "exec", "shadowbroker-backend", "curl", "-s", "--max-time", str(timeout_s)]
if admin_key:
payload.extend(["-H", f"X-Admin-Key: {admin_key}"])
if body is not None:
payload.extend(["-H", "Content-Type: application/json", "-X", method.upper(), "-d", json.dumps(body)])
else:
payload.extend(["-X", method.upper()])
payload.append(f"http://127.0.0.1:8000{path}")
proc = subprocess.run(payload, capture_output=True, text=True, timeout=timeout_s + 15, check=False)
raw = (proc.stdout or "").strip()
if not raw:
raise RuntimeError(proc.stderr.strip() or f"{method} {path} produced no response")
parsed = json.loads(raw)
if isinstance(parsed, dict) and parsed.get("detail") == "private_delivery_item_not_found" and method.upper() == "POST":
return parsed
return parsed if isinstance(parsed, dict) else {"ok": False, "detail": "invalid json response"}
def _json(method: str, path: str, body: dict | None = None, *, admin_key: str = "") -> dict:
data = None
headers = {"Content-Type": "application/json"}
if admin_key:
headers["X-Admin-Key"] = admin_key
if body is not None:
data = json.dumps(body, separators=(",", ":"), sort_keys=True).encode("utf-8")
req = urllib.request.Request(f"{API}{path}", data=data, headers=headers, method=method.upper())
try:
with urllib.request.urlopen(req, timeout=300) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"{method} {path} -> {exc.code}: {detail}") from exc
def _docker_admin_key() -> str:
proc = subprocess.run(
["docker", "exec", "shadowbroker-backend", "printenv", "ADMIN_KEY"],
capture_output=True,
text=True,
check=True,
)
return proc.stdout.strip()
def _ssh_pete_admin_key() -> str:
proc = subprocess.run(
["ssh", "-o", "BatchMode=yes", SSH_PETE, "docker exec shadowbroker-backend printenv ADMIN_KEY"],
capture_output=True,
text=True,
check=True,
)
return proc.stdout.strip()
def _ensure_pete_invite(pete_admin: str) -> tuple[str, str]:
if PETE_HANDLE:
lookup = PETE_LOOKUP_PEER_URL or (
f"http://{PETE_ONION}" if PETE_ONION else ""
)
return PETE_HANDLE, lookup.rstrip("/")
proc = subprocess.run(
[
"ssh",
"-o",
"BatchMode=yes",
SSH_PETE,
f"curl -s -H 'X-Admin-Key: {pete_admin}' 'http://127.0.0.1:8000/api/wormhole/dm/invite?label=e2e-live'",
],
capture_output=True,
text=True,
check=True,
)
invite = json.loads(proc.stdout)
payload = dict(invite.get("invite", {}).get("payload", {}) or {})
handle = str(payload.get("prekey_lookup_handle", "") or "").strip()
lookup_peer_url = str(payload.get("lookup_peer_url", "") or "").strip().rstrip("/")
if not handle:
raise RuntimeError(f"could not mint Pete short handle: {invite}")
return handle, lookup_peer_url
def _docker_python(code: str) -> dict:
proc = subprocess.run(
["docker", "exec", "shadowbroker-backend", "python", "-c", code],
capture_output=True,
text=True,
timeout=600,
check=False,
)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or "docker python failed")
line = proc.stdout.strip().splitlines()[-1]
return json.loads(line)
def _restart_local_backend() -> None:
"""Clear in-memory DM relay state (MESH_DM_PERSIST_SPOOL=false) before a repeat run."""
proc = subprocess.run(
[
"docker",
"compose",
"-f",
"docker-compose.yml",
"-f",
"docker-compose.build.yml",
"restart",
"backend",
],
capture_output=True,
text=True,
timeout=180,
check=False,
)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or "backend restart failed")
deadline = time.time() + 120
while time.time() < deadline:
probe = subprocess.run(
[
"docker",
"exec",
"shadowbroker-backend",
"curl",
"-sf",
"--max-time",
"5",
"http://127.0.0.1:8000/api/health",
],
capture_output=True,
text=True,
check=False,
)
if probe.returncode == 0:
print("local backend restarted and healthy")
return
time.sleep(3)
raise RuntimeError("backend did not become healthy after restart")
def _wait_hidden_transport_ready(*, timeout_s: int = 120) -> dict:
code = (
"import json, time\n"
"from services.mesh.mesh_private_dispatcher import _anonymous_dm_hidden_transport_enforced\n"
f"deadline = time.time() + {int(timeout_s)}\n"
"while time.time() < deadline:\n"
" if _anonymous_dm_hidden_transport_enforced():\n"
" print(json.dumps({'ok': True}))\n"
" break\n"
" time.sleep(2)\n"
"else:\n"
" print(json.dumps({'ok': False, 'detail': 'hidden transport not ready'}))\n"
)
return _docker_python(code)
def _release_dm_outbox(admin_key: str, outbox_id: str, *, timeout_s: int = 180) -> dict:
outbox_id = str(outbox_id or "").strip()
if not outbox_id:
return {"ok": False, "detail": "missing outbox_id"}
_docker_json(
"POST",
f"/api/wormhole/private-delivery/{outbox_id}/action",
{"action": "relay"},
admin_key=admin_key,
)
deadline = time.time() + timeout_s
while time.time() < deadline:
status = _docker_json("GET", "/api/wormhole/status", admin_key=admin_key)
items = list((status.get("private_delivery") or {}).get("items") or [])
item = next((entry for entry in items if str(entry.get("id", "")) == outbox_id), None)
if item and str(item.get("release_state", "")) == "delivered":
return {"ok": True, "item": item}
time.sleep(3)
return {"ok": False, "detail": "private release did not complete in time", "outbox_id": outbox_id}
def _drain_pete_request_mailbox() -> None:
drain_code = textwrap.dedent(
"""
import json, secrets, time, urllib.request
from services.mesh.mesh_protocol import PROTOCOL_VERSION
from services.mesh.mesh_wormhole_persona import get_dm_identity, sign_dm_wormhole_event
def _poll_once():
identity = get_dm_identity()
agent_id = str(identity.get("node_id") or "")
claims = [{"type": "requests", "token": "e2e-drain"}]
signed = sign_dm_wormhole_event(
event_type="dm_poll",
payload={"mailbox_claims": claims, "agent_id": agent_id},
)
body = {
"agent_id": agent_id,
"mailbox_claims": 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),
}
data = json.dumps(body, separators=(",", ":"), sort_keys=True).encode("utf-8")
req = urllib.request.Request(
"http://127.0.0.1:8000/api/mesh/dm/poll",
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read().decode("utf-8"))
drained = 0
for _ in range(8):
payload = _poll_once()
count = int(payload.get("count", 0) or 0)
drained += count
if count <= 0 and not payload.get("has_more"):
break
time.sleep(1)
print(json.dumps({"ok": True, "drained": drained}))
"""
).strip()
result = _ssh_pete_python(drain_code)
print(f"Pete request mailbox drain: {result.get('drained', 0)} message(s)")
def _warmup_tor() -> None:
"""Prime local Arti SOCKS before fleet lookups (cold Tor can exceed lookup budgets)."""
if not PETE_ONION:
return
proc = subprocess.run(
[
"docker",
"exec",
"shadowbroker-backend",
"curl",
"-s",
"-o",
"/dev/null",
"-w",
"%{http_code}",
"--max-time",
"120",
"--socks5-hostname",
"127.0.0.1:9050",
f"http://{PETE_ONION}/api/health",
],
capture_output=True,
text=True,
timeout=180,
check=False,
)
code = (proc.stdout or "").strip()
print(f"Tor warmup Pete health: {code or proc.stderr.strip() or 'failed'}")
def _ssh_pete_python(code: str) -> dict:
# Pipe script stdin to Pete's running backend container — avoids Windows
# docker-exec base64 bugs and SSH command-line length limits on long polls.
proc = subprocess.run(
[
"ssh",
"-o",
"BatchMode=yes",
SSH_PETE,
"docker exec -i shadowbroker-backend python",
],
input=code.encode("utf-8"),
capture_output=True,
timeout=300,
check=False,
)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or "pete python failed")
lines = [line for line in proc.stdout.strip().splitlines() if line.strip()]
if not lines:
raise RuntimeError(proc.stderr.strip() or "pete python produced no output")
return json.loads(lines[-1])
def main() -> int:
if FRESH_BACKEND:
print("== prep: restart local backend for clean in-memory DM relay ==")
_restart_local_backend()
print("== prep: drain stale Pete request mailbox ==")
_drain_pete_request_mailbox()
print("== warmup: prime Tor to Pete ==")
_warmup_tor()
print("== warmup: wait for anonymous hidden transport ==")
hidden = _wait_hidden_transport_ready()
print(json.dumps(hidden, indent=2))
if not hidden.get("ok"):
raise RuntimeError(f"hidden transport not ready: {hidden}")
local_admin = _docker_admin_key()
pete_admin = _ssh_pete_admin_key()
handle, lookup_peer_url = _ensure_pete_invite(pete_admin)
print(f"Pete short handle: {handle}")
if lookup_peer_url:
print(f"Pete lookup peer: {lookup_peer_url}")
print("== step 1: fleet pubkey lookup from local ==")
lookup_path = f"/api/mesh/dm/pubkey?lookup_token={handle}"
if lookup_peer_url:
lookup_path += f"&lookup_peer_url={urllib.parse.quote(lookup_peer_url, safe='')}"
lookup = _json("GET", lookup_path)
if not lookup.get("ok") or not lookup.get("agent_id") or not lookup.get("dh_pub_key"):
print(json.dumps(lookup, indent=2))
raise RuntimeError("pubkey fleet lookup failed")
pete_id = str(lookup["agent_id"])
pete_dh = str(lookup.get("dh_pub_key") or "")
print(f"resolved Pete agent_id: {pete_id}")
print("== step 2: send contact request from local ==")
send_code = (
"import json\n"
"from services.openclaw_infonet import send_contact_request\n"
f"result = send_contact_request(lookup_token={json.dumps(handle)}, note={json.dumps(MARKER)}, lookup_peer_url={json.dumps(lookup_peer_url)})\n"
"print(json.dumps({"
"'ok': bool(result.get('ok')), "
"'send': result, "
"'msg_id': result.get('msg_id',''), "
"'sender_id': result.get('sender_id',''), "
"'recipient_id': result.get('recipient_id','')"
"}))\n"
)
send_result = _docker_python(send_code)
print(json.dumps(send_result, indent=2))
if not send_result.get("ok"):
raise RuntimeError(f"local send failed: {send_result}")
msg_id = str(send_result.get("msg_id", "") or "")
print("== step 2b: approve relay release and wait for fleet push ==")
outbox_id = str((send_result.get("send") or {}).get("outbox_id", "") or "")
release = _release_dm_outbox(local_admin, outbox_id)
print(json.dumps(release, indent=2))
if not release.get("ok"):
raise RuntimeError(f"private release failed: {release}")
print("== step 3: wait for fleet replication, poll Pete relay ==")
# Hit the running uvicorn process via localhost HTTP — dm_relay is in-memory
# and is not visible to one-off `docker exec python` shells.
poll_code = textwrap.dedent(
f"""
import json, secrets, time, urllib.error, urllib.request
from services.mesh.mesh_protocol import PROTOCOL_VERSION
from services.mesh.mesh_wormhole_persona import get_dm_identity, sign_dm_wormhole_event
msg_id = {json.dumps(msg_id)}
marker = {json.dumps(MARKER)}
sender_id = {json.dumps(send_result.get('sender_id', ''))}
def _mailbox_claims():
return [{{"type": "requests", "token": "e2e-poll"}}]
def _poll_once():
identity = get_dm_identity()
agent_id = str(identity.get("node_id") or "")
claims = _mailbox_claims()
signed = sign_dm_wormhole_event(
event_type="dm_poll",
payload={{"mailbox_claims": claims, "agent_id": agent_id}},
)
body = {{
"agent_id": agent_id,
"mailbox_claims": 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),
}}
data = json.dumps(body, separators=(",", ":"), sort_keys=True).encode("utf-8")
req = urllib.request.Request(
"http://127.0.0.1:8000/api/mesh/dm/poll",
data=data,
headers={{"Content-Type": "application/json"}},
method="POST",
)
with urllib.request.urlopen(req, timeout=120) as resp:
return json.loads(resp.read().decode("utf-8"))
hit = None
for attempt in range(30):
time.sleep(4)
try:
payload = _poll_once()
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
if exc.code == 202:
continue
print(json.dumps({{"ok": False, "detail": f"poll http {{exc.code}}: {{detail}}"}}))
break
except Exception as exc:
print(json.dumps({{"ok": False, "detail": str(exc) or type(exc).__name__}}))
break
for message in list(payload.get("messages") or []):
if str(message.get("msg_id", "")) == msg_id:
hit = message
break
if marker in str(message.get("ciphertext", "")):
hit = message
break
if hit:
print(json.dumps({{"ok": True, "attempt": attempt, "msg_id": msg_id}}))
break
else:
print(json.dumps({{"ok": False, "detail": "request not in Pete relay mailboxes"}}))
"""
).strip()
poll = _ssh_pete_python(poll_code)
print(json.dumps(poll, indent=2))
if not poll.get("ok"):
raise RuntimeError(f"Pete did not receive request: {poll}")
print("== step 4: Pete bootstrap-decrypt contact offer ==")
decrypt_code = textwrap.dedent(
f"""
import json, secrets, time, urllib.error, urllib.request
from services.mesh.mesh_protocol import PROTOCOL_VERSION
from services.mesh.mesh_wormhole_persona import get_dm_identity, sign_dm_wormhole_event
from services.mesh.mesh_wormhole_prekey import bootstrap_decrypt_from_sender
sender_id = {json.dumps(send_result.get('sender_id', ''))}
msg_id = {json.dumps(msg_id)}
identity = get_dm_identity()
agent_id = str(identity.get("node_id") or "")
claims = [{{"type": "requests", "token": "e2e-poll"}}]
signed = sign_dm_wormhole_event(
event_type="dm_poll",
payload={{"mailbox_claims": claims, "agent_id": agent_id}},
)
body = {{
"agent_id": agent_id,
"mailbox_claims": 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),
}}
data = json.dumps(body, separators=(",", ":"), sort_keys=True).encode("utf-8")
req = urllib.request.Request(
"http://127.0.0.1:8000/api/mesh/dm/poll",
data=data,
headers={{"Content-Type": "application/json"}},
method="POST",
)
ciphertext = ""
try:
with urllib.request.urlopen(req, timeout=120) as resp:
payload = json.loads(resp.read().decode("utf-8"))
for message in list(payload.get("messages") or []):
if str(message.get("msg_id", "")) == msg_id:
ciphertext = str(message.get("ciphertext", "") or "")
break
except Exception as exc:
print(json.dumps({{"ok": False, "detail": str(exc) or type(exc).__name__}}))
elif not ciphertext:
print(json.dumps({{"ok": False, "detail": "ciphertext missing on Pete"}}))
else:
dec = bootstrap_decrypt_from_sender(sender_id, ciphertext)
print(json.dumps({{"ok": bool(dec.get("ok")), "plaintext": dec.get("result", ""), "detail": dec.get("detail", "")}}))
"""
).strip()
decrypted = _ssh_pete_python(decrypt_code)
print(json.dumps(decrypted, indent=2))
if not decrypted.get("ok") or MARKER not in str(decrypted.get("plaintext", "")):
raise RuntimeError(f"Pete could not decrypt contact offer: {decrypted}")
local_sender_id = str(send_result.get("sender_id", "") or "")
if not local_sender_id:
raise RuntimeError("local sender_id missing from send result")
print("== step 5: Pete accepts contact request ==")
accept_code = textwrap.dedent(
f"""
import json, os
os.environ.setdefault("SB_API_BASE", "http://127.0.0.1:8000")
from services.openclaw_infonet import send_contact_accept
result = send_contact_accept(peer_id={json.dumps(local_sender_id)})
print(json.dumps({{
"ok": bool(result.get("ok")),
"msg_id": result.get("msg_id", ""),
"shared_alias": result.get("shared_alias", ""),
"detail": result.get("detail", ""),
}}))
"""
).strip()
accept_result = _ssh_pete_python(accept_code)
print(json.dumps(accept_result, indent=2))
if not accept_result.get("ok"):
raise RuntimeError(f"Pete accept failed: {accept_result}")
accept_msg_id = str(accept_result.get("msg_id", "") or "")
print("== step 5b: release Pete accept to fleet relay ==")
print(json.dumps(_ssh_pete_python(release_code), indent=2))
print("== step 6: local polls and decrypts contact accept ==")
local_accept_code = textwrap.dedent(
f"""
import json, secrets, time, urllib.error, urllib.request
from services.mesh.mesh_protocol import PROTOCOL_VERSION
from services.mesh.mesh_wormhole_dead_drop import parse_contact_consent
from services.mesh.mesh_wormhole_persona import get_dm_identity, sign_dm_wormhole_event
from services.mesh.mesh_wormhole_prekey import bootstrap_decrypt_from_sender
sender_id = {json.dumps(local_sender_id)}
accept_msg_id = {json.dumps(accept_msg_id)}
pete_id = {json.dumps(pete_id)}
identity = get_dm_identity()
agent_id = str(identity.get("node_id") or "")
claims = [{{"type": "requests", "token": "e2e-local-poll"}}]
signed = sign_dm_wormhole_event(
event_type="dm_poll",
payload={{"mailbox_claims": claims, "agent_id": agent_id}},
)
body = {{
"agent_id": agent_id,
"mailbox_claims": 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),
}}
data = json.dumps(body, separators=(",", ":"), sort_keys=True).encode("utf-8")
hit = None
for attempt in range(30):
time.sleep(4)
req = urllib.request.Request(
"http://127.0.0.1:8000/api/mesh/dm/poll",
data=data,
headers={{"Content-Type": "application/json"}},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=120) as resp:
payload = json.loads(resp.read().decode("utf-8"))
except Exception as exc:
print(json.dumps({{"ok": False, "detail": str(exc) or type(exc).__name__}}))
break
for message in list(payload.get("messages") or []):
if str(message.get("msg_id", "")) == accept_msg_id:
hit = message
break
if str(message.get("sender_id", "")) == pete_id:
hit = message
break
if hit:
break
if not hit:
print(json.dumps({{"ok": False, "detail": "accept not in local requests mailbox"}}))
else:
ciphertext = str(hit.get("ciphertext", "") or "")
dec = bootstrap_decrypt_from_sender(pete_id, ciphertext)
consent = parse_contact_consent(str(dec.get("result", "") or ""))
print(json.dumps({{
"ok": bool(dec.get("ok") and consent and consent.get("kind") == "contact_accept"),
"shared_alias": str((consent or {{}}).get("shared_alias", "") or ""),
"detail": dec.get("detail", ""),
}}))
"""
).strip()
local_accept = _docker_python(local_accept_code)
print(json.dumps(local_accept, indent=2))
if not local_accept.get("ok") or not local_accept.get("shared_alias"):
raise RuntimeError(f"local could not decrypt contact accept: {local_accept}")
print("== step 7: local sends shared DM reply ==")
shared_send_code = textwrap.dedent(
f"""
import json, os
os.environ.setdefault("SB_API_BASE", "http://127.0.0.1:8000")
from services.mesh.mesh_wormhole_dead_drop import derive_dead_drop_token_pair
from services.openclaw_infonet import send_dm
token_pair = derive_dead_drop_token_pair(
peer_id={json.dumps(pete_id)},
peer_dh_pub={json.dumps(pete_dh)},
)
if not token_pair.get("ok"):
print(json.dumps(token_pair))
else:
result = send_dm(
{json.dumps(pete_id)},
{json.dumps(REPLY_MARKER)},
delivery_class="shared",
recipient_token=str(token_pair.get("current") or ""),
)
print(json.dumps({{
"ok": bool(result.get("ok")),
"msg_id": result.get("msg_id", ""),
"detail": result.get("detail", ""),
}}))
"""
).strip()
shared_send = _docker_python(shared_send_code)
print(json.dumps(shared_send, indent=2))
if not shared_send.get("ok"):
raise RuntimeError(f"local shared DM send failed: {shared_send}")
shared_msg_id = str(shared_send.get("msg_id", "") or "")
print("== step 7b: release local shared DM to fleet relay ==")
print(json.dumps(_docker_python(release_code), indent=2))
print("== step 8: Pete polls shared mailbox and decrypts reply ==")
shared_poll_code = textwrap.dedent(
f"""
import json, secrets, time, urllib.error, urllib.request
from services.mesh.mesh_protocol import PROTOCOL_VERSION
from services.mesh.mesh_wormhole_dead_drop import derive_dead_drop_token_pair
from services.mesh.mesh_wormhole_persona import get_dm_identity, sign_dm_wormhole_event
sender_id = {json.dumps(local_sender_id)}
shared_msg_id = {json.dumps(shared_msg_id)}
marker = {json.dumps(REPLY_MARKER)}
bundle = __import__(
"services.mesh.mesh_wormhole_prekey",
fromlist=["fetch_dm_prekey_bundle"],
).fetch_dm_prekey_bundle(agent_id=sender_id)
sender_dh = str(bundle.get("dh_pub_key") or "")
token_pair = derive_dead_drop_token_pair(peer_id=sender_id, peer_dh_pub=sender_dh)
if not token_pair.get("ok"):
print(json.dumps(token_pair))
raise SystemExit(0)
tokens = [str(token_pair.get("current") or "")]
prev = str(token_pair.get("previous") or "")
if prev and prev not in tokens:
tokens.append(prev)
identity = get_dm_identity()
agent_id = str(identity.get("node_id") or "")
claims = [{{"type": "shared", "token": token}} for token in tokens if token]
hit = None
for attempt in range(30):
time.sleep(4)
signed = sign_dm_wormhole_event(
event_type="dm_poll",
payload={{"mailbox_claims": claims, "agent_id": agent_id}},
)
body = {{
"agent_id": agent_id,
"mailbox_claims": 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),
}}
data = json.dumps(body, separators=(",", ":"), sort_keys=True).encode("utf-8")
req = urllib.request.Request(
"http://127.0.0.1:8000/api/mesh/dm/poll",
data=data,
headers={{"Content-Type": "application/json"}},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=120) as resp:
payload = json.loads(resp.read().decode("utf-8"))
except Exception as exc:
print(json.dumps({{"ok": False, "detail": str(exc) or type(exc).__name__}}))
break
for message in list(payload.get("messages") or []):
if str(message.get("msg_id", "")) == shared_msg_id:
hit = message
break
if hit:
break
if not hit:
print(json.dumps({{"ok": False, "detail": "shared reply not in Pete mailbox"}}))
else:
ciphertext = str(hit.get("ciphertext", "") or "")
dec = __import__("main", fromlist=["decrypt_wormhole_dm_envelope"]).decrypt_wormhole_dm_envelope(
peer_id=sender_id,
ciphertext=ciphertext,
payload_format=str(hit.get("format", "") or "mls1"),
session_welcome=str(hit.get("session_welcome", "") or ""),
)
plaintext = str(dec.get("plaintext", "") or "")
print(json.dumps({{
"ok": bool(dec.get("ok") and marker in plaintext),
"plaintext": plaintext,
"detail": dec.get("detail", ""),
}}))
"""
).strip()
shared_decrypt = _ssh_pete_python(shared_poll_code)
print(json.dumps(shared_decrypt, indent=2))
if not shared_decrypt.get("ok") or REPLY_MARKER not in str(shared_decrypt.get("plaintext", "")):
raise RuntimeError(f"Pete could not decrypt shared DM: {shared_decrypt}")
print("== E2E PASS: invite -> accept -> private shared DM ==")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as exc:
print(f"E2E FAIL: {exc}", file=sys.stderr)
raise
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""Live E2E: OpenClaw HMAC agent posts to Infonet gate and verifies propagation."""
from __future__ import annotations
import asyncio
import json
import os
import subprocess
import sys
import time
import urllib.error
import urllib.request
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
SKILL_DIR = os.path.join(ROOT, "openclaw-skills", "shadowbroker")
API = os.environ.get("SHADOWBROKER_API", "http://127.0.0.1:8000")
MARKER = os.environ.get("E2E_MARKER", f"OPENCLAW-AGENT-E2E-{int(time.time())}")
def _json_request(method: str, path: str, body: dict | None = None, *, inside_docker: bool = False) -> dict:
data = None
headers = {"Content-Type": "application/json"}
if body is not None:
data = json.dumps(body, separators=(",", ":"), sort_keys=True).encode("utf-8")
req = urllib.request.Request(f"{API}{path}", data=data, headers=headers, method=method.upper())
try:
with urllib.request.urlopen(req, timeout=180) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"{method} {path} -> {exc.code}: {detail}") from exc
def docker_python(code: str) -> str:
proc = subprocess.run(
["docker", "exec", "shadowbroker-backend", "python", "-c", code],
capture_output=True,
text=True,
timeout=300,
check=False,
)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or "docker exec failed")
return proc.stdout.strip()
def bootstrap_hmac_and_full_tier() -> str:
setup = r"""
import json, urllib.request
BASE = 'http://127.0.0.1:8000'
def call(method, path, body=None):
data = json.dumps(body or {}, separators=(',', ':'), sort_keys=True).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, headers={'Content-Type': 'application/json'}, method=method)
with urllib.request.urlopen(req, timeout=60) as r:
return json.loads(r.read().decode())
call('POST', '/api/ai/connect-info/bootstrap', {})
call('PUT', '/api/ai/connect-info/access-tier', {'tier': 'full'})
secret = call('POST', '/api/ai/connect-info/reveal', {})['hmac_secret']
print(secret)
"""
secret = docker_python(setup)
if not secret or len(secret) < 16:
raise RuntimeError(f"unexpected HMAC secret: {secret!r}")
return secret
async def agent_post(secret: str, message: str) -> dict:
sys.path.insert(0, SKILL_DIR)
from sb_query import ShadowBrokerClient
os.environ["SHADOWBROKER_HMAC_SECRET"] = secret
client = ShadowBrokerClient(base_url=API)
try:
ready = await client.ensure_infonet_ready(join_swarm=True)
print("ensure_infonet_ready:", json.dumps(ready, indent=2)[:2000])
if not ready.get("ok"):
raise RuntimeError(f"ensure_infonet_ready failed: {ready}")
post = await client.post_to_gate("infonet", message)
print("post_to_gate:", json.dumps(post, indent=2)[:2000])
if not post.get("ok"):
raise RuntimeError(f"post_to_gate failed: {post}")
return post
finally:
await client.close()
def local_gate_has_event(event_id: str) -> bool:
code = f"""
from services.mesh.mesh_hashchain import gate_store
evt = gate_store.get_event({event_id!r})
print('yes' if evt else 'no')
"""
return docker_python(code) == "yes"
REMOTE_CONTAINERS = {
"shadowbroker": "shadowbroker-relay", # seed VPS
"pete": "shadowbroker-backend",
}
def peer_gate_has_event(host: str, event_id: str) -> bool:
container = REMOTE_CONTAINERS.get(host, "shadowbroker-backend")
remote_code = (
"from services.mesh.mesh_hashchain import gate_store; "
f"print('yes' if gate_store.get_event({event_id!r}) else 'no')"
)
import shlex
ssh = [
"ssh",
"-o",
"BatchMode=yes",
"-o",
"StrictHostKeyChecking=accept-new",
host,
f"docker exec {container} python -c {shlex.quote(remote_code)}",
]
proc = subprocess.run(ssh, capture_output=True, text=True, timeout=120, check=False)
out = (proc.stdout or "").strip()
if proc.returncode != 0:
print(f"ssh {host} warn:", proc.stderr.strip() or proc.stdout.strip())
return False
return out == "yes"
def wait_for_propagation(event_id: str, *, seconds: int = 90) -> dict[str, bool]:
deadline = time.time() + seconds
results = {"local": False, "seed": False, "pete": False}
while time.time() < deadline:
results["local"] = local_gate_has_event(event_id)
results["seed"] = peer_gate_has_event("shadowbroker", event_id)
results["pete"] = peer_gate_has_event("pete", event_id)
if all(results.values()):
break
time.sleep(5)
return results
def main() -> int:
print(f"E2E marker: {MARKER}")
secret = bootstrap_hmac_and_full_tier()
print("HMAC secret bootstrapped (full tier)")
post = asyncio.run(agent_post(secret, MARKER))
event_id = str(post.get("event_id") or "")
if not event_id:
raise RuntimeError(f"post succeeded but no event_id in response: {post}")
print(f"event_id={event_id}")
print("Waiting for propagation to local / seed / pete ...")
results = wait_for_propagation(event_id, seconds=120)
print("propagation:", json.dumps(results, indent=2))
if not results["local"]:
raise SystemExit("FAIL: event not in local gate_store")
if not results["seed"] and not results["pete"]:
raise SystemExit("FAIL: event not observed on seed or pete within timeout")
if results["seed"] and results["pete"]:
print("PASS: agent HMAC post propagated to local, seed, and pete")
return 0
print("PARTIAL: local ok; seed=%s pete=%s" % (results["seed"], results["pete"]))
return 0 if results["local"] else 1
if __name__ == "__main__":
raise SystemExit(main())
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
import shlex
import subprocess
import sys
EVENT_ID = sys.argv[1] if len(sys.argv) > 1 else ""
if not EVENT_ID:
raise SystemExit("usage: verify_gate_event.py <event_id>")
code = (
"from services.mesh.mesh_hashchain import gate_store; "
f"e=gate_store.get_event({EVENT_ID!r}); "
"print('ok' if e else 'no')"
)
hosts = [
("local", None, "shadowbroker-backend"),
("seed", "shadowbroker", "shadowbroker-relay"),
("pete", "pete", "shadowbroker-backend"),
]
for label, ssh_host, container in hosts:
remote = f"docker exec {container} python -c {shlex.quote(code)}"
if ssh_host:
proc = subprocess.run(
["ssh", "-o", "BatchMode=yes", ssh_host, remote],
capture_output=True,
text=True,
timeout=60,
)
else:
proc = subprocess.run(
["docker", "exec", container, "python", "-c", code],
capture_output=True,
text=True,
timeout=60,
)
out = (proc.stdout or proc.stderr).strip()
print(f"{label}: {out}")
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""Verify v1 Infonet swarm: fleet join, manifest peers, optional gate propagation."""
from __future__ import annotations
import json
import os
import subprocess
import sys
import time
import urllib.error
import urllib.request
API = os.environ.get("SHADOWBROKER_API", "http://127.0.0.1:8000").strip().rstrip("/")
MARKER = os.environ.get("SWARM_VERIFY_MARKER", f"SWARM-V1-{int(time.time())}")
def http_json(method: str, path: str, body: dict | None = None, *, timeout: int = 180) -> dict:
data = None
headers = {"Content-Type": "application/json"}
if body is not None:
data = json.dumps(body, separators=(",", ":"), sort_keys=True).encode("utf-8")
req = urllib.request.Request(f"{API}{path}", data=data, headers=headers, method=method.upper())
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"{method} {path} -> {exc.code}: {detail}") from exc
def docker_python(code: str) -> str:
proc = subprocess.run(
["docker", "exec", "shadowbroker-backend", "python", "-c", code],
capture_output=True,
text=True,
timeout=300,
check=False,
)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or "docker exec failed")
return proc.stdout.strip()
def step_ghcr_fleet_join() -> None:
out = docker_python(
"import json; "
"from services.mesh.mesh_fleet_defaults import infonet_fleet_join_enabled, FLEET_SEED_ONION_URL; "
"print(json.dumps({'fleet_join': infonet_fleet_join_enabled(), 'seed': FLEET_SEED_ONION_URL}))"
)
payload = json.loads(out)
if not payload.get("fleet_join"):
raise RuntimeError(f"fleet join disabled in container: {payload}")
if not str(payload.get("seed") or "").endswith(".onion:8000"):
raise RuntimeError(f"unexpected fleet seed: {payload}")
print("PASS: container has fleet join defaults")
def step_enable_node_and_join() -> dict:
code = r"""
import json
import main as main_mod
from services.mesh.mesh_swarm_runtime import (
announce_local_peer_to_seeds,
refresh_swarm_manifest_from_seeds,
)
main_mod._set_participant_node_enabled(True)
announce = announce_local_peer_to_seeds(force=True)
manifest = refresh_swarm_manifest_from_seeds(force=True)
print(json.dumps({
'ok': bool(announce.get('ok')) or bool(manifest.get('ok')),
'announce': announce,
'manifest_pull': manifest,
}))
"""
join = json.loads(docker_python(code))
print("swarm/join:", json.dumps(join, indent=2)[:4000])
if not join.get("ok"):
raise RuntimeError(f"swarm join failed: {join}")
manifest = join.get("manifest_pull") or {}
peer_count = int(manifest.get("merged_peer_count") or manifest.get("peer_count") or 0)
if peer_count < 1:
raise RuntimeError(f"manifest has no peers: {join}")
print(f"PASS: swarm join ok ({peer_count} manifest peer(s))")
return join
def step_manifest_lists_pete(join: dict) -> None:
manifest = join.get("manifest_pull") or {}
peer_count = int(manifest.get("merged_peer_count") or manifest.get("peer_count") or 0)
if peer_count < 2:
raise RuntimeError(f"expected fleet manifest with seed + participants, got: {manifest}")
code = r"""
import json
from services.mesh.mesh_router import authenticated_push_peer_urls
print(json.dumps({'push_peers': authenticated_push_peer_urls()[:12]}))
"""
payload = json.loads(docker_python(code))
push_peers = [p for p in payload.get("push_peers") or [] if p]
onion_peers = [p for p in push_peers if ".onion" in p]
print("sync peer store:", json.dumps(payload, indent=2))
if not onion_peers:
raise RuntimeError(f"expected onion peers in local sync store after manifest pull, got: {push_peers}")
print("PASS: manifest pull populated onion fleet peer(s)")
def step_gate_propagation() -> None:
try:
subprocess.run(
[sys.executable, os.path.join(os.path.dirname(__file__), "e2e_openclaw_infonet_agent_live.py")],
env={**os.environ, "E2E_MARKER": MARKER, "SHADOWBROKER_API": API},
check=True,
timeout=600,
)
print("PASS: gate event propagated across fleet")
except subprocess.CalledProcessError as exc:
raise RuntimeError(f"gate propagation check failed (exit {exc.returncode})") from exc
except FileNotFoundError:
print("SKIP: e2e_openclaw_infonet_agent_live.py not available")
def main() -> int:
print(f"Swarm v1 verify against {API}")
step_ghcr_fleet_join()
join = step_enable_node_and_join()
step_manifest_lists_pete(join)
if os.environ.get("SWARM_VERIFY_SKIP_PROPAGATION") != "1":
step_gate_propagation()
print("ALL SWARM V1 CHECKS PASSED")
return 0
if __name__ == "__main__":
raise SystemExit(main())