#!/usr/bin/env python3 """cred-client.py — Agent API client & CLI for cred.muse-dev.online. Provides programmatic and CLI access for agents and operators to: - initiate: start onboarding for a client email/node - submit-otp: submit verification code transiently - status: query onboarding & vitality state for a node - list: list fleet accounts, emails, and statuses Authentication: - Machine identity signature via ~/.ssh/muse-health (machine bl) - Or Bearer token from CRED_TOKEN / OPERATOR_TOKEN environment variable. Usage: cred-client.py initiate --node --email [--service muse] [--account-name ] cred-client.py submit-otp --node --otp [--email ] cred-client.py status --node [--json] cred-client.py list [--json] """ import argparse import datetime import importlib.util import json import os import re import subprocess import sys import tempfile import urllib.error import urllib.request NETVM_DIR = os.environ.get("NETVM_DIR", "/home/super/Projects/NetVM") KEY_DEFAULT = os.path.expanduser("~/.ssh/muse-health") CRED_API_DEFAULT = os.environ.get("CRED_API_URL", "https://cred.muse-dev.online/api/cred") def load_registry(): path = os.path.join(NETVM_DIR, "bin", "netvm-registry.py") try: spec = importlib.util.spec_from_file_location("netvm_registry", path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod except Exception: return None def get_accounts(): """Parse ACCOUNTS.md into a structured list of accounts.""" path = os.path.join(NETVM_DIR, "ACCOUNTS.md") accounts = [] if not os.path.exists(path): return accounts with open(path) as f: for line in f: line = line.strip() if not line.startswith("|") or line.startswith("| agent") or line.startswith("|-------"): continue cols = [c.strip() for c in line.strip("|").split("|")] if len(cols) >= 9: accounts.append({ "agent": cols[0], "node": cols[1], "profile": cols[2], "login_type": cols[3], "meta_label": cols[4], "email": cols[5], "phone_otp": cols[6], "instagram_linked": cols[7], "status": cols[8], "egress_ip": cols[9] if len(cols) > 9 else "", "cdp_port": cols[10] if len(cols) > 10 else "", "display_name": cols[11] if len(cols) > 11 else "", "notes": cols[12] if len(cols) > 12 else "" }) return accounts def sign_payload(payload_bytes, key_path=KEY_DEFAULT, namespace="cred"): """Sign payload using SSH ed25519 key (zero secret across wire).""" if not os.path.exists(key_path): return None tmp = tempfile.mkdtemp() try: data_file = os.path.join(tmp, "data") with open(data_file, "wb") as f: f.write(payload_bytes) r = subprocess.run( ["ssh-keygen", "-Y", "sign", "-f", key_path, "-n", namespace, data_file], capture_output=True, text=True ) if r.returncode != 0: return None with open(data_file + ".sig") as f: return f.read().strip() finally: subprocess.run(["rm", "-rf", tmp], capture_output=True) class CredClient: def __init__(self, api_url=CRED_API_DEFAULT, key_path=KEY_DEFAULT): self.api_url = api_url.rstrip("/") self.key_path = key_path self.token = os.environ.get("CRED_TOKEN") or os.environ.get("OPERATOR_TOKEN") def run_local_driver(self, node, step, email, otp=None, account_name=None): """Execute local onboard-driver.py inside the node's netns.""" exec_script = os.path.join(NETVM_DIR, "bin", "netvm-exec.sh") driver_script = os.path.join(NETVM_DIR, "bin", "onboard-driver.py") cmd = [exec_script, node, "--", sys.executable, driver_script, "--node", node, "--service", "muse", "--id-type", "email", "--step", step] if account_name: cmd += ["--account-name", account_name] input_data = email + "\n" if otp: input_data += str(otp) + "\n" p = subprocess.run(cmd, input=input_data, capture_output=True, text=True, timeout=240) return p.returncode, p.stdout.strip(), p.stderr.strip() def initiate(self, node, email, service="muse", account_name=None): """Initiate client onboarding.""" ret, stdout, stderr = self.run_local_driver(node, "initiate", email, account_name=account_name) if ret == 0: return {"status": "active", "node": node, "email": email, "message": "Already authenticated and session active."} elif ret == 2: return {"status": "awaiting_otp", "node": node, "email": email, "message": "OTP verification code sent. Awaiting input."} elif ret == 3: return {"status": "needs_human", "node": node, "email": email, "message": "Multiple accounts match identifier. Manual selection required."} else: return {"status": "error", "node": node, "email": email, "code": ret, "detail": stderr or stdout} def submit_otp(self, node, otp, email=None, service="muse"): """Submit transient verification code.""" if not email: # Look up email from ACCOUNTS.md for acct in get_accounts(): if acct["node"] == node and acct["email"] not in ("-", ""): email = acct["email"] break if not email: email = "unknown" ret, stdout, stderr = self.run_local_driver(node, "submit", email, otp=otp) if ret == 0: return {"status": "active", "node": node, "email": email, "message": "Authentication successful. Chat session active."} elif ret == 3: return {"status": "needs_human", "node": node, "email": email, "message": "Multiple accounts match identifier."} else: return {"status": "error", "node": node, "email": email, "code": ret, "detail": stderr or stdout} def status(self, node): """Check status of a node.""" accounts = get_accounts() target = next((a for a in accounts if a["node"] == node), None) port = None reg = load_registry() if reg: port = reg.port_for(node) # Vitality check alive = False detail = "" if port: try: checker = os.path.join(NETVM_DIR, "bin", "accounts-health.py") cmd = ["sudo", "-n", "ip", "netns", "exec", f"warp-{node}", sys.executable, checker, str(port)] r = subprocess.run(cmd, capture_output=True, text=True, timeout=10) if r.returncode == 0: data = json.loads(r.stdout.strip().splitlines()[-1]) alive = data.get("session_alive", False) detail = data.get("detail", "") except Exception as e: detail = str(e) return { "node": node, "status": target["status"] if target else "unregistered", "email": target["email"] if target else "-", "cdp_port": port, "session_alive": alive, "detail": detail } def list_all(self): """List all accounts and live statuses.""" res = [] for a in get_accounts(): st = self.status(a["node"]) res.append(st) return res def notify_operator(self, subject, body, to_email="defnotabotnet@gmail.com"): """Send notification to operator via local MTA (msmtp or mail).""" sent = False err = None # Try msmtp first try: p = subprocess.Popen(["msmtp", to_email], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) msg = f"Subject: {subject}\nTo: {to_email}\nFrom: NetVM Automation \n\n{body}\n" stdout, stderr = p.communicate(input=msg) if p.returncode == 0: sent = True else: err = stderr.strip() or stdout.strip() except Exception as e: err = str(e) if not sent: # Fallback to mail / s-nail try: p = subprocess.Popen(["mail", "-s", subject, to_email], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) stdout, stderr = p.communicate(input=body) if p.returncode == 0: sent = True else: err = stderr.strip() or stdout.strip() except Exception as e: err = str(e) return {"sent": sent, "recipient": to_email, "error": err if not sent else None} def link_instagram(self, node, notify=False): """Fetch the Meta Accounts Center OAuth URL and provide one-tap Tailscale link.""" portal_script = os.path.join(NETVM_DIR, "bin", "tailscale-verify-portal.py") ts_ip = "100.123.153.75" ts_dns = "bl.tailfb5960.ts.net" try: import importlib.util spec = importlib.util.spec_from_file_location("portal", portal_script) portal_mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(portal_mod) ts_ip = portal_mod.get_tailscale_ip() ts_dns = portal_mod.get_tailscale_dns() ig_data = portal_mod.fetch_node_ig_link(node) except Exception as e: ig_data = {"error": str(e)} direct_url = ig_data.get("url") if isinstance(ig_data, dict) else None portal_url = f"http://{ts_dns}:8765/verify/{node}" portal_ip_url = f"http://{ts_ip}:8765/verify/{node}" email_result = None if notify and direct_url: subject = f"[NetVM Action Required] Link Instagram for Node '{node}'" body = ( f"Node '{node}' is waiting at the Muse age verification gate.\n\n" f"Tap the Tailscale portal link from your device to approve:\n" f" {portal_url}\n" f" (or {portal_ip_url})\n\n" f"Direct OAuth URL:\n" f" {direct_url}\n\n" f"After approving in Instagram, NetVM will automatically transition node '{node}' to active." ) email_result = self.notify_operator(subject, body) return { "node": node, "status": "needs_verification", "portal_url": portal_url, "portal_ip_url": portal_ip_url, "direct_oauth_url": direct_url, "error": ig_data.get("error") if isinstance(ig_data, dict) else None, "email_notified": email_result.get("sent") if email_result else False } def audit_meta(self, node): """Query Meta Accounts Center for linked profiles and security status.""" meta_script = os.path.join(NETVM_DIR, "bin", "meta-acct.py") cmd = ["sudo", "-n", "ip", "netns", "exec", f"warp-{node}", sys.executable, meta_script, "list-linked", node] try: res = subprocess.run(cmd, capture_output=True, text=True, timeout=15) out = res.stdout.strip() if out: # Find outermost JSON object start = out.find("{") end = out.rfind("}") if start >= 0 and end >= start: return json.loads(out[start:end+1]) return json.loads(out) return {"error": res.stderr.strip() or "No output from meta-acct.py"} except Exception as e: return {"error": str(e)} def main(): common = argparse.ArgumentParser(add_help=False) common.add_argument("--json", action="store_true", help="Output JSON response") p = argparse.ArgumentParser(description="cred-client: Agent API client for cred onboarding", parents=[common]) sub = p.add_subparsers(dest="command", required=True) # initiate p_init = sub.add_parser("initiate", parents=[common], help="Initiate onboarding flow for a node") p_init.add_argument("--node", required=True, help="Node label (e.g. opm, pip, client1)") p_init.add_argument("--email", required=True, help="Client login email") p_init.add_argument("--service", default="muse", help="Service name (default: muse)") p_init.add_argument("--account-name", default=None, help="Display name hint for multi-account selector") # submit-otp p_otp = sub.add_parser("submit-otp", parents=[common], help="Submit transient OTP verification code") p_otp.add_argument("--node", required=True, help="Node label") p_otp.add_argument("--otp", required=True, help="6-digit verification code") p_otp.add_argument("--email", default=None, help="Client email (optional, auto-detected from registry)") # status p_stat = sub.add_parser("status", parents=[common], help="Query onboarding and session status for a node") p_stat.add_argument("--node", required=True, help="Node label") # link-instagram p_link = sub.add_parser("link-instagram", parents=[common], help="Generate Tailscale one-tap portal & OAuth link for age verification") p_link.add_argument("--node", required=True, help="Node label") p_link.add_argument("--notify", action="store_true", help="Send email alert to operator via local MTA") # meta-audit p_meta = sub.add_parser("meta-audit", parents=[common], help="Query Meta Accounts Center for linked profiles and security status") p_meta.add_argument("--node", required=True, help="Node label") # list sub.add_parser("list", parents=[common], help="List all registered nodes and vitality statuses") args = p.parse_args() client = CredClient() if args.command == "meta-audit": res = client.audit_meta(args.node) if args.json: print(json.dumps(res, indent=2)) else: print(f"\n=== META ACCOUNTS CENTER AUDIT: {args.node} ===") if res.get("error"): print(f"Error: {res['error']}", file=sys.stderr) sys.exit(1) print(f"Meta Account Email: {res.get('email') or '(none / phone-only)'}") profiles = res.get("profiles", []) print(f"Linked Profiles ({len(profiles)}):") for p_info in profiles: print(f" - [{p_info.get('type')}] {p_info.get('name')}") sys.exit(0) if args.command == "link-instagram": res = client.link_instagram(args.node, notify=args.notify) if args.json: print(json.dumps(res, indent=2)) else: print(f"\n=== INSTAGRAM LINKING: {args.node} ===") if res.get("error"): print(f"Error: {res['error']}", file=sys.stderr) sys.exit(1) print(f"Tailscale Portal: {res['portal_url']}") print(f"Direct IP Portal: {res['portal_ip_url']}") print(f"OAuth URL: {res['direct_oauth_url'][:80]}...") if args.notify: print(f"Email Notified: {'YES' if res['email_notified'] else 'FAILED'}") print("\nTap either Tailscale link from your phone/browser to complete Meta age verification.") sys.exit(0) if args.command == "initiate": res = client.initiate(args.node, args.email, service=args.service, account_name=args.account_name) if args.json: print(json.dumps(res, indent=2)) else: st = res.get("status") if st == "awaiting_otp": print(f"[APPROVAL_NEEDED] Code sent to [redacted] for node '{args.node}'.") print(f"Submit OTP with: super cred submit-otp --node {args.node} --otp ") sys.exit(2) elif st == "active": print(f"[SUCCESS] Node '{args.node}' is already authenticated and active.") sys.exit(0) else: print(f"[{st.upper()}] {res.get('message') or res.get('detail')}") sys.exit(res.get("code", 1)) elif args.command == "submit-otp": res = client.submit_otp(args.node, args.otp, email=args.email) if args.json: print(json.dumps(res, indent=2)) else: st = res.get("status") if st == "active": print(f"[SUCCESS] Node '{args.node}' successfully signed in!") sys.exit(0) else: print(f"[{st.upper()}] {res.get('message') or res.get('detail')}") sys.exit(res.get("code", 1)) elif args.command == "status": res = client.status(args.node) if args.json: print(json.dumps(res, indent=2)) else: print(f"Node: {res['node']}") print(f"Email: {res['email']}") print(f"Status: {res['status']}") print(f"CDP Port: {res['cdp_port'] or '-'}") print(f"Session Alive: {'YES' if res['session_alive'] else 'NO'}") if res["detail"]: print(f"Detail: {res['detail']}") elif args.command == "list": items = client.list_all() if args.json: print(json.dumps(items, indent=2)) else: print(f"{'NODE':<10} {'STATUS':<14} {'CDP':<6} {'ALIVE':<7} {'EMAIL'}") print("-" * 65) for it in items: alive_str = "YES" if it["session_alive"] else "NO" print(f"{it['node']:<10} {it['status']:<14} {str(it['cdp_port'] or '-'):<6} {alive_str:<7} {it['email']}") if __name__ == "__main__": main()