feat(cred): harden client onboarding with Instagram linking portal, age verification bypass, and fleet runbook

This commit is contained in:
operator
2026-10-04 21:43:32 +00:00
parent 7902622852
commit 178ddc5dbf
10 changed files with 898 additions and 41 deletions
+378
View File
@@ -0,0 +1,378 @@
#!/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 <node> --email <email> [--service muse] [--account-name <name>]
cred-client.py submit-otp --node <node> --otp <code> [--email <email>]
cred-client.py status --node <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 <root@bl>\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 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")
# list
sub.add_parser("list", parents=[common], help="List all registered nodes and vitality statuses")
args = p.parse_args()
client = CredClient()
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 <code>")
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()