#!/usr/bin/env python3 """ Install a single soul's seed into their own dedicated Engram instance. Uses the native Engram binary HTTP API: POST /api/nodes {"content": "...", "node_type": "...", "salience": 0.8, "_auth": ""} POST /api/edges {"from_id": "...", "to_id": "...", "relation": "...", "weight": 0.8, "_auth": ""} GET /stats health check ({"node_count": N, "edge_count": M, "layer_count": K}) Auth: _auth field in the POST body. Per-soul keys: ntn--2026 (matching the running DHARMA infrastructure). Usage: python3 install_soul.py python3 install_soul.py richard-feynman python3 install_soul.py richard-feynman --force # reinstall even if data exists Steps: 1. Checks if soul's Engram is already running (port in use) — installs into it. If not running, starts the Engram binary, installs, then stops it. 2. Creates forge/imprints// data directory 3. Polls GET /stats until the instance is ready 4. Reads the soul's seed JSON 5. POSTs all nodes and edges using the soul's API key 6. Captures and returns the root node ID 7. If we started it, stops the instance (data persists) """ import json import os import socket import subprocess import sys import time from pathlib import Path import requests # ── Config ────────────────────────────────────────────────────────────────── FORGE_DIR = Path("/Users/will/Development/neuron-technologies/forge") ENGRAM_BIN = Path( "/Users/will/Development/neuron-technologies/foundation/engram/dist/engram" ) REGISTRY_FILE = FORGE_DIR / "registry.json" STARTUP_TIMEOUT = 30 STARTUP_POLL_INTERVAL = 0.3 def load_registry() -> dict: return json.loads(REGISTRY_FILE.read_text()) def save_registry(registry: dict) -> None: REGISTRY_FILE.write_text(json.dumps(registry, indent=2)) def soul_api_key(soul: dict) -> str: """ Return the API key for a soul's Engram instance. Per-soul keys: ntn--2026 (matching the running DHARMA infrastructure). If registry has engram_api_key set, that takes precedence. """ if soul.get("engram_api_key"): return soul["engram_api_key"] return f"ntn-{soul['slug']}-2026" def find_soul(registry: dict, slug: str) -> dict | None: for imp in registry["imprints"]: if imp["slug"] == slug: return imp return None def find_seed_file(registry_entry: dict) -> Path: """Resolve the seed file path from the registry entry.""" slug = registry_entry["slug"] # Try registry-specified path first seed_rel = registry_entry.get("seed_file", "") if seed_rel: candidate = FORGE_DIR / seed_rel if candidate.exists(): return candidate # Fallback: search known locations candidates = [ FORGE_DIR / "seeds" / f"{slug}-seed.json", FORGE_DIR / f"{slug}-seed.json", # Legacy names used before seed file reorganization FORGE_DIR / "seeds" / f"{slug.split('-')[0]}-seed.json", # e.g. alan from alan-turing ] for c in candidates: if c.exists(): return c raise FileNotFoundError( f"Cannot find seed file for {slug!r}. " f"Tried: {[str(c) for c in candidates]}" ) def is_port_in_use(port: int) -> bool: """Return True if a process is already listening on the port.""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: return s.connect_ex(("127.0.0.1", port)) == 0 def start_engram(slug: str, port: int, db_path: Path, api_key: str) -> subprocess.Popen: """Start an Engram instance for the soul and return the process handle.""" db_path.mkdir(parents=True, exist_ok=True) log_dir = FORGE_DIR / "log" log_dir.mkdir(exist_ok=True) log_file = log_dir / f"dharma-{slug}.log" env = os.environ.copy() # Native binary uses ENGRAM_DATA_DIR and ENGRAM_DB_PATH (both accepted) env["ENGRAM_DATA_DIR"] = str(db_path) env["ENGRAM_DB_PATH"] = str(db_path) env["ENGRAM_BIND"] = f"0.0.0.0:{port}" env["ENGRAM_API_KEY"] = api_key with open(log_file, "a") as lf: proc = subprocess.Popen( [str(ENGRAM_BIN)], env=env, stdout=lf, stderr=lf, ) return proc def wait_for_ready(port: int, timeout: float = STARTUP_TIMEOUT) -> bool: """Poll /stats until the instance responds or timeout expires.""" url = f"http://localhost:{port}/stats" deadline = time.time() + timeout while time.time() < deadline: try: resp = requests.get(url, timeout=2) if resp.status_code == 200: return True except requests.exceptions.ConnectionError: pass time.sleep(STARTUP_POLL_INTERVAL) return False def sanitize_content(s: str) -> str: """ Sanitize text content for the native Engram binary's JSON parser. The native binary's JSON response serializer does not escape double-quotes in node content, which causes the response JSON to be malformed. Replace double-quotes and other problematic characters with safe ASCII equivalents. """ replacements = [ ('"', "'"), # double-quote → single (CRITICAL: prevents JSON response corruption) ("—", "--"), # em dash ("–", "-"), # en dash ("‘", "'"), ("’", "'"), # curly single quotes ("“", "'"), ("”", "'"), # curly double quotes ("…", "..."), # ellipsis ("é", "e"), ("è", "e"), ("ê", "e"), ("ë", "e"), ("à", "a"), ("â", "a"), ("á", "a"), ("ü", "u"), ("û", "u"), ("ú", "u"), ("ö", "o"), ("ô", "o"), ("ó", "o"), ("ä", "a"), ("ß", "ss"), ("î", "i"), ("í", "i"), ("É", "E"), ("Ê", "E"), ("È", "E"), ("Â", "A"), ("Î", "I"), ("Ô", "O"), ("Û", "U"), ("ñ", "n"), ("Ñ", "N"), ("ç", "c"), ("Ç", "C"), ] for char, replacement in replacements: s = s.replace(char, replacement) # Replace any remaining non-ASCII with '?' return s.encode("ascii", errors="replace").decode("ascii") def post_node(port: int, content: str, node_type: str, salience: float, api_key: str) -> str: """POST a node via /api/nodes. Returns the node UUID.""" url = f"http://localhost:{port}/api/nodes" safe_content = sanitize_content(content) body = { "content": safe_content, "node_type": node_type, "salience": salience, "_auth": api_key, } resp = requests.post(url, json=body, timeout=30) resp.raise_for_status() result = resp.json() if "id" not in result: raise ValueError(f"Unexpected response from /api/nodes: {result}") return result["id"] def post_edge(port: int, from_id: str, to_id: str, relation: str, weight: float, api_key: str) -> None: """POST an edge via /api/edges.""" url = f"http://localhost:{port}/api/edges" body = { "from_id": from_id, "to_id": to_id, "relation": relation, "weight": weight, "_auth": api_key, } resp = requests.post(url, json=body, timeout=30) resp.raise_for_status() def install_seed(port: int, seed: dict, api_key: str) -> str: """ Install all nodes and edges from a seed dict. Returns the root node UUID. """ subject = seed["subject"] print(f" Installing nodes for {subject}...") # Root identity node root_id = post_node( port, content=f"IMPRINT: {subject} | dharma/1.0", node_type="Identity", salience=1.0, api_key=api_key, ) print(f" Root node: {root_id}") # Values for v in seed.get("values", []): content = f"VALUE: {v['value']}" if v.get("grounding"): content += f" | grounding: {v['grounding']}" nid = post_node(port, content, "Identity", float(v.get("weight", 0.8)), api_key) post_edge(port, root_id, nid, "has_value", float(v.get("weight", 0.8)), api_key) # Biography for b in seed.get("biography", []): content = f"BIOGRAPHY: {b['event']}" nid = post_node(port, content, "Identity", float(b.get("weight", 0.7)), api_key) post_edge(port, root_id, nid, "formed_by", float(b.get("weight", 0.7)), api_key) # Relationships for r in seed.get("relationships", []): content = f"RELATIONSHIP: {r['name']}" if r.get("role"): content += f" ({r['role']})" nid = post_node(port, content, "Identity", float(r.get("weight", 0.6)), api_key) post_edge(port, root_id, nid, "relates_to", float(r.get("weight", 0.6)), api_key) # Reasoning patterns for pattern in seed.get("reasoning_patterns", []): content = f"REASONING: {pattern}" nid = post_node(port, content, "Identity", 0.7, api_key) post_edge(port, root_id, nid, "reasons_with", 0.7, api_key) # Voice profile — one consolidated node voice = seed.get("voice_profile", {}) if voice: voice_parts = [] for k, v in voice.items(): if v: voice_parts.append(f"{k}: {v}") if voice_parts: voice_content = "VOICE: " + " | ".join(voice_parts) nid = post_node(port, voice_content[:2000], "Identity", 0.9, api_key) post_edge(port, root_id, nid, "speaks_as", 0.9, api_key) # Stats after install stats_resp = requests.get(f"http://localhost:{port}/stats", timeout=10) stats = stats_resp.json() print( f" Engram stats after install: " f"{stats.get('node_count', '?')} nodes, " f"{stats.get('edge_count', '?')} edges" ) # Persist to disk save_url = f"http://localhost:{port}/api/save" try: save_resp = requests.post(save_url, json={"_auth": api_key}, timeout=10) save_result = save_resp.json() print(f" Saved to: {save_result.get('path', '?')}") except Exception as exc: print(f" WARN: save failed: {exc}") return root_id def install_soul(slug: str, force: bool = False) -> dict: """ Full install pipeline for one soul. If the soul's Engram is already running (port in use), installs directly into it. Otherwise starts a temporary instance, installs, and stops it. Returns dict with: slug, root_id, engram_db_path, engram_port, engram_url, engram_api_key """ if not ENGRAM_BIN.exists(): raise FileNotFoundError( f"Engram binary not found at {ENGRAM_BIN}." ) registry = load_registry() soul = find_soul(registry, slug) if soul is None: raise ValueError(f"Soul {slug!r} not found in registry.json") port = soul["engram_port"] db_path = FORGE_DIR / soul.get("engram_db_path", f"imprints/{slug}") api_key = soul_api_key(soul) # Skip if already installed (data dir non-empty AND registry has root_id), # unless --force is given. already_has_data = db_path.exists() and any(db_path.iterdir()) already_has_root = bool(soul.get("engram_root_id")) if not force and already_has_data and already_has_root: print(f"[{slug}] Already installed (root={soul['engram_root_id']}) — skipping.") print(f"[{slug}] Use --force to reinstall.") return { "slug": slug, "root_id": soul.get("engram_root_id"), "skipped": True, } seed_file = find_seed_file(soul) seed = json.loads(seed_file.read_text()) print( f"[{slug}] Seed: {seed_file.name} " f"({len(seed.get('values', []))} values, " f"{len(seed.get('biography', []))} bio, " f"{len(seed.get('reasoning_patterns', []))} patterns, " f"{len(seed.get('relationships', []))} relationships)" ) print(f"[{slug}] API key: {api_key}") already_running = is_port_in_use(port) proc = None if already_running: print(f"[{slug}] Engram already running on port {port} — installing into live instance.") if not wait_for_ready(port, timeout=5): raise RuntimeError( f"Port {port} is in use but Engram is not responding at /stats" ) else: print(f"[{slug}] Starting Engram on port {port}...") proc = start_engram(slug, port, db_path, api_key) try: if proc is not None: if not wait_for_ready(port): proc.terminate() raise RuntimeError( f"Engram for {slug} did not become ready within {STARTUP_TIMEOUT}s" ) print(f"[{slug}] Engram ready on port {port}") root_id = install_seed(port, seed, api_key) print(f"[{slug}] Install complete. Root node: {root_id}") finally: if proc is not None: proc.terminate() try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() print(f"[{slug}] Instance stopped.") # Update registry entry from datetime import date for imp in registry["imprints"]: if imp["slug"] == slug: imp["engram_db_path"] = str(db_path.relative_to(FORGE_DIR)) imp["engram_port"] = port imp["engram_url"] = f"http://localhost:{port}" imp["engram_api_key"] = api_key imp["engram_root_id"] = root_id imp["installed"] = True imp["installed_at"] = str(date.today()) break save_registry(registry) print(f"[{slug}] Registry updated.") return { "slug": slug, "root_id": root_id, "engram_db_path": str(db_path.relative_to(FORGE_DIR)), "engram_port": port, "engram_url": f"http://localhost:{port}", "engram_api_key": api_key, "skipped": False, } def main(): import argparse parser = argparse.ArgumentParser( description="Install a soul's seed into its Engram instance" ) parser.add_argument("slug", help="Soul slug (e.g. richard-feynman)") parser.add_argument( "--force", action="store_true", help="Reinstall even if imprint directory already exists", ) args = parser.parse_args() result = install_soul(args.slug, force=args.force) if result.get("skipped"): print(f"\nSkipped (already installed). Root: {result.get('root_id', 'unknown')}") else: print(f"\nDone. Root node ID: {result['root_id']}") if __name__ == "__main__": main()