diff --git a/ACCOUNTS.md b/ACCOUNTS.md index 1b6d7d2..49cb85f 100644 --- a/ACCOUNTS.md +++ b/ACCOUNTS.md @@ -16,7 +16,7 @@ node name, chrome-box profile, API `--account`, and the agent's display name. | email | Email used for login (if email_otp). Never store passwords. | | phone_otp | `yes` if phone OTP was used. Never store the phone number. | | instagram_linked | `yes`/`no`/`unknown` — whether Meta account has IG linked | -| status | `active`, `pending_auth`, `expired`, `disabled` | +| status | `active`, `pending_auth`, `needs_signup`, `expired`, `disabled` | | egress_ip | Current WARP egress IP for the node | | cdp_port | CDP port for the browser | | display_name | Agent's chosen display name in muse.ai (may differ from `agent`) | diff --git a/bin/muse-signin.py b/bin/muse-signin.py index 93692ad..e2a40a6 100755 --- a/bin/muse-signin.py +++ b/bin/muse-signin.py @@ -15,6 +15,13 @@ 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. + +Exit codes: + 0: Success / already logged in + 2: APPROVAL_NEEDED (OTP prompt reached, awaiting code) + 3: NEEDS_HUMAN (multi-account selection needs manual choice) + 4: NEEDS_SIGNUP (account not found / registration required) + 1: General error """ import json, urllib.request, websocket, time, sys, argparse, importlib.util @@ -199,6 +206,35 @@ def main(): ws.close() sys.exit(1) else: + body_full = ev(ws, "document.body.innerText.slice(0,2000)") or "" + body_lower = body_full.lower() + signup_cues = [ + "no account found", + "couldn't find your account", + "cannot find", + "create an account", + "create your account", + "create account", + "sign up", + "sign-up", + "doesn't have an account", + "not registered", + "register", + "get started" + ] + has_signup_cue = any(cue in body_lower for cue in signup_cues) + has_name_or_pwd = ev(ws, """(()=>{ + const inps = [...document.querySelectorAll('input')]; + return inps.some(i => (i.placeholder && i.placeholder.toLowerCase().includes('name')) || + (i.name && i.name.toLowerCase().includes('name')) || + (i.type === 'password')); + })()""") + + if has_signup_cue or has_name_or_pwd: + print(f"NEEDS_SIGNUP: Account does not exist on Muse for {args.email}. Client must sign up first at https://muse.ai", file=sys.stderr) + ws.close() + sys.exit(4) + print("No OTP prompt found - check page state", file=sys.stderr) print(f"Body: {body[:200]}", file=sys.stderr) ws.close() diff --git a/bin/onboard-driver.py b/bin/onboard-driver.py index 7794c49..264645f 100755 --- a/bin/onboard-driver.py +++ b/bin/onboard-driver.py @@ -8,7 +8,9 @@ Runs INSIDE the node's netns (via netvm-exec.sh). Reads the identifier onboard-driver.py --node muse --service muse --id-type email --step submit Exit codes: 0 = step done, 2 = APPROVAL_NEEDED (code sent, awaiting OTP), -3 = NEEDS_HUMAN (multi-account selection needs a person), 1 = failed. +3 = NEEDS_HUMAN (multi-account selection needs a person), +4 = NEEDS_SIGNUP (unregistered client email — client must sign up first), +1 = failed. The identifier/code are passed to the local signin script as argv (transient, same trust domain — bl is operator infrastructure); they never cross a network boundary except inside the already-encrypted @@ -17,6 +19,7 @@ VM->bl SSH stdin pipe. Part of the cred onboarding module (front-door repo, docs/CRED-MODULE.md). """ import argparse +import datetime import importlib.util import json import subprocess @@ -54,10 +57,6 @@ def main(): help="display-name hint for multi-account selection") args = p.parse_args() - lines = sys.stdin.read().splitlines() - identifier = lines[0].strip() if lines else "" - code = lines[1].strip() if len(lines) > 1 else "" - if args.service != "muse" or args.id_type != "email": print("ERROR: unsupported service/id_type " "(muse+email only for now)", file=sys.stderr) @@ -77,6 +76,11 @@ def main(): print("ERROR: CDP unreachable on %s" % port, file=sys.stderr) return 1 + lines = sys.stdin.read().splitlines() + identifier = lines[0].strip() if lines else "" + code = lines[1].strip() if len(lines) > 1 else "" + + if not identifier: print("ERROR: no identifier on stdin", file=sys.stderr) return 1 @@ -90,9 +94,28 @@ def main(): return 1 cmd += ["--otp", code] r = subprocess.run(cmd, capture_output=True, text=True, timeout=220) - # Propagate the signin script's contract: 2 = OTP prompt reached. + # Propagate the signin script's contract: + # 0 = done, 2 = OTP prompt reached, 3 = NEEDS_HUMAN, 4 = NEEDS_SIGNUP sys.stdout.write(r.stdout) sys.stderr.write(r.stderr) + + if r.returncode == 4: + log_entry = { + "ts": datetime.datetime.now(datetime.timezone.utc).isoformat(), + "type": "onboarding_needs_signup", + "node": args.node, + "service": args.service, + "email": identifier, + "status": "needs_signup", + "action_required": "ask_client_to_sign_up", + "signup_url": "https://muse.ai" + } + try: + with open("/home/super/Projects/NetVM/job-log.jsonl", "a") as f: + f.write(json.dumps(log_entry) + "\n") + except Exception: + pass + return r.returncode diff --git a/web/box/www/index.html b/web/box/www/index.html index 84bb5c1..bdefd3a 100644 --- a/web/box/www/index.html +++ b/web/box/www/index.html @@ -401,6 +401,6 @@ - +