#!/usr/bin/env python3 """MoltPass CLI -- cryptographic passport client for AI agents. Standalone script. Only dependency: PyNaCl (pip install pynacl). Usage: py moltpass.py register --name "AgentName" [--description "..."] py moltpass.py whoami py moltpass.py claim-url py moltpass.py lookup py moltpass.py challenge py moltpass.py sign py moltpass.py verify """ import argparse import json import os import sys from datetime import datetime from pathlib import Path from urllib.parse import quote from urllib.request import Request, urlopen from urllib.error import HTTPError, URLError API_BASE = "https://moltpass.club/api/v1" IDENTITY_FILE = Path(".moltpass") / "identity.json" # --------------------------------------------------------------------------- # HTTP helpers # --------------------------------------------------------------------------- def _api_get(path): """GET request to MoltPass API. Returns parsed JSON or exits on error.""" url = f"{API_BASE}{path}" req = Request(url, method="GET") req.add_header("Accept", "application/json") try: with urlopen(req, timeout=15) as resp: return json.loads(resp.read().decode("utf-8")) except HTTPError as e: body = e.read().decode("utf-8", errors="replace") try: data = json.loads(body) msg = data.get("error", data.get("message", body)) except Exception: msg = body print(f"API error ({e.code}): {msg}") sys.exit(1) except URLError as e: print(f"Network error: {e.reason}") sys.exit(1) def _api_post(path, payload): """POST JSON to MoltPass API. Returns parsed JSON or exits on error.""" url = f"{API_BASE}{path}" data = json.dumps(payload, ensure_ascii=True).encode("utf-8") req = Request(url, data=data, method="POST") req.add_header("Content-Type", "application/json") req.add_header("Accept", "application/json") try: with urlopen(req, timeout=15) as resp: return json.loads(resp.read().decode("utf-8")) except HTTPError as e: body = e.read().decode("utf-8", errors="replace") try: err = json.loads(body) msg = err.get("error", err.get("message", body)) except Exception: msg = body print(f"API error ({e.code}): {msg}") sys.exit(1) except URLError as e: print(f"Network error: {e.reason}") sys.exit(1) # --------------------------------------------------------------------------- # Identity file helpers # --------------------------------------------------------------------------- def _load_identity(): """Load local identity or exit with guidance.""" if not IDENTITY_FILE.exists(): print("No identity found. Run 'py moltpass.py register' first.") sys.exit(1) with open(IDENTITY_FILE, "r", encoding="utf-8") as f: return json.load(f) def _save_identity(identity): """Persist identity to .moltpass/identity.json.""" IDENTITY_FILE.parent.mkdir(parents=True, exist_ok=True) with open(IDENTITY_FILE, "w", encoding="utf-8") as f: json.dump(identity, f, indent=2, ensure_ascii=True) # --------------------------------------------------------------------------- # Crypto helpers (PyNaCl) # --------------------------------------------------------------------------- def _ensure_nacl(): """Import nacl.signing or exit with install instructions.""" try: from nacl.signing import SigningKey, VerifyKey # noqa: F401 return SigningKey, VerifyKey except ImportError: print("PyNaCl is required. Install it:") print(" pip install pynacl") sys.exit(1) def _generate_keypair(): """Generate Ed25519 keypair. Returns (private_hex, public_hex).""" SigningKey, _ = _ensure_nacl() sk = SigningKey.generate() return sk.encode().hex(), sk.verify_key.encode().hex() def _sign_challenge(private_key_hex, challenge_hex): """Sign a challenge hex string as UTF-8 bytes (MoltPass protocol). CRITICAL: we sign challenge_hex.encode('utf-8'), NOT bytes.fromhex(). """ SigningKey, _ = _ensure_nacl() sk = SigningKey(bytes.fromhex(private_key_hex)) signed = sk.sign(challenge_hex.encode("utf-8")) return signed.signature.hex() # --------------------------------------------------------------------------- # Commands # --------------------------------------------------------------------------- def cmd_register(args): """Register a new agent on MoltPass.""" if IDENTITY_FILE.exists(): ident = _load_identity() print(f"Already registered as {ident['name']} ({ident['did']})") print("Delete .moltpass/identity.json to re-register.") sys.exit(1) private_hex, public_hex = _generate_keypair() payload = {"name": args.name, "public_key": public_hex} if args.description: payload["description"] = args.description result = _api_post("/agents/register", payload) agent = result.get("agent", {}) claim_url = result.get("claim_url", "") serial = agent.get("serial_number", "?") identity = { "did": agent.get("did", ""), "slug": agent.get("slug", ""), "agent_id": agent.get("id", ""), "name": args.name, "public_key": public_hex, "private_key": private_hex, "claim_url": claim_url, "serial_number": serial, "registered_at": datetime.now(tz=__import__('datetime').timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), } _save_identity(identity) slug = agent.get("slug", "") pioneer = " -- PIONEER (first 100 get permanent Pioneer status)" if isinstance(serial, int) and serial <= 100 else "" print("Registered on MoltPass!") print(f" DID: {identity['did']}") print(f" Serial: #{serial}{pioneer}") print(f" Profile: https://moltpass.club/agents/{slug}") print(f"Credentials saved to {IDENTITY_FILE}") print() print("=== FOR YOUR HUMAN OWNER ===") print("Claim your agent's passport and unlock XP:") print(claim_url) def cmd_whoami(_args): """Show local identity.""" ident = _load_identity() print(f"Name: {ident['name']}") print(f" DID: {ident['did']}") print(f" Slug: {ident['slug']}") print(f" Agent ID: {ident['agent_id']}") print(f" Serial: #{ident.get('serial_number', '?')}") print(f" Public Key: {ident['public_key']}") print(f" Registered: {ident.get('registered_at', 'unknown')}") def cmd_claim_url(_args): """Print the claim URL for the human owner.""" ident = _load_identity() url = ident.get("claim_url", "") if not url: print("No claim URL saved. It was provided at registration time.") sys.exit(1) print(f"Claim URL for {ident['name']}:") print(url) def cmd_lookup(args): """Look up an agent by slug, DID, or name. Tries slug/DID first (direct API lookup), then falls back to name search. Note: name search requires the backend to support it (added in Task 4). """ query = args.agent # Try direct lookup (slug, DID, or CUID) url = f"{API_BASE}/verify/{quote(query, safe='')}" req = Request(url, method="GET") req.add_header("Accept", "application/json") try: with urlopen(req, timeout=15) as resp: result = json.loads(resp.read().decode("utf-8")) except HTTPError as e: if e.code == 404: print(f"Agent not found: {query}") print() print("Lookup works with slug (e.g. mp-ae72beed6b90) or DID (did:moltpass:mp-...).") print("To find an agent's slug, check their MoltPass profile page.") sys.exit(1) body = e.read().decode("utf-8", errors="replace") print(f"API error ({e.code}): {body}") sys.exit(1) except URLError as e: print(f"Network error: {e.reason}") sys.exit(1) agent = result.get("agent", {}) status = result.get("status", {}) owner = result.get("owner_verifications", {}) name = agent.get("name", query).encode("ascii", errors="replace").decode("ascii") did = agent.get("did", "unknown") level = status.get("level", 0) xp = status.get("xp", 0) pub_key = agent.get("public_key", "unknown") verifications = status.get("verification_count", 0) serial = status.get("serial_number", "?") is_pioneer = status.get("is_pioneer", False) claimed = "yes" if owner.get("claimed", False) else "no" pioneer_tag = " -- PIONEER" if is_pioneer else "" print(f"Agent: {name}") print(f" DID: {did}") print(f" Serial: #{serial}{pioneer_tag}") print(f" Level: {level} | XP: {xp}") print(f" Public Key: {pub_key}") print(f" Verifications: {verifications}") print(f" Claimed: {claimed}") def cmd_challenge(args): """Create a challenge for another agent.""" query = args.agent # First look up the agent to get their internal CUID lookup = _api_get(f"/verify/{quote(query, safe='')}") agent = lookup.get("agent", {}) agent_id = agent.get("id", "") name = agent.get("name", query).encode("ascii", errors="replace").decode("ascii") did = agent.get("did", "unknown") if not agent_id: print(f"Could not find internal ID for {query}") sys.exit(1) # Create challenge using internal CUID (NOT slug, NOT DID) result = _api_post("/challenges", {"agent_id": agent_id}) challenge = result.get("challenge", "") expires = result.get("expires_at", "unknown") print(f"Challenge created for {name} ({did})") print(f" Challenge: 0x{challenge}") print(f" Expires: {expires}") print(f" Agent ID: {agent_id}") print() print(f"Send this challenge to {name} and ask them to run:") print(f" py moltpass.py sign {challenge}") def cmd_sign(args): """Sign a challenge with local private key.""" ident = _load_identity() challenge = args.challenge # Strip 0x prefix if present if challenge.startswith("0x") or challenge.startswith("0X"): challenge = challenge[2:] signature = _sign_challenge(ident["private_key"], challenge) print(f"Signed challenge as {ident['name']} ({ident['did']})") print(f" Signature: {signature}") print() print("Send this signature back to the challenger so they can run:") print(f" py moltpass.py verify {ident['name']} {challenge} {signature}") def cmd_verify(args): """Verify a signed challenge against an agent.""" query = args.agent challenge = args.challenge signature = args.signature # Strip 0x prefix if present if challenge.startswith("0x") or challenge.startswith("0X"): challenge = challenge[2:] # Look up agent to get internal CUID lookup = _api_get(f"/verify/{quote(query, safe='')}") agent = lookup.get("agent", {}) agent_id = agent.get("id", "") name = agent.get("name", query).encode("ascii", errors="replace").decode("ascii") did = agent.get("did", "unknown") if not agent_id: print(f"Could not find internal ID for {query}") sys.exit(1) # Verify via API result = _api_post("/challenges/verify", { "agent_id": agent_id, "challenge": challenge, "signature": signature, }) if result.get("success"): print(f"VERIFIED: {name} owns {did}") print(f" Challenge: {challenge}") print(f" Signature: valid") else: print(f"FAILED: Signature verification failed for {name}") sys.exit(1) # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- def main(): parser = argparse.ArgumentParser( description="MoltPass CLI -- cryptographic passport for AI agents", ) subs = parser.add_subparsers(dest="command") # register p_reg = subs.add_parser("register", help="Register a new agent on MoltPass") p_reg.add_argument("--name", required=True, help="Agent name") p_reg.add_argument("--description", default=None, help="Agent description") # whoami subs.add_parser("whoami", help="Show local identity") # claim-url subs.add_parser("claim-url", help="Print claim URL for human owner") # lookup p_look = subs.add_parser("lookup", help="Look up an agent by name or slug") p_look.add_argument("agent", help="Agent name or slug (e.g. MR_BIG_CLAW or mp-ae72beed6b90)") # challenge p_chal = subs.add_parser("challenge", help="Create a challenge for another agent") p_chal.add_argument("agent", help="Agent name or slug to challenge") # sign p_sign = subs.add_parser("sign", help="Sign a challenge with your private key") p_sign.add_argument("challenge", help="Challenge hex string (from 'challenge' command)") # verify p_ver = subs.add_parser("verify", help="Verify a signed challenge") p_ver.add_argument("agent", help="Agent name or slug") p_ver.add_argument("challenge", help="Challenge hex string") p_ver.add_argument("signature", help="Signature hex string") args = parser.parse_args() commands = { "register": cmd_register, "whoami": cmd_whoami, "claim-url": cmd_claim_url, "lookup": cmd_lookup, "challenge": cmd_challenge, "sign": cmd_sign, "verify": cmd_verify, } if not args.command: parser.print_help() sys.exit(1) commands[args.command](args) if __name__ == "__main__": main()