propagation: single registry drives all node references

New bin/netvm-registry.py parses NODES.md (node -> cdp_port); every
consumer reads from it instead of hardcoding:
- onboard-driver.py: CDP_PORTS dict -> registry lookup (new nodes work)
- muse-signin.py: hardcoded 9410 -> --node/--cdp-port args
- muse-chat-api.py: hardcoded ACCOUNTS -> registry-built
- agent-health.sh: hardcoded muse/pip blocks -> loop over all active
  nodes (646 and opm now get health coverage too)
NODES.md: record opm node (9440).
This commit is contained in:
operator
2026-10-03 21:28:36 +00:00
parent 1191cbd86e
commit a7639e2341
6 changed files with 156 additions and 36 deletions
+1
View File
@@ -11,3 +11,4 @@ Unified naming: node == agent == profile == API account.
- 2026-10-03: Renamed smoke->muse, phone-test->pip for unified naming.
Profiles preserved, sessions persisted (muse). WireGuard identities renamed.
| 646 | warp-646 | 104.28.195.181 | 9430 | active | 646 (phone_otp, first Meta Account option) |
| opm | warp-opm | 104.28.195.181 | 9440 | active | opm (yourfriendnico@proton.me, email_otp, Nico Parada) |
+19 -20
View File
@@ -49,28 +49,27 @@ restart_browser() {
# Main
echo "=== Health check $(date -Iseconds) ===" >> "$LOG"
# muse (node muse, CDP 9410)
if ! check_agent "muse" "muse" "9410"; then
restart_browser "muse" "9410"
sleep 5
if ! check_agent "muse" "muse" "9410"; then
echo "$(date -Iseconds) muse: CRITICAL - still down after restart" >> "$LOG"
# TODO: Alert operator (e.g., via board post or email)
else
echo "$(date -Iseconds) muse: RECOVERED after restart" >> "$LOG"
check_one() {
local agent=$1
local cdp_port=$2
# node == agent == profile (unified naming)
if ! check_agent "$agent" "$agent" "$cdp_port"; then
restart_browser "$agent" "$cdp_port"
sleep 5
if ! check_agent "$agent" "$agent" "$cdp_port"; then
echo "$(date -Iseconds) $agent: CRITICAL - still down after restart" >> "$LOG"
# TODO: Alert operator (e.g., via board post or email)
else
echo "$(date -Iseconds) $agent: RECOVERED after restart" >> "$LOG"
fi
fi
fi
}
# pip (node pip, CDP 9420)
if ! check_agent "pip" "pip" "9420"; then
restart_browser "pip" "9420"
sleep 5
if ! check_agent "pip" "pip" "9420"; then
echo "$(date -Iseconds) pip: CRITICAL - still down after restart" >> "$LOG"
else
echo "$(date -Iseconds) pip: RECOVERED after restart" >> "$LOG"
fi
fi
# Every active node in the NODES.md registry gets checked — new nodes
# propagate automatically, no per-node blocks to add.
"$NETVM_BIN/netvm-registry.py" 2>/dev/null | while IFS=: read -r node port; do
[ -n "$node" ] && [ -n "$port" ] && check_one "$node" "$port"
done
# Trim log (keep last 1000 lines)
tail -1000 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG"
+17 -7
View File
@@ -16,14 +16,24 @@ Usage:
muse-chat-api.py --account <agent> wait [timeout]
muse-chat-api.py --account <agent> approvals # check pending approvals
"""
import json, urllib.request, websocket, time, sys, argparse
import json, urllib.request, websocket, time, sys, argparse, importlib.util
ACCOUNTS = {
"muse": ("muse", "http://127.0.0.1:9410/json/list"),
"pip": ("pip", "http://127.0.0.1:9420/json/list"),
"646": ("646", "http://127.0.0.1:9430/json/list"),
"opm": ("opm", "http://127.0.0.1:9440/json/list"),
}
def _load_accounts():
"""Build the accounts table from the NODES.md registry — new nodes
propagate automatically instead of being hardcoded here."""
path = "/home/super/Projects/NetVM/bin/netvm-registry.py"
spec = importlib.util.spec_from_file_location("netvm_registry", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
accounts = {}
for node, rec in mod.load().items():
accounts[node] = (node, "http://127.0.0.1:%d/json/list" %
rec["cdp_port"])
return accounts
ACCOUNTS = _load_accounts()
# IPs we trust for auto-approval (our infrastructure)
TRUSTED_IPS = {
+29 -5
View File
@@ -16,12 +16,25 @@ The operator then obtains the OTP (via chat with user) and re-runs:
This keeps credentials out of the automation — the OTP is provided
transiently via the operator, never stored.
"""
import json, urllib.request, websocket, time, sys, argparse
import json, urllib.request, websocket, time, sys, argparse, importlib.util
CDP_URL = "http://127.0.0.1:9410/json/list"
CDP_PORT_DEFAULT = 9410
def get_page():
with urllib.request.urlopen(CDP_URL, timeout=5) as r:
def _registry_port(node):
try:
path = "/home/super/Projects/NetVM/bin/netvm-registry.py"
spec = importlib.util.spec_from_file_location("netvm_registry", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod.port_for(node)
except Exception:
return None
def get_page(port):
cdp_url = "http://127.0.0.1:%s/json/list" % port
with urllib.request.urlopen(cdp_url, timeout=5) as r:
ts = json.load(r)
pages = [t for t in ts if t.get('type') == 'page']
if not pages:
@@ -41,9 +54,20 @@ def main():
p = argparse.ArgumentParser()
p.add_argument('--email', required=True)
p.add_argument('--otp', default=None)
p.add_argument('--node', default=None,
help='node name: CDP port comes from the NODES.md registry')
p.add_argument('--cdp-port', default=None,
help='explicit CDP port (overrides --node lookup)')
args = p.parse_args()
page = get_page()
if args.cdp_port:
port = args.cdp_port
elif args.node:
port = _registry_port(args.node) or CDP_PORT_DEFAULT
else:
port = CDP_PORT_DEFAULT
page = get_page(port)
ws = websocket.create_connection(page['webSocketDebuggerUrl'], timeout=15)
# Step 1: Check if already logged in
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""
netvm-registry.py — single source of truth for node -> CDP port mapping.
Parses NODES.md (the fleet registry). Every script that needs a node's
CDP port reads it from here instead of hardcoding — new nodes propagate
automatically.
Usage:
from netvm_registry import port_for, load
port_for("muse") # -> 9410 (int), None if unknown
CLI:
netvm-registry.py # prints "node:port" lines for active nodes
netvm-registry.py <node> # prints just the port (for bash)
"""
import os
import re
import sys
REGISTRY = os.path.join(os.path.dirname(os.path.dirname(
os.path.abspath(__file__))), "NODES.md")
def load(registry_path=None):
"""Return {node: {netns, egress_ip, cdp_port, status, agent}}."""
path = registry_path or REGISTRY
nodes = {}
try:
with open(path) as f:
for line in f:
line = line.rstrip("\n")
if not line.startswith("|"):
continue
cells = [c.strip() for c in line.strip("|").split("|")]
if len(cells) < 6:
continue
node, netns, egress, port, status, agent = cells[:6]
if node in ("node", "") or not re.fullmatch(r"[a-z0-9-]+",
node):
continue
if node.startswith("-"):
continue
try:
port_n = int(port)
except ValueError:
continue
nodes[node] = {"netns": netns, "egress_ip": egress,
"cdp_port": port_n, "status": status,
"agent": agent}
except FileNotFoundError:
pass
return nodes
def port_for(node, registry_path=None):
"""CDP port for a node, or None if the node isn't registered."""
rec = load(registry_path).get(node)
return rec["cdp_port"] if rec else None
def active_nodes(registry_path=None):
"""{node: port} for nodes with status == active."""
return {n: r["cdp_port"] for n, r in load(registry_path).items()
if r["status"] == "active"}
if __name__ == "__main__":
if len(sys.argv) == 2:
port = port_for(sys.argv[1])
if port is None:
print("unknown node: %s" % sys.argv[1], file=sys.stderr)
sys.exit(1)
print(port)
else:
for node, port in sorted(active_nodes().items()):
print("%s:%d" % (node, port))
+13 -4
View File
@@ -16,13 +16,21 @@ VM->bl SSH stdin pipe.
Part of the cred onboarding module (front-door repo, docs/CRED-MODULE.md).
"""
import argparse
import importlib.util
import json
import subprocess
import sys
import urllib.request
SIGNIN = "/home/super/Projects/NetVM/bin/muse-signin.py"
CDP_PORTS = {"muse": "9410", "pip": "9420"}
def _load_registry():
path = "/home/super/Projects/NetVM/bin/netvm-registry.py"
spec = importlib.util.spec_from_file_location("netvm_registry", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def cdp_ok(port):
@@ -51,9 +59,10 @@ def main():
print("ERROR: unsupported service/id_type "
"(muse+email only for now)", file=sys.stderr)
return 1
port = CDP_PORTS.get(args.node)
port = _load_registry().port_for(args.node)
if not port:
print("ERROR: unknown node", file=sys.stderr)
print("ERROR: unknown node '%s' (not in NODES.md registry)"
% args.node, file=sys.stderr)
return 1
if args.dry_run:
@@ -69,7 +78,7 @@ def main():
print("ERROR: no identifier on stdin", file=sys.stderr)
return 1
cmd = [sys.executable, SIGNIN, "--email", identifier]
cmd = [sys.executable, SIGNIN, "--node", args.node, "--email", identifier]
if args.step == "submit":
if not code:
print("ERROR: no code on stdin", file=sys.stderr)