mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-18 20:10:09 +02:00
Ship DM connect delivery, fleet pubkey lookup, OpenClaw Infonet agent, and relay auto-wormhole.
Auto-relay connect DMs with End Contact severing, signed fleet prekey lookup, OpenClaw private Infonet channel intents, headless relay Tor bootstrap on redeploy, and swarm/DM live verification scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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"))
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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}")
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user