2026-10-03 20:34:33 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""onboard-driver.py — bl-side OTP onboarding driver.
|
|
|
|
|
|
|
|
|
|
Runs INSIDE the node's netns (via netvm-exec.sh). Reads the identifier
|
|
|
|
|
(line 1) and OTP code (line 2, submit step only) from stdin — never argv.
|
|
|
|
|
|
|
|
|
|
onboard-driver.py --node muse --service muse --id-type email --step initiate [--dry-run]
|
|
|
|
|
onboard-driver.py --node muse --service muse --id-type email --step submit
|
|
|
|
|
|
|
|
|
|
Exit codes: 0 = step done, 2 = APPROVAL_NEEDED (code sent, awaiting OTP),
|
2026-10-04 16:37:26 +00:00
|
|
|
3 = NEEDS_HUMAN (multi-account selection needs a person),
|
|
|
|
|
4 = NEEDS_SIGNUP (unregistered client email — client must sign up first),
|
|
|
|
|
1 = failed.
|
2026-10-03 21:30:32 +00:00
|
|
|
The identifier/code are passed to the local signin script as
|
2026-10-03 20:34:33 +00:00
|
|
|
argv (transient, same trust domain — bl is operator infrastructure);
|
|
|
|
|
they never cross a network boundary except inside the already-encrypted
|
|
|
|
|
VM->bl SSH stdin pipe.
|
|
|
|
|
|
|
|
|
|
Part of the cred onboarding module (front-door repo, docs/CRED-MODULE.md).
|
|
|
|
|
"""
|
|
|
|
|
import argparse
|
2026-10-04 16:37:26 +00:00
|
|
|
import datetime
|
2026-10-03 21:28:36 +00:00
|
|
|
import importlib.util
|
2026-10-03 20:34:33 +00:00
|
|
|
import json
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
import urllib.request
|
|
|
|
|
|
|
|
|
|
SIGNIN = "/home/super/Projects/NetVM/bin/muse-signin.py"
|
2026-10-03 21:28:36 +00:00
|
|
|
|
|
|
|
|
|
2026-10-04 17:48:54 +00:00
|
|
|
def _scrub(text, *secrets):
|
|
|
|
|
"""Redact secret values from captured output before passthrough."""
|
|
|
|
|
for s in secrets:
|
|
|
|
|
if s and len(s) >= 4:
|
|
|
|
|
text = text.replace(s, "[redacted]")
|
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _mask_email(identifier):
|
|
|
|
|
local, _, domain = identifier.partition("@")
|
|
|
|
|
return (local[:1] + "***@" + domain) if domain else "***"
|
|
|
|
|
|
|
|
|
|
|
2026-10-03 21:28:36 +00:00
|
|
|
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
|
2026-10-03 20:34:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def cdp_ok(port):
|
|
|
|
|
try:
|
|
|
|
|
ts = json.load(urllib.request.urlopen(
|
|
|
|
|
"http://127.0.0.1:%s/json/list" % port, timeout=5))
|
|
|
|
|
return any(t.get("type") == "page" for t in ts)
|
|
|
|
|
except Exception:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
p = argparse.ArgumentParser()
|
|
|
|
|
p.add_argument("--node", required=True)
|
|
|
|
|
p.add_argument("--service", required=True)
|
|
|
|
|
p.add_argument("--id-type", required=True)
|
|
|
|
|
p.add_argument("--step", required=True, choices=["initiate", "submit"])
|
|
|
|
|
p.add_argument("--dry-run", action="store_true")
|
2026-10-03 21:30:32 +00:00
|
|
|
p.add_argument("--account-name", default=None,
|
|
|
|
|
help="display-name hint for multi-account selection")
|
2026-10-03 20:34:33 +00:00
|
|
|
args = p.parse_args()
|
|
|
|
|
|
|
|
|
|
if args.service != "muse" or args.id_type != "email":
|
|
|
|
|
print("ERROR: unsupported service/id_type "
|
|
|
|
|
"(muse+email only for now)", file=sys.stderr)
|
|
|
|
|
return 1
|
2026-10-03 21:28:36 +00:00
|
|
|
port = _load_registry().port_for(args.node)
|
2026-10-03 20:34:33 +00:00
|
|
|
if not port:
|
2026-10-03 21:28:36 +00:00
|
|
|
print("ERROR: unknown node '%s' (not in NODES.md registry)"
|
|
|
|
|
% args.node, file=sys.stderr)
|
2026-10-03 20:34:33 +00:00
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
if args.dry_run:
|
|
|
|
|
# Walk the chain without sending anything: netns + CDP + page.
|
|
|
|
|
if cdp_ok(port):
|
|
|
|
|
print("dry-run ok: node=%s cdp=%s reachable, page present"
|
|
|
|
|
% (args.node, port))
|
|
|
|
|
return 0
|
|
|
|
|
print("ERROR: CDP unreachable on %s" % port, file=sys.stderr)
|
|
|
|
|
return 1
|
|
|
|
|
|
2026-10-04 16:37:26 +00:00
|
|
|
lines = sys.stdin.read().splitlines()
|
|
|
|
|
identifier = lines[0].strip() if lines else ""
|
|
|
|
|
code = lines[1].strip() if len(lines) > 1 else ""
|
|
|
|
|
|
|
|
|
|
|
2026-10-03 20:34:33 +00:00
|
|
|
if not identifier:
|
|
|
|
|
print("ERROR: no identifier on stdin", file=sys.stderr)
|
|
|
|
|
return 1
|
|
|
|
|
|
2026-10-03 21:28:36 +00:00
|
|
|
cmd = [sys.executable, SIGNIN, "--node", args.node, "--email", identifier]
|
2026-10-03 21:30:32 +00:00
|
|
|
if args.account_name:
|
|
|
|
|
cmd += ["--account-name", args.account_name]
|
2026-10-03 20:34:33 +00:00
|
|
|
if args.step == "submit":
|
|
|
|
|
if not code:
|
|
|
|
|
print("ERROR: no code on stdin", file=sys.stderr)
|
|
|
|
|
return 1
|
|
|
|
|
cmd += ["--otp", code]
|
2026-10-04 17:48:54 +00:00
|
|
|
try:
|
|
|
|
|
r = subprocess.run(cmd, capture_output=True, text=True,
|
|
|
|
|
timeout=220)
|
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
|
# NB: TimeoutExpired str() includes the argv (with secrets) -
|
|
|
|
|
# never let it reach stderr uncaught.
|
|
|
|
|
print("ERROR: signin step timed out", file=sys.stderr)
|
|
|
|
|
return 1
|
2026-10-04 16:37:26 +00:00
|
|
|
# Propagate the signin script's contract:
|
2026-10-04 17:48:54 +00:00
|
|
|
# 0 = done, 2 = OTP prompt reached, 3 = NEEDS_HUMAN, 4 = NEEDS_SIGNUP.
|
|
|
|
|
# Scrub: the signin script echoes the identifier/code in progress
|
|
|
|
|
# output; redact before passthrough (server scrubs too, defense
|
|
|
|
|
# in depth).
|
|
|
|
|
sys.stdout.write(_scrub(r.stdout, identifier, code))
|
|
|
|
|
sys.stderr.write(_scrub(r.stderr, identifier, code))
|
2026-10-04 16:37:26 +00:00
|
|
|
|
|
|
|
|
if r.returncode == 4:
|
|
|
|
|
log_entry = {
|
|
|
|
|
"ts": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
|
|
|
"type": "onboarding_needs_signup",
|
|
|
|
|
"node": args.node,
|
|
|
|
|
"service": args.service,
|
2026-10-04 17:48:54 +00:00
|
|
|
"email_masked": _mask_email(identifier),
|
2026-10-04 16:37:26 +00:00
|
|
|
"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
|
|
|
|
|
|
2026-10-03 20:34:33 +00:00
|
|
|
return r.returncode
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
sys.exit(main())
|