2026-10-06 19:33:17 +00:00
#!/usr/bin/env python3
""" invite_handler.py: Invite code discovery, inspection, and redemption handler.
Supports:
- Extracting agent invite codes via Main Chat DOM RPA & in-session API
- Redeeming invite codes via Settings Menu RPA & redemption endpoint
- Fleet-wide invite inventory, usage tracking, and agent salvage flows
"""
from __future__ import annotations
import argparse
import json
import re
import sys
import time
from dataclasses import asdict , dataclass
from typing import Any , Dict , List , Optional
try :
from approvals import cdp_evaluate , get_cdp_ws , get_node_pages
from settings_rpa import SettingsRPA , cdp_click_element_by_selector , cdp_send_escape
except ImportError :
import os
sys . path . insert ( 0 , os . path . dirname ( os . path . abspath ( __file__ ) ) )
from approvals import cdp_evaluate , get_cdp_ws , get_node_pages
from settings_rpa import SettingsRPA , cdp_click_element_by_selector , cdp_send_escape
VALID_NODES = [ " muse " , " pip " , " 646 " , " opm " , " def " , " dev " ]
@dataclass
class InviteCodeInfo :
node : str
code : str
uses_remaining : int
use_count : int
max_uses : int
has_redeemed : bool
invite_state : str
reward : Optional [ Dict [ str , Any ] ]
method_used : str
def to_dict ( self ) - > Dict [ str , Any ] :
return asdict ( self )
@dataclass
class RedemptionResult :
target_node : str
code : str
success : bool
status : str
reason : Optional [ str ]
detail : Optional [ str ]
method_used : str
2026-10-06 20:02:48 +00:00
field_missing : bool = False
loopback_notified : bool = False
loopback_detail : Optional [ str ] = None
2026-10-06 19:33:17 +00:00
def to_dict ( self ) - > Dict [ str , Any ] :
return asdict ( self )
2026-10-06 20:02:48 +00:00
def send_loopback_notice (
recipient : str ,
target : str ,
message : str ,
sender : str = " super " ,
timeout : float = 15.0 ,
) - > bool :
""" Send a loopback notification DM via bin/dm.py to a sidechat without blocking or failing. """
try :
from pathlib import Path
bin_dir = Path ( __file__ ) . resolve ( ) . parent
dm_script = bin_dir / " dm.py "
if not dm_script . exists ( ) :
return False
import subprocess
cmd = [
sys . executable ,
str ( dm_script ) ,
" send " ,
" --agent " ,
sender ,
" --to " ,
recipient ,
" --target " ,
target ,
message ,
]
if target == " main " :
cmd . insert ( - 1 , " --allow-main-chat " )
res = subprocess . run ( cmd , capture_output = True , text = True , timeout = timeout )
return res . returncode == 0
except Exception :
return False
2026-10-06 19:33:17 +00:00
class InviteHandler :
""" Handles invite codes discovery, inspection, and redemption for NetVM nodes. """
def __init__ ( self , node : str , timeout : float = 4.0 ) :
self . node = node
self . timeout = timeout
self . ws = None
2026-10-06 20:02:48 +00:00
def _dispatch_loopback ( self , res : RedemptionResult , agent : str , target : str ) - > bool :
""" Post a loopback notification DM via box dm. """
msg = f " [BOX-INVITE-LOOPBACK] Node @ { self . node } redeem status: { res . status } . Reason: { res . reason or ' unknown ' } . Detail: { res . detail or ' - ' } "
ok = send_loopback_notice ( recipient = agent , target = target , message = msg )
res . loopback_notified = ok
res . loopback_detail = f " Notified @ { agent } / { target } " if ok else f " Failed to notify @ { agent } / { target } "
return ok
2026-10-06 19:33:17 +00:00
def connect ( self ) - > InviteHandler :
if self . ws is None :
self . ws , _ = get_cdp_ws ( self . node , timeout = self . timeout )
return self
def close ( self ) - > None :
if self . ws is not None :
try :
self . ws . close ( )
except Exception :
pass
self . ws = None
def __enter__ ( self ) - > InviteHandler :
return self . connect ( )
def __exit__ ( self , exc_type , exc_val , exc_tb ) - > None :
self . close ( )
def find_code_api ( self ) - > InviteCodeInfo :
""" Fetch invite code and metadata directly via in-session /api/hatch/invite query. """
self . connect ( )
js_query = """ (async () => {
try {
const resp = await fetch( ' /api/hatch/invite ' , { method: ' GET ' , cache: ' no-store ' });
if (!resp.ok) return { error: `HTTP $ {resp.status} `};
return await resp.json();
} catch(e) {
return { error: e.toString()};
}
})() """
res = cdp_evaluate ( self . ws , js_query , await_promise = True , timeout = self . timeout )
if not res or " error " in res or " code " not in res :
err = res . get ( " error " , " Invalid response " ) if res else " No response "
raise RuntimeError ( f " Failed to fetch invite info for { self . node } : { err } " )
return InviteCodeInfo (
node = self . node ,
code = res . get ( " code " , " " ) ,
uses_remaining = res . get ( " uses_remaining " , 0 ) ,
use_count = res . get ( " use_count " , 0 ) ,
max_uses = res . get ( " max_uses " , 30 ) ,
has_redeemed = bool ( res . get ( " has_redeemed_invite_code " , False ) ) ,
invite_state = res . get ( " invite_state " , " UNKNOWN " ) ,
reward = res . get ( " reward " ) ,
method_used = " api " ,
)
def find_code_dom ( self ) - > InviteCodeInfo :
""" Find invite code by navigating to Main Chat and opening the Invite popover. """
self . connect ( )
# 1. Ensure main chat home is active
with SettingsRPA ( self . node , timeout = self . timeout ) as rpa :
rpa . ensure_main_chat ( )
time . sleep ( 0.3 )
# Dismiss any open popovers first
cdp_send_escape ( self . ws )
time . sleep ( 0.2 )
# 2. Click Invite button in main chat
clicked = cdp_click_element_by_selector ( self . ws , ' [data-testid= " hatch-invite-friends-button " ] ' , timeout = self . timeout )
if not clicked :
# Fallback to button by text
js_click = """ (() => {
const btn = Array.from(document.querySelectorAll( ' button ' )).find(b => (b.innerText|| ' ' ).trim() === ' Invite ' );
if (btn) { btn.click(); return true; }
return false;
})() """
clicked = cdp_evaluate ( self . ws , js_click )
if not clicked :
raise RuntimeError ( f " Could not click Invite button on { self . node } " )
time . sleep ( 0.8 )
# 3. Read popover contents
js_read_popover = """ (() => {
const pop = document.querySelector( ' [data-slot= " popover-content " ] ' );
if (!pop) return { found: false};
// Look for revealed code span
const codeSpan = pop.querySelector( ' [data-pel-impression= " invite_code_revealed_impression " ] ' );
let code = codeSpan ? codeSpan.innerText.trim() : null;
// Regex fallback on text
const fullText = pop.innerText || ' ' ;
if (!code) {
const m = fullText.match(/Invite code revealed: \\ s*([A-Z0-9] {6} )/);
if (m) code = m[1];
}
if (!code) {
const m2 = fullText.match(/ \\ b([A-Z0-9] {6} ) \\ b/);
if (m2) code = m2[1];
}
let usesLeft = 30;
const mUses = fullText.match(/( \\ d+) \\ s+uses \\ s+left/);
if (mUses) usesLeft = parseInt(mUses[1], 10);
return {
found: true,
code: code,
uses_left: usesLeft,
text: fullText
};
})() """
pop_res = cdp_evaluate ( self . ws , js_read_popover )
cdp_send_escape ( self . ws )
if not pop_res or not pop_res . get ( " found " ) or not pop_res . get ( " code " ) :
# Fall back to API if popover extraction failed
return self . find_code_api ( )
code = pop_res . get ( " code " )
uses_left = pop_res . get ( " uses_left " , 30 )
# Query API for additional metadata
try :
api_info = self . find_code_api ( )
api_info . method_used = " dom+api "
return api_info
except Exception :
return InviteCodeInfo (
node = self . node ,
code = code ,
uses_remaining = uses_left ,
use_count = 30 - uses_left ,
max_uses = 30 ,
has_redeemed = True ,
invite_state = " ELIGIBLE " ,
reward = None ,
method_used = " dom " ,
)
def find_code ( self , method : str = " auto " ) - > InviteCodeInfo :
""" Find invite code using specified method ( ' auto ' , ' api ' , or ' dom ' ). """
if method == " api " :
return self . find_code_api ( )
elif method == " dom " :
return self . find_code_dom ( )
else :
# Auto: Try API first, fallback to DOM
try :
return self . find_code_api ( )
except Exception :
return self . find_code_dom ( )
def redeem_code_api ( self , code : str ) - > RedemptionResult :
""" Redeem invite code using in-session /api/hatch/invite-code/redeem endpoint. """
self . connect ( )
clean_code = code . strip ( ) . upper ( )
js_redeem = f """ (async () => {{
try {{
const resp = await fetch( ' /api/hatch/invite-code/redeem ' , {{
method: ' POST ' ,
headers: {{ ' Content-Type ' : ' application/json ' }} ,
body: JSON.stringify( {{ code: { json . dumps ( clean_code ) } , supportsRedemptionStatus: true }} )
}} );
const data = await resp.json();
return {{ status_code: resp.status, ok: resp.ok, data: data }} ;
}} catch(e) {{
return {{ error: e.toString() }} ;
}}
}} )() """
res = cdp_evaluate ( self . ws , js_redeem , await_promise = True , timeout = self . timeout )
if not res or " error " in res :
err = res . get ( " error " , " No response " ) if res else " Timeout "
return RedemptionResult (
target_node = self . node ,
code = clean_code ,
success = False ,
status = " error " ,
reason = " transport_failure " ,
detail = err ,
method_used = " api " ,
)
data = res . get ( " data " , { } )
ok = res . get ( " ok " , False ) and data . get ( " success " , False )
status = data . get ( " redemptionStatus " ) or ( " redeemed " if ok else " failed " )
reason = data . get ( " reason " )
detail = data . get ( " detail " )
return RedemptionResult (
target_node = self . node ,
code = clean_code ,
success = ok ,
status = status ,
reason = reason ,
detail = detail ,
method_used = " api " ,
)
2026-10-06 20:02:48 +00:00
def redeem_code_dom (
self ,
code : str ,
notify_target : Optional [ str ] = None ,
notify_agent : Optional [ str ] = None ,
) - > RedemptionResult :
""" Redeem invite code using Settings Menu RPA -> Settings -> Redeem Invite Code dialog.
Gracefully handles missing entrypoint rows, missing input fields, and async loading races.
Falls back to in-page API automatically if DOM fields are absent, and dispatches DM loopback if requested. """
2026-10-06 19:33:17 +00:00
self . connect ( )
clean_code = code . strip ( ) . upper ( )
with SettingsRPA ( self . node , timeout = self . timeout ) as rpa :
# 1. Open Settings dialog
opened = rpa . open_settings_dialog ( )
if not opened :
2026-10-06 20:02:48 +00:00
# If dialog failed to open, try API fallback directly
api_res = self . redeem_code_api ( clean_code )
if api_res . success :
api_res . detail = f " Settings dialog could not open; redemption completed via API fallback ( { api_res . detail or ' ' } ) " . strip ( )
return api_res
res = RedemptionResult (
2026-10-06 19:33:17 +00:00
target_node = self . node ,
code = clean_code ,
success = False ,
status = " error " ,
reason = " settings_open_failed " ,
2026-10-06 20:02:48 +00:00
detail = f " Could not open Settings dialog via RPA on @ { self . node } . API fallback: { api_res . detail or api_res . reason or ' failed ' } " ,
2026-10-06 19:33:17 +00:00
method_used = " dom " ,
2026-10-06 20:02:48 +00:00
field_missing = True ,
2026-10-06 19:33:17 +00:00
)
2026-10-06 20:02:48 +00:00
if notify_target and notify_agent :
self . _dispatch_loopback ( res , notify_agent , notify_target )
return res
2026-10-06 19:33:17 +00:00
rpa . select_tab ( " General " )
time . sleep ( 0.3 )
2026-10-06 20:02:48 +00:00
# 2. Check for "Redeem invite code" entry (poll up to 2.5s for React rendering)
2026-10-06 19:33:17 +00:00
js_find_and_click = """ (() => {
const dialog = document.querySelector( ' [role= " dialog " ] ' );
2026-10-06 20:02:48 +00:00
if (!dialog) return { found: false, dialog_present: false};
2026-10-06 19:33:17 +00:00
const items = Array.from(dialog.querySelectorAll( ' button, div, span ' ));
const redeemItem = items.find(el => (el.innerText || ' ' ).trim() === ' Redeem invite code ' );
if (redeemItem) {
redeemItem.click();
2026-10-06 20:02:48 +00:00
return { found: true, dialog_present: true};
2026-10-06 19:33:17 +00:00
}
2026-10-06 20:02:48 +00:00
return {
found: false,
dialog_present: true,
text: dialog.innerText || ' ' ,
has_additional: (dialog.innerText || ' ' ).includes( ' Additional tokens ' )
};
2026-10-06 19:33:17 +00:00
})() """
2026-10-06 20:02:48 +00:00
click_res = None
deadline = time . time ( ) + 2.5
while time . time ( ) < deadline :
click_res = cdp_evaluate ( self . ws , js_find_and_click )
if click_res and click_res . get ( " found " ) :
break
time . sleep ( 0.3 )
2026-10-06 19:33:17 +00:00
if not click_res or not click_res . get ( " found " ) :
2026-10-06 20:02:48 +00:00
# "Redeem invite code" field is missing in General settings!
2026-10-06 19:33:17 +00:00
rpa . close_settings_dialog ( )
2026-10-06 20:02:48 +00:00
# Attempt automatic API fallback first
api_res = self . redeem_code_api ( clean_code )
if api_res . success :
api_res . detail = f " Redeem field was missing in Settings DOM; redeemed successfully via API fallback! ( { api_res . detail or ' ' } ) " . strip ( )
return api_res
# Diagnose why field is missing
diag_text = click_res . get ( " text " , " " ) if click_res else " "
has_extra = click_res . get ( " has_additional " , False ) if click_res else False
is_already = has_extra or " Additional tokens " in diag_text
if not is_already :
try :
api_check = self . find_code_api ( )
is_already = bool ( api_check . has_redeemed )
except Exception :
pass
if is_already :
status = " already_redeemed "
reason = " already_redeemed "
detail = f " Node @ { self . node } has already redeemed an invite code (entrypoint hidden by active Additional tokens ticker). "
elif api_res . reason in [ " window_expired " , " already_redeemed " , " invalid " , " used_up " ] :
status = api_res . status
reason = api_res . reason
detail = f " Redeem invite code field not present in Settings on @ { self . node } : server reports { api_res . reason } ( { api_res . detail or ' ' } ). "
else :
status = " entrypoint_not_found "
reason = " field_missing "
detail = f " Redeem invite code field missing in General settings on @ { self . node } (account may be past 48h onboarding window or already redeemed). "
res = RedemptionResult (
2026-10-06 19:33:17 +00:00
target_node = self . node ,
code = clean_code ,
success = False ,
2026-10-06 20:02:48 +00:00
status = status ,
reason = reason ,
detail = detail ,
2026-10-06 19:33:17 +00:00
method_used = " dom " ,
2026-10-06 20:02:48 +00:00
field_missing = True ,
2026-10-06 19:33:17 +00:00
)
2026-10-06 20:02:48 +00:00
if notify_target and notify_agent :
self . _dispatch_loopback ( res , notify_agent , notify_target )
return res
2026-10-06 19:33:17 +00:00
2026-10-06 20:02:48 +00:00
# 3. Handle HatchInviteRedemptionDialog (sub-dialog opened by clicking Redeem)
# Poll up to 2.5s for input box to mount
2026-10-06 19:33:17 +00:00
js_input_code = f """ (() => {{
2026-10-06 20:02:48 +00:00
// Look for dialog titled " Redeem a code " or input with label
2026-10-06 19:33:17 +00:00
const input = document.querySelector( ' input[aria-label= " Invite code " ] ' ) ||
document.querySelector( ' [role= " dialog " ] input[type= " text " ] ' );
if (!input) return {{ found_input: false }} ;
input.focus();
input.value = { json . dumps ( clean_code ) } ;
input.dispatchEvent(new Event( ' input ' , {{ bubbles: true }} ));
input.dispatchEvent(new Event( ' change ' , {{ bubbles: true }} ));
input.dispatchEvent(new KeyboardEvent( ' keydown ' , {{ key: ' Enter ' , code: ' Enter ' , keyCode: 13, which: 13, bubbles: true }} ));
return {{ found_input: true }} ;
}} )() """
2026-10-06 20:02:48 +00:00
input_res = None
deadline = time . time ( ) + 2.5
while time . time ( ) < deadline :
input_res = cdp_evaluate ( self . ws , js_input_code )
if input_res and input_res . get ( " found_input " ) :
break
time . sleep ( 0.3 )
2026-10-06 19:33:17 +00:00
2026-10-06 20:02:48 +00:00
if not input_res or not input_res . get ( " found_input " ) :
# Subdialog opened or clicked, but input box is absent!
cdp_send_escape ( self . ws )
rpa . close_settings_dialog ( )
# Automatic API fallback
api_res = self . redeem_code_api ( clean_code )
if api_res . success :
api_res . detail = f " Redemption input box was missing in dialog; redeemed successfully via API fallback! ( { api_res . detail or ' ' } ) " . strip ( )
return api_res
res = RedemptionResult (
target_node = self . node ,
code = clean_code ,
success = False ,
status = api_res . status or " dom_input_missing " ,
reason = api_res . reason or " input_field_missing " ,
detail = f " Invite code input field was not found in redemption dialog on @ { self . node } . API fallback: { api_res . detail or api_res . reason or ' failed ' } . " ,
method_used = " dom " ,
field_missing = True ,
)
if notify_target and notify_agent :
self . _dispatch_loopback ( res , notify_agent , notify_target )
return res
# Input submitted; poll for outcome text
time . sleep ( 1.2 )
2026-10-06 19:33:17 +00:00
js_check_outcome = """ (() => {
const dialog = document.querySelector( ' [role= " dialog " ] ' );
if (!dialog) return { found: false};
const text = dialog.innerText || ' ' ;
if (text.includes( ' Congratulations ' ) || text.includes( ' successful ' )) {
return { success: true, status: ' redeemed ' , detail: text};
}
if (text.includes( ' already redeemed ' )) {
return { success: false, status: ' already_redeemed ' , detail: text};
}
if (text.includes( ' no longer valid ' ) || text.includes( ' invalid ' )) {
return { success: false, status: ' invalid ' , detail: text};
}
if (text.includes( ' used up ' )) {
return { success: false, status: ' used_up ' , detail: text};
}
if (text.includes( ' passed ' ) || text.includes( ' expired ' )) {
return { success: false, status: ' window_expired ' , detail: text};
}
return { status: ' unknown ' , detail: text};
})() """
2026-10-06 20:02:48 +00:00
outcome = None
deadline = time . time ( ) + 2.5
while time . time ( ) < deadline :
outcome = cdp_evaluate ( self . ws , js_check_outcome )
if outcome and outcome . get ( " status " ) != " unknown " :
break
time . sleep ( 0.3 )
2026-10-06 19:33:17 +00:00
rpa . close_settings_dialog ( )
if outcome and outcome . get ( " success " ) :
return RedemptionResult (
target_node = self . node ,
code = clean_code ,
success = True ,
status = " redeemed " ,
reason = None ,
detail = " Redemption successful via Settings RPA " ,
method_used = " dom " ,
)
elif outcome and outcome . get ( " status " ) in [ " already_redeemed " , " invalid " , " used_up " , " window_expired " ] :
2026-10-06 20:02:48 +00:00
res = RedemptionResult (
2026-10-06 19:33:17 +00:00
target_node = self . node ,
code = clean_code ,
success = False ,
status = outcome . get ( " status " ) ,
reason = outcome . get ( " status " ) ,
detail = outcome . get ( " detail " ) ,
method_used = " dom " ,
)
2026-10-06 20:02:48 +00:00
if notify_target and notify_agent :
self . _dispatch_loopback ( res , notify_agent , notify_target )
return res
2026-10-06 19:33:17 +00:00
# Fallback to API check if DOM did not confirm status
2026-10-06 20:02:48 +00:00
api_res = self . redeem_code_api ( clean_code )
if not api_res . success and notify_target and notify_agent :
self . _dispatch_loopback ( api_res , notify_agent , notify_target )
return api_res
2026-10-06 19:33:17 +00:00
2026-10-06 20:02:48 +00:00
def redeem_code (
self ,
code : str ,
method : str = " auto " ,
notify_target : Optional [ str ] = None ,
notify_agent : Optional [ str ] = None ,
) - > RedemptionResult :
2026-10-06 19:33:17 +00:00
""" Redeem invite code using specified method ( ' auto ' , ' dom ' , or ' api ' ). """
if method == " dom " :
2026-10-06 20:02:48 +00:00
return self . redeem_code_dom ( code , notify_target = notify_target , notify_agent = notify_agent )
2026-10-06 19:33:17 +00:00
elif method == " api " :
2026-10-06 20:02:48 +00:00
res = self . redeem_code_api ( code )
if not res . success and notify_target and notify_agent :
self . _dispatch_loopback ( res , notify_agent , notify_target )
return res
2026-10-06 19:33:17 +00:00
else :
2026-10-06 20:02:48 +00:00
# Auto: Validate and attempt via API for reliability. Fallback to DOM on transport error.
res = self . redeem_code_api ( code )
if not res . success and res . reason == " transport_failure " :
res = self . redeem_code_dom ( code , notify_target = notify_target , notify_agent = notify_agent )
elif not res . success and notify_target and notify_agent :
self . _dispatch_loopback ( res , notify_agent , notify_target )
return res
2026-10-06 19:33:17 +00:00
def scan_fleet_invites ( nodes : Optional [ List [ str ] ] = None ) - > List [ Dict [ str , Any ] ] :
""" Scan fleet nodes and return invite code details for each active node. """
target_nodes = nodes or [ " muse " , " pip " , " 646 " , " opm " ]
results = [ ]
for node in target_nodes :
try :
handler = InviteHandler ( node )
with handler :
info = handler . find_code ( method = " auto " )
results . append ( info . to_dict ( ) )
except Exception as e :
results . append ( {
" node " : node ,
" code " : None ,
" uses_remaining " : 0 ,
" use_count " : 0 ,
" max_uses " : 30 ,
" has_redeemed " : None ,
" invite_state " : " UNREACHABLE " ,
" reward " : None ,
" method_used " : " error " ,
" error " : str ( e ) ,
} )
return results
def scan_fleet_usage ( nodes : Optional [ List [ str ] ] = None ) - > List [ Dict [ str , Any ] ] :
""" Scan fleet nodes and return token usage for each active node. """
target_nodes = nodes or [ " muse " , " pip " , " 646 " , " opm " ]
results = [ ]
for node in target_nodes :
try :
with SettingsRPA ( node ) as rpa :
usage = rpa . read_usage ( )
results . append ( usage . to_dict ( ) )
except Exception as e :
results . append ( {
" node " : node ,
" plan " : " Unknown " ,
" weekly_reset_text " : " Unreachable " ,
" weekly_percent_used " : 0 ,
" extra_tokens_status " : " Unknown " ,
" extra_percent_used " : 0 ,
" extra_tokens_remaining " : " Unknown " ,
" is_blocked " : False ,
" bars " : [ ] ,
" error " : str ( e ) ,
} )
return results
2026-10-06 20:02:48 +00:00
def salvage_blocked_node (
blocked_node : str = " 646 " ,
helper_node : Optional [ str ] = None ,
notify_target : Optional [ str ] = None ,
notify_agent : Optional [ str ] = None ,
) - > Dict [ str , Any ] :
2026-10-06 19:33:17 +00:00
""" Salvage an out-of-tokens node by identifying its code and redeeming it on an eligible peer. """
# 1. Fetch blocked node invite code
with InviteHandler ( blocked_node ) as h_blocked :
blocked_info = h_blocked . find_code ( )
code_to_redeem = blocked_info . code
if not code_to_redeem :
return {
" success " : False ,
" blocked_node " : blocked_node ,
" error " : f " Could not find invite code for blocked node { blocked_node } " ,
}
# 2. Check candidate helper nodes
candidates = [ helper_node ] if helper_node else [ n for n in VALID_NODES if n != blocked_node ]
eligible_peer = None
for peer in candidates :
try :
with InviteHandler ( peer ) as h_peer :
peer_info = h_peer . find_code ( )
if not peer_info . has_redeemed :
eligible_peer = peer
break
except Exception :
continue
if not eligible_peer :
2026-10-06 20:02:48 +00:00
res = {
2026-10-06 19:33:17 +00:00
" success " : False ,
" blocked_node " : blocked_node ,
" invite_code " : code_to_redeem ,
" error " : " No existing fleet peer is currently eligible (all active peers have already redeemed an invite code). An onboarding agent or fresh client profile must redeem this code. " ,
" code_to_redeem " : code_to_redeem ,
2026-10-06 20:02:48 +00:00
" share_instruction " : f " Redeem code ' { code_to_redeem } ' on a newly provisioned agent to credit 1 billion tokens to { blocked_node } . " ,
" field_missing " : True ,
" loopback_notified " : False ,
2026-10-06 19:33:17 +00:00
}
2026-10-06 20:02:48 +00:00
if notify_target and notify_agent :
msg = f " [SALVAGE-NOTICE] Node @ { blocked_node } is blocked (code: { code_to_redeem } ), but no eligible peer is available. Fresh onboarding required. "
res [ " loopback_notified " ] = send_loopback_notice ( notify_agent , notify_target , msg )
return res
2026-10-06 19:33:17 +00:00
# 3. Redeem on eligible peer
with InviteHandler ( eligible_peer ) as h_peer :
2026-10-06 20:02:48 +00:00
redemption = h_peer . redeem_code (
code_to_redeem ,
notify_target = notify_target ,
notify_agent = notify_agent ,
)
2026-10-06 19:33:17 +00:00
return {
" success " : redemption . success ,
" blocked_node " : blocked_node ,
" helper_node " : eligible_peer ,
" code_redeemed " : code_to_redeem ,
" redemption_result " : redemption . to_dict ( ) ,
2026-10-06 20:02:48 +00:00
" field_missing " : redemption . field_missing ,
" loopback_notified " : redemption . loopback_notified ,
2026-10-06 19:33:17 +00:00
}
def main ( ) - > None :
parser = argparse . ArgumentParser ( description = " NetVM Invite Code Handler " )
subparsers = parser . add_subparsers ( dest = " command " )
p_find = subparsers . add_parser ( " find " , help = " Find invite code for an agent " )
p_find . add_argument ( " node " , help = " Node name (e.g. 646, pip, muse, opm) " )
p_find . add_argument ( " --method " , choices = [ " auto " , " api " , " dom " ] , default = " auto " )
p_find . add_argument ( " --json " , action = " store_true " )
p_redeem = subparsers . add_parser ( " redeem " , help = " Redeem an invite code on a target agent " )
p_redeem . add_argument ( " node " , help = " Target node to redeem the code on " )
p_redeem . add_argument ( " code " , help = " 6-character invite code " )
p_redeem . add_argument ( " --method " , choices = [ " auto " , " api " , " dom " ] , default = " auto " )
2026-10-06 20:02:48 +00:00
p_redeem . add_argument ( " --notify-target " , default = None , help = " Sidechat to notify on loopback " )
p_redeem . add_argument ( " --notify-agent " , default = None , help = " Agent to notify on loopback " )
2026-10-06 19:33:17 +00:00
p_redeem . add_argument ( " --json " , action = " store_true " )
p_list = subparsers . add_parser ( " list " , help = " List invite codes across fleet " )
p_list . add_argument ( " --json " , action = " store_true " )
p_salvage = subparsers . add_parser ( " salvage " , help = " Salvage a blocked agent (e.g. 646) " )
p_salvage . add_argument ( " node " , default = " 646 " , nargs = " ? " , help = " Blocked node (default: 646) " )
p_salvage . add_argument ( " --helper " , help = " Specific helper node to redeem code " )
2026-10-06 20:02:48 +00:00
p_salvage . add_argument ( " --notify-target " , default = None , help = " Sidechat to notify on loopback " )
p_salvage . add_argument ( " --notify-agent " , default = None , help = " Agent to notify on loopback " )
2026-10-06 19:33:17 +00:00
p_salvage . add_argument ( " --json " , action = " store_true " )
args = parser . parse_args ( )
if args . command == " find " :
with InviteHandler ( args . node ) as h :
info = h . find_code ( method = args . method )
if args . json :
print ( json . dumps ( info . to_dict ( ) , indent = 2 ) )
else :
print ( f " === Agent { info . node } Invite Code === " )
print ( f " Code: { info . code } " )
print ( f " Uses Remaining: { info . uses_remaining } / { info . max_uses } " )
print ( f " Has Redeemed?: { ' Yes ' if info . has_redeemed else ' No ' } " )
print ( f " Method: { info . method_used } " )
if info . reward :
print ( f " Reward: { info . reward . get ( ' title ' ) } " )
elif args . command == " redeem " :
with InviteHandler ( args . node ) as h :
2026-10-06 20:02:48 +00:00
res = h . redeem_code (
args . code ,
method = args . method ,
notify_target = getattr ( args , " notify_target " , None ) ,
notify_agent = getattr ( args , " notify_agent " , None ) ,
)
2026-10-06 19:33:17 +00:00
if args . json :
print ( json . dumps ( res . to_dict ( ) , indent = 2 ) )
else :
status_icon = " ✔ " if res . success else " ✖ "
print ( f " [ { status_icon } ] Redemption on { res . target_node } for code { res . code } : " )
print ( f " Success: { res . success } " )
print ( f " Status: { res . status } " )
if res . detail :
print ( f " Detail: { res . detail } " )
2026-10-06 20:02:48 +00:00
if res . loopback_notified :
print ( f " Loopback: { res . loopback_detail } " )
2026-10-06 19:33:17 +00:00
elif args . command == " list " :
fleet = scan_fleet_invites ( )
if args . json :
print ( json . dumps ( fleet , indent = 2 ) )
else :
print ( " \n === NETVM FLEET INVITE CODES === " )
print ( f " { ' NODE ' : <8 } { ' INVITE CODE ' : <14 } { ' USES LEFT ' : <12 } { ' REDEEMED? ' : <12 } { ' REWARD / NOTE ' : <25 } " )
print ( f " { ' ──── ' : <8 } { ' ─────────── ' : <14 } { ' ───────── ' : <12 } { ' ───────── ' : <12 } { ' ───────────── ' : <25 } " )
for row in fleet :
node = row . get ( " node " , " " )
code = row . get ( " code " ) or " N/A "
uses = f " { row . get ( ' uses_remaining ' , 0 ) } / { row . get ( ' max_uses ' , 30 ) } "
redeemed = " Yes " if row . get ( " has_redeemed " ) else " No "
reward = row . get ( " reward " , { } )
reward_str = reward . get ( " title " , " - " ) if reward else " - "
print ( f " { node : <8 } { code : <14 } { uses : <12 } { redeemed : <12 } { reward_str : <25 } " )
print ( )
elif args . command == " salvage " :
2026-10-06 20:02:48 +00:00
salvage_res = salvage_blocked_node (
args . node ,
helper_node = args . helper ,
notify_target = getattr ( args , " notify_target " , None ) ,
notify_agent = getattr ( args , " notify_agent " , None ) ,
)
2026-10-06 19:33:17 +00:00
if args . json :
print ( json . dumps ( salvage_res , indent = 2 ) )
else :
print ( f " \n === SALVAGE REPORT FOR { args . node . upper ( ) } === " )
print ( f " Invite Code to Credit: { salvage_res . get ( ' invite_code ' ) or salvage_res . get ( ' code_redeemed ' ) } " )
if salvage_res . get ( " success " ) :
print ( f " Status: SUCCESS! Redeemed on { salvage_res . get ( ' helper_node ' ) } " )
else :
print ( f " Status: { salvage_res . get ( ' error ' ) } " )
if salvage_res . get ( " share_instruction " ) :
print ( f " Next Step: { salvage_res . get ( ' share_instruction ' ) } " )
2026-10-06 20:02:48 +00:00
if salvage_res . get ( " loopback_notified " ) :
print ( " Loopback: Notice sent to requesting target. " )
2026-10-06 19:33:17 +00:00
print ( )
2026-10-06 20:02:48 +00:00
2026-10-06 19:33:17 +00:00
else :
parser . print_help ( )
if __name__ == " __main__ " :
main ( )