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 @@
-
+