sync: scission owner/template + brain-template-export + BRAIN_MODE guard + /visualize scope filter + port orphelins fix
This commit is contained in:
193
scripts/brain-pair-client.py
Normal file
193
scripts/brain-pair-client.py
Normal file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
"""brain-pair client — laptop/new machine side (ADR-041)
|
||||
Scans LAN for a brain-pair server, sends code, receives config.
|
||||
|
||||
Usage: python3 brain-pair-client.py <brain_root> <code>
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
BRAIN_ROOT = sys.argv[1]
|
||||
CODE = sys.argv[2]
|
||||
BROADCAST_PORT = 7711 # UDP listen port
|
||||
SCAN_TIMEOUT = 30 # seconds to scan for server
|
||||
|
||||
|
||||
def get_ssh_pubkey():
|
||||
"""Read the local SSH public key."""
|
||||
for name in ["id_ed25519.pub", "id_rsa.pub", "id_ecdsa.pub"]:
|
||||
path = os.path.expanduser(f"~/.ssh/{name}")
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
return f.read().strip()
|
||||
return ""
|
||||
|
||||
|
||||
def get_local_machine():
|
||||
"""Read machine name from brain-compose.local.yml."""
|
||||
try:
|
||||
import yaml
|
||||
compose_path = os.path.join(BRAIN_ROOT, "brain-compose.local.yml")
|
||||
with open(compose_path) as f:
|
||||
compose = yaml.safe_load(f)
|
||||
return compose.get("machine", socket.gethostname())
|
||||
except Exception:
|
||||
return socket.gethostname()
|
||||
|
||||
|
||||
def scan_for_server():
|
||||
"""Listen for UDP broadcast from brain-pair server."""
|
||||
print(f"🔍 Scan du LAN pour brain-pair server ({SCAN_TIMEOUT}s)...")
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.settimeout(2)
|
||||
sock.bind(("0.0.0.0", BROADCAST_PORT))
|
||||
|
||||
start = time.time()
|
||||
while time.time() - start < SCAN_TIMEOUT:
|
||||
try:
|
||||
data, addr = sock.recvfrom(1024)
|
||||
msg = json.loads(data.decode())
|
||||
if msg.get("type") == "brain-pair":
|
||||
server_ip = msg["ip"]
|
||||
server_port = msg["port"]
|
||||
print(f" ✅ Serveur trouvé : {server_ip}:{server_port}")
|
||||
sock.close()
|
||||
return server_ip, server_port
|
||||
except socket.timeout:
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
sock.close()
|
||||
return None, None
|
||||
|
||||
|
||||
def do_handshake(server_ip, server_port, code, machine, ssh_pubkey):
|
||||
"""Connect to server, send code, receive config."""
|
||||
print(f"🤝 Handshake avec {server_ip}:{server_port}...")
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(15)
|
||||
sock.connect((server_ip, server_port))
|
||||
|
||||
request = json.dumps({
|
||||
"code": code,
|
||||
"machine": machine,
|
||||
"ssh_pubkey": ssh_pubkey,
|
||||
})
|
||||
sock.sendall(request.encode())
|
||||
|
||||
data = sock.recv(8192).decode()
|
||||
sock.close()
|
||||
|
||||
response = json.loads(data)
|
||||
return response
|
||||
|
||||
|
||||
def apply_config(response, server_ip):
|
||||
"""Apply received config to local brain-compose.local.yml."""
|
||||
import yaml
|
||||
|
||||
if response.get("status") != "ok":
|
||||
print(f"❌ Pairing refusé : {response.get('msg', 'unknown error')}")
|
||||
return False
|
||||
|
||||
server_machine = response["machine"]
|
||||
api_key = response.get("api_key")
|
||||
engine_port = response.get("brain_engine_port", 7700)
|
||||
|
||||
compose_path = os.path.join(BRAIN_ROOT, "brain-compose.local.yml")
|
||||
|
||||
# Read or create compose
|
||||
if os.path.exists(compose_path):
|
||||
with open(compose_path) as f:
|
||||
compose = yaml.safe_load(f) or {}
|
||||
else:
|
||||
compose = {
|
||||
"machine": get_local_machine(),
|
||||
"instances": {},
|
||||
"kernel_path": BRAIN_ROOT,
|
||||
}
|
||||
|
||||
# Add peer
|
||||
if "peers" not in compose:
|
||||
compose["peers"] = {}
|
||||
|
||||
compose["peers"][server_machine] = {
|
||||
"url": f"http://{server_ip}:{engine_port}",
|
||||
"active": True,
|
||||
}
|
||||
|
||||
# Inject API key if provided
|
||||
if api_key:
|
||||
instances = compose.get("instances", {})
|
||||
# Find or create active instance
|
||||
active_found = False
|
||||
for name, inst in instances.items():
|
||||
if inst.get("active"):
|
||||
inst["brain_api_key"] = api_key
|
||||
active_found = True
|
||||
break
|
||||
if not active_found:
|
||||
machine = compose.get("machine", "unknown")
|
||||
instances[machine] = {
|
||||
"active": True,
|
||||
"brain_name": machine,
|
||||
"brain_api_key": api_key,
|
||||
"path": BRAIN_ROOT,
|
||||
}
|
||||
compose["instances"] = instances
|
||||
|
||||
with open(compose_path, "w") as f:
|
||||
yaml.dump(compose, f, default_flow_style=False, allow_unicode=True)
|
||||
|
||||
print(f" ✅ Peer {server_machine} ({server_ip}) ajouté")
|
||||
if api_key:
|
||||
print(f" ✅ Brain API Key injectée dans brain-compose.local.yml")
|
||||
print(f" ✅ brain-compose.local.yml mis à jour")
|
||||
|
||||
# Add server host to known_hosts
|
||||
os.system(f"ssh-keyscan -H {server_ip} >> ~/.ssh/known_hosts 2>/dev/null")
|
||||
print(f" ✅ Fingerprint {server_ip} ajoutée à known_hosts")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
machine = get_local_machine()
|
||||
ssh_pubkey = get_ssh_pubkey()
|
||||
|
||||
print(f"🔗 brain-pair join — machine : {machine}")
|
||||
if not ssh_pubkey:
|
||||
print(f"⚠️ Aucune clé SSH trouvée — ssh-keygen recommandé")
|
||||
print()
|
||||
|
||||
# Scan LAN
|
||||
server_ip, server_port = scan_for_server()
|
||||
if not server_ip:
|
||||
print(f"❌ Aucun serveur brain-pair trouvé sur le LAN")
|
||||
print(f" Vérifier : brain-pair.sh start sur la machine source")
|
||||
sys.exit(1)
|
||||
|
||||
# Handshake
|
||||
response = do_handshake(server_ip, server_port, CODE, machine, ssh_pubkey)
|
||||
|
||||
# Apply config
|
||||
success = apply_config(response, server_ip)
|
||||
if success:
|
||||
print(f"\n✅ Pairing terminé !")
|
||||
print(f" Vérifier : bash scripts/bsi-query.sh peers")
|
||||
print(f" Secrets : bash scripts/brain-secrets-sync.sh status")
|
||||
else:
|
||||
print(f"\n❌ Pairing échoué")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user