sync: scission owner/template + brain-template-export + BRAIN_MODE guard + /visualize scope filter + port orphelins fix

This commit is contained in:
2026-03-21 02:34:47 +01:00
parent 78323a0094
commit 2fd53cce8e
93 changed files with 6953 additions and 684 deletions

View 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()