mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-28 00:39:59 +02:00
7151563a41
Adds connect-contact HTTP endpoint with cached-bundle support, subprocess contact send via docker cp bundle file, and direct Tor prekey fetch to avoid wedging single-worker uvicorn. Co-authored-by: Cursor <cursoragent@cursor.com>
797 lines
27 KiB
Python
797 lines
27 KiB
Python
"""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 join_swarm_with_retries
|
|
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:
|
|
joined = join_swarm_with_retries()
|
|
steps["announce"] = joined.get("announce") or {}
|
|
steps["manifest_pull"] = joined.get("manifest_pull") or {}
|
|
steps["swarm_attempts"] = joined.get("attempts")
|
|
ok = bool(joined.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 join_swarm_with_retries
|
|
|
|
joined = join_swarm_with_retries()
|
|
return {
|
|
"ok": bool(joined.get("ok")),
|
|
"announce": joined.get("announce") or {},
|
|
"manifest_pull": joined.get("manifest_pull") or {},
|
|
"attempts": joined.get("attempts"),
|
|
"detail": joined.get("detail"),
|
|
}
|
|
|
|
|
|
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 = "",
|
|
peer_dh_pub: 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)
|
|
|
|
try:
|
|
from services.config import get_settings
|
|
from services.mesh.mesh_wormhole_seal import build_sender_seal
|
|
|
|
if (
|
|
delivery == "shared"
|
|
and bool(get_settings().MESH_DM_REQUIRE_SENDER_SEAL_SHARED)
|
|
and not str(dm_payload.get("sender_seal", "") or "").strip()
|
|
):
|
|
seal = build_sender_seal(
|
|
recipient_id=recipient,
|
|
recipient_dh_pub=str(peer_dh_pub or ""),
|
|
msg_id=msg_id,
|
|
timestamp=timestamp,
|
|
)
|
|
if seal.get("ok"):
|
|
dm_payload["sender_seal"] = str(seal.get("sender_seal") or "")
|
|
except Exception:
|
|
pass
|
|
|
|
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,
|
|
"sender_seal": str(dm_payload.get("sender_seal") 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),
|
|
"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 = "",
|
|
cached_prekey_bundle: dict[str, Any] | None = None,
|
|
) -> 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("/")
|
|
if cached_prekey_bundle and cached_prekey_bundle.get("ok"):
|
|
bundle = dict(cached_prekey_bundle)
|
|
else:
|
|
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,
|
|
fetched_bundle=bundle,
|
|
)
|
|
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 = "",
|
|
lookup_token: str = "",
|
|
lookup_peer_url: 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"}
|
|
|
|
token = str(lookup_token or "").strip()
|
|
preferred_peer = str(lookup_peer_url or "").strip().rstrip("/")
|
|
dh_pub = str(peer_dh_pub or "").strip()
|
|
if not dh_pub:
|
|
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
|
|
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, lookup_token=token)
|
|
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",
|
|
lookup_peer_url=preferred_peer,
|
|
)
|
|
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"}
|