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. - 2026-10-03: Renamed smoke->muse, phone-test->pip for unified naming.
Profiles preserved, sessions persisted (muse). WireGuard identities renamed. Profiles preserved, sessions persisted (muse). WireGuard identities renamed.
| 646 | warp-646 | 104.28.195.181 | 9430 | active | 646 (phone_otp, first Meta Account option) | | 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) |
+16 -17
View File
@@ -49,28 +49,27 @@ restart_browser() {
# Main # Main
echo "=== Health check $(date -Iseconds) ===" >> "$LOG" echo "=== Health check $(date -Iseconds) ===" >> "$LOG"
# muse (node muse, CDP 9410) check_one() {
if ! check_agent "muse" "muse" "9410"; then local agent=$1
restart_browser "muse" "9410" local cdp_port=$2
# node == agent == profile (unified naming)
if ! check_agent "$agent" "$agent" "$cdp_port"; then
restart_browser "$agent" "$cdp_port"
sleep 5 sleep 5
if ! check_agent "muse" "muse" "9410"; then if ! check_agent "$agent" "$agent" "$cdp_port"; then
echo "$(date -Iseconds) muse: CRITICAL - still down after restart" >> "$LOG" echo "$(date -Iseconds) $agent: CRITICAL - still down after restart" >> "$LOG"
# TODO: Alert operator (e.g., via board post or email) # TODO: Alert operator (e.g., via board post or email)
else else
echo "$(date -Iseconds) muse: RECOVERED after restart" >> "$LOG" echo "$(date -Iseconds) $agent: RECOVERED after restart" >> "$LOG"
fi fi
fi fi
}
# pip (node pip, CDP 9420) # Every active node in the NODES.md registry gets checked — new nodes
if ! check_agent "pip" "pip" "9420"; then # propagate automatically, no per-node blocks to add.
restart_browser "pip" "9420" "$NETVM_BIN/netvm-registry.py" 2>/dev/null | while IFS=: read -r node port; do
sleep 5 [ -n "$node" ] && [ -n "$port" ] && check_one "$node" "$port"
if ! check_agent "pip" "pip" "9420"; then done
echo "$(date -Iseconds) pip: CRITICAL - still down after restart" >> "$LOG"
else
echo "$(date -Iseconds) pip: RECOVERED after restart" >> "$LOG"
fi
fi
# Trim log (keep last 1000 lines) # Trim log (keep last 1000 lines)
tail -1000 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG" 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> wait [timeout]
muse-chat-api.py --account <agent> approvals # check pending approvals 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"), def _load_accounts():
"pip": ("pip", "http://127.0.0.1:9420/json/list"), """Build the accounts table from the NODES.md registry — new nodes
"646": ("646", "http://127.0.0.1:9430/json/list"), propagate automatically instead of being hardcoded here."""
"opm": ("opm", "http://127.0.0.1:9440/json/list"), 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) # IPs we trust for auto-approval (our infrastructure)
TRUSTED_IPS = { 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 This keeps credentials out of the automation — the OTP is provided
transiently via the operator, never stored. 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) ts = json.load(r)
pages = [t for t in ts if t.get('type') == 'page'] pages = [t for t in ts if t.get('type') == 'page']
if not pages: if not pages:
@@ -41,9 +54,20 @@ def main():
p = argparse.ArgumentParser() p = argparse.ArgumentParser()
p.add_argument('--email', required=True) p.add_argument('--email', required=True)
p.add_argument('--otp', default=None) 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() 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) ws = websocket.create_connection(page['webSocketDebuggerUrl'], timeout=15)
# Step 1: Check if already logged in # 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). Part of the cred onboarding module (front-door repo, docs/CRED-MODULE.md).
""" """
import argparse import argparse
import importlib.util
import json import json
import subprocess import subprocess
import sys import sys
import urllib.request import urllib.request
SIGNIN = "/home/super/Projects/NetVM/bin/muse-signin.py" 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): def cdp_ok(port):
@@ -51,9 +59,10 @@ def main():
print("ERROR: unsupported service/id_type " print("ERROR: unsupported service/id_type "
"(muse+email only for now)", file=sys.stderr) "(muse+email only for now)", file=sys.stderr)
return 1 return 1
port = CDP_PORTS.get(args.node) port = _load_registry().port_for(args.node)
if not port: 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 return 1
if args.dry_run: if args.dry_run:
@@ -69,7 +78,7 @@ def main():
print("ERROR: no identifier on stdin", file=sys.stderr) print("ERROR: no identifier on stdin", file=sys.stderr)
return 1 return 1
cmd = [sys.executable, SIGNIN, "--email", identifier] cmd = [sys.executable, SIGNIN, "--node", args.node, "--email", identifier]
if args.step == "submit": if args.step == "submit":
if not code: if not code:
print("ERROR: no code on stdin", file=sys.stderr) print("ERROR: no code on stdin", file=sys.stderr)