@neuroverseos/nv-sim 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +187 -535
  2. package/connectors/nv_mirofish_wrapper.py +841 -0
  3. package/connectors/nv_scienceclaw_wrapper.py +453 -0
  4. package/dist/adapters/scienceclaw.js +52 -2
  5. package/dist/assets/index-CH_VswRM.css +1 -0
  6. package/dist/assets/index-sT4b_z7w.js +686 -0
  7. package/dist/assets/{reportEngine-D2ZrMny8.js → reportEngine-Bu8bB5Yq.js} +1 -1
  8. package/dist/connectors/nv-scienceclaw-post.js +363 -0
  9. package/dist/engine/aiProvider.js +82 -3
  10. package/dist/engine/analyzer.js +12 -24
  11. package/dist/engine/cli.js +89 -114
  12. package/dist/engine/dynamicsGovernance.js +4 -0
  13. package/dist/engine/fullGovernedLoop.js +16 -1
  14. package/dist/engine/goalEngine.js +3 -4
  15. package/dist/engine/governance.js +18 -0
  16. package/dist/engine/index.js +19 -28
  17. package/dist/engine/intentTranslator.js +281 -0
  18. package/dist/engine/liveAdapter.js +100 -18
  19. package/dist/engine/liveVisualizer.js +2071 -1023
  20. package/dist/engine/primeRadiant.js +2 -8
  21. package/dist/engine/reasoningEngine.js +2 -7
  22. package/dist/engine/scenarioCapsule.js +5 -5
  23. package/dist/engine/swarmSimulation.js +1 -9
  24. package/dist/engine/universalAdapter.js +371 -0
  25. package/dist/engine/worldBridge.js +22 -8
  26. package/dist/index.html +2 -2
  27. package/dist/lib/reasoningEngine.js +17 -1
  28. package/dist/lib/simulationAdapter.js +11 -11
  29. package/dist/lib/swarmParser.js +1 -1
  30. package/dist/runtime/govern.js +160 -7
  31. package/dist/runtime/index.js +1 -4
  32. package/dist/runtime/types.js +91 -0
  33. package/package.json +23 -6
  34. package/dist/adapters/mirofish.js +0 -461
  35. package/dist/assets/index-B64NuIXu.css +0 -1
  36. package/dist/assets/index-BMkPevVr.js +0 -532
  37. package/dist/assets/mirotir-logo-DUexumBH.svg +0 -185
  38. package/dist/engine/mirofish.js +0 -295
@@ -0,0 +1,841 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ NeuroVerse MiroFish Connector — Runtime Governance Without Hooks
4
+
5
+ OpenClaw didn't accept our governance hook PR. So we built this.
6
+
7
+ This wrapper governs MiroFish simulations WITHOUT modifying MiroFish code.
8
+ It monkey-patches the OASIS action execution layer at import time so every
9
+ agent action flows through NeuroVerse governance before reaching the
10
+ simulation environment.
11
+
12
+ Three operating modes:
13
+
14
+ MODE 1: Runtime Patch (recommended)
15
+ Imports MiroFish/OASIS, patches agent action methods, runs governed.
16
+ Requires: MiroFish installed in the same Python environment.
17
+
18
+ MODE 2: Process Wrapper
19
+ Spawns MiroFish as a subprocess, parses structured JSON output,
20
+ evaluates each action/round through the governance API.
21
+ Requires: MiroFish on PATH or MIROFISH_BIN set.
22
+
23
+ MODE 3: Bridge-Only
24
+ Doesn't run MiroFish at all. Instead, starts a governance bridge
25
+ that MiroFish can call into via neuroverse_bridge.py.
26
+ Requires: User drops neuroverse_bridge.py into their MiroFish project.
27
+
28
+ Usage:
29
+ # Mode 1: Runtime patch (auto-detected if OASIS is importable)
30
+ python nv_mirofish_wrapper.py --simulation twitter --world social-media
31
+
32
+ # Mode 2: Process wrapper (if OASIS not importable)
33
+ python nv_mirofish_wrapper.py --simulation twitter --world social-media
34
+
35
+ # Mode 3: Bridge only
36
+ python nv_mirofish_wrapper.py --bridge-only
37
+
38
+ # With custom governance server
39
+ python nv_mirofish_wrapper.py --nv-url http://localhost:4000
40
+
41
+ Requires:
42
+ - NeuroVerse governance server running: npx nv-sim serve
43
+ - pip install requests
44
+ - For Mode 1: pip install oasis-social (MiroFish/OASIS)
45
+
46
+ Zero edits to MiroFish required. Zero PRs needed.
47
+ """
48
+
49
+ import subprocess
50
+ import sys
51
+ import os
52
+ import json
53
+ import time
54
+ import signal
55
+ import threading
56
+ import importlib
57
+ import functools
58
+ from typing import Any, Callable, Optional
59
+
60
+ try:
61
+ import requests
62
+ except ImportError:
63
+ print("ERROR: 'requests' package required. Install with: pip install requests")
64
+ sys.exit(1)
65
+
66
+ # ============================================
67
+ # CONFIGURATION
68
+ # ============================================
69
+
70
+ NEUROVERSE_URL = os.environ.get("NEUROVERSE_URL", "http://localhost:3456")
71
+ EVALUATE_ENDPOINT = f"{NEUROVERSE_URL}/api/evaluate"
72
+ MIROFISH_BIN = os.environ.get("MIROFISH_BIN", "python")
73
+ MIROFISH_SCRIPT = os.environ.get("MIROFISH_SCRIPT", "run_twitter_simulation.py")
74
+ VERBOSE = os.environ.get("NV_VERBOSE", "0") == "1"
75
+ ENFORCE_STREAMING = os.environ.get("NV_ENFORCE_STREAMING", "1") == "1"
76
+ NV_FAIL_OPEN = os.environ.get("NV_FAIL_OPEN", "true").lower() in ("true", "1", "yes")
77
+ NV_WORLD = os.environ.get("NV_WORLD", "social-media")
78
+
79
+ # ============================================
80
+ # STATS
81
+ # ============================================
82
+
83
+ stats = {
84
+ "evaluated": 0,
85
+ "allowed": 0,
86
+ "blocked": 0,
87
+ "modified": 0,
88
+ "penalized": 0,
89
+ "rewarded": 0,
90
+ "paused": 0,
91
+ "rounds_seen": 0,
92
+ "actions_seen": 0,
93
+ "actions_intercepted": 0,
94
+ "mode": "unknown",
95
+ }
96
+
97
+ # ============================================
98
+ # ARGUMENT PARSING
99
+ # ============================================
100
+
101
+ def parse_args(argv):
102
+ """Parse wrapper-specific and passthrough arguments."""
103
+ args = argv[1:]
104
+ parsed = {
105
+ "simulation": "twitter",
106
+ "world": NV_WORLD,
107
+ "bridge_only": False,
108
+ "compare": False,
109
+ "rounds": None,
110
+ "agents": None,
111
+ "rules_file": None,
112
+ "passthrough_args": [],
113
+ }
114
+
115
+ nv_args = set()
116
+ i = 0
117
+ while i < len(args):
118
+ flag = args[i]
119
+ if flag == "--simulation" and i + 1 < len(args):
120
+ parsed["simulation"] = args[i + 1]
121
+ nv_args.update([i, i + 1])
122
+ i += 2
123
+ elif flag == "--world" and i + 1 < len(args):
124
+ parsed["world"] = args[i + 1]
125
+ nv_args.update([i, i + 1])
126
+ i += 2
127
+ elif flag == "--bridge-only":
128
+ parsed["bridge_only"] = True
129
+ nv_args.add(i)
130
+ i += 1
131
+ elif flag == "--compare":
132
+ parsed["compare"] = True
133
+ nv_args.add(i)
134
+ i += 1
135
+ elif flag == "--rounds" and i + 1 < len(args):
136
+ parsed["rounds"] = int(args[i + 1])
137
+ nv_args.update([i, i + 1])
138
+ i += 2
139
+ elif flag == "--agents" and i + 1 < len(args):
140
+ parsed["agents"] = int(args[i + 1])
141
+ nv_args.update([i, i + 1])
142
+ i += 2
143
+ elif flag == "--rules" and i + 1 < len(args):
144
+ parsed["rules_file"] = args[i + 1]
145
+ nv_args.update([i, i + 1])
146
+ i += 2
147
+ elif flag == "--nv-url" and i + 1 < len(args):
148
+ nv_args.update([i, i + 1])
149
+ global NEUROVERSE_URL, EVALUATE_ENDPOINT
150
+ NEUROVERSE_URL = args[i + 1]
151
+ EVALUATE_ENDPOINT = f"{NEUROVERSE_URL}/api/evaluate"
152
+ i += 2
153
+ elif flag == "--nv-verbose":
154
+ nv_args.add(i)
155
+ global VERBOSE
156
+ VERBOSE = True
157
+ i += 1
158
+ elif flag == "--nv-fail-closed":
159
+ nv_args.add(i)
160
+ global NV_FAIL_OPEN
161
+ NV_FAIL_OPEN = False
162
+ i += 1
163
+ elif flag == "--mirofish-bin" and i + 1 < len(args):
164
+ nv_args.update([i, i + 1])
165
+ global MIROFISH_BIN
166
+ MIROFISH_BIN = args[i + 1]
167
+ i += 2
168
+ elif flag == "--mirofish-script" and i + 1 < len(args):
169
+ nv_args.update([i, i + 1])
170
+ global MIROFISH_SCRIPT
171
+ MIROFISH_SCRIPT = args[i + 1]
172
+ i += 2
173
+ else:
174
+ i += 1
175
+
176
+ parsed["passthrough_args"] = [
177
+ a for idx, a in enumerate(args) if idx not in nv_args
178
+ ]
179
+
180
+ return parsed
181
+
182
+ # ============================================
183
+ # GOVERNANCE EVALUATION
184
+ # ============================================
185
+
186
+ def evaluate_action(action_type, agent, payload_extra=None):
187
+ """Call NeuroVerse /api/evaluate and return the verdict."""
188
+ payload = {
189
+ "actor": agent,
190
+ "action": action_type,
191
+ "payload": {
192
+ "description": f"Agent {agent} executing {action_type}",
193
+ },
194
+ "world": NV_WORLD,
195
+ }
196
+
197
+ if payload_extra:
198
+ payload["payload"].update(payload_extra)
199
+
200
+ try:
201
+ resp = requests.post(EVALUATE_ENDPOINT, json=payload, timeout=5)
202
+ resp.raise_for_status()
203
+ result = resp.json()
204
+ stats["evaluated"] += 1
205
+
206
+ decision = result.get("decision", result.get("status", "ALLOW")).upper()
207
+ if decision == "ALLOW":
208
+ stats["allowed"] += 1
209
+ elif decision == "BLOCK":
210
+ stats["blocked"] += 1
211
+ elif decision == "PENALIZE":
212
+ stats["penalized"] += 1
213
+ elif decision == "REWARD":
214
+ stats["rewarded"] += 1
215
+ elif decision == "MODIFY":
216
+ stats["modified"] += 1
217
+ elif decision == "PAUSE":
218
+ stats["paused"] += 1
219
+
220
+ if VERBOSE:
221
+ reason = result.get("reason", "")
222
+ print(f" [NV] {agent}/{action_type} -> {decision}: {reason}")
223
+
224
+ return result
225
+ except requests.ConnectionError:
226
+ if stats["evaluated"] == 0:
227
+ print(f" [NV] WARNING: NeuroVerse server not reachable at {NEUROVERSE_URL}")
228
+ print(f" [NV] Start it with: npx nv-sim serve")
229
+ if NV_FAIL_OPEN:
230
+ print(f" [NV] Falling back to ALLOW (ungoverned mode)\n")
231
+ else:
232
+ print(f" [NV] Failing CLOSED — all actions blocked\n")
233
+ if NV_FAIL_OPEN:
234
+ return {"decision": "ALLOW", "reason": "Governance server unavailable — fail open"}
235
+ return {"decision": "BLOCK", "reason": "Governance server unavailable — fail closed"}
236
+ except Exception as e:
237
+ if NV_FAIL_OPEN:
238
+ return {"decision": "ALLOW", "reason": f"Evaluation error: {e}"}
239
+ return {"decision": "BLOCK", "reason": f"Evaluation error: {e}"}
240
+
241
+
242
+ def apply_rules_from_file(rules_file):
243
+ """Load a rules file and send it to the governance server."""
244
+ if not rules_file or not os.path.exists(rules_file):
245
+ return
246
+
247
+ with open(rules_file, "r") as f:
248
+ rules_text = f.read()
249
+
250
+ try:
251
+ resp = requests.post(
252
+ f"{NEUROVERSE_URL}/api/apply-rules",
253
+ json={"text": rules_text},
254
+ timeout=5,
255
+ )
256
+ resp.raise_for_status()
257
+ result = resp.json()
258
+ rule_count = result.get("ruleCount", 0)
259
+ enforced = result.get("enforcedCount", 0)
260
+ health = result.get("healthScore", 0)
261
+ print(f" [NV] Rules loaded: {rule_count} rules ({enforced} enforced), health {health}/100")
262
+ except requests.ConnectionError:
263
+ print(f" [NV] WARNING: Could not load rules — server not reachable")
264
+ except Exception as e:
265
+ print(f" [NV] WARNING: Could not load rules: {e}")
266
+
267
+
268
+ # ============================================
269
+ # MODE 1: RUNTIME PATCH
270
+ # ============================================
271
+
272
+ def try_runtime_patch(parsed):
273
+ """
274
+ Attempt to import OASIS and monkey-patch agent action execution.
275
+
276
+ MiroFish/OASIS architecture:
277
+ 1. Agent generates action via LLM -> agent.generate_action()
278
+ 2. Action is submitted to env -> env.step(actions)
279
+
280
+ We patch env.step() so every action dict flows through governance
281
+ BEFORE reaching the simulation environment. Blocked actions are
282
+ replaced with no-ops. Modified actions have reduced magnitude.
283
+
284
+ Returns True if patching succeeded, False if OASIS not available.
285
+ """
286
+ try:
287
+ # Try importing OASIS modules — this tells us if MiroFish is installed
288
+ oasis_env = importlib.import_module("oasis.social_platform.platform")
289
+ print(f" [NV] OASIS detected — using runtime patch mode")
290
+ stats["mode"] = "runtime_patch"
291
+ except (ImportError, ModuleNotFoundError):
292
+ return False
293
+
294
+ # Find the Platform class and patch its step method
295
+ Platform = getattr(oasis_env, "Platform", None)
296
+ if Platform is None:
297
+ # Try alternative module paths
298
+ try:
299
+ oasis_env = importlib.import_module("oasis.social_agent.agent")
300
+ print(f" [NV] OASIS agent module detected")
301
+ except (ImportError, ModuleNotFoundError):
302
+ pass
303
+ return False
304
+
305
+ original_step = Platform.step
306
+
307
+ @functools.wraps(original_step)
308
+ def governed_step(self, actions):
309
+ """
310
+ Governed version of Platform.step().
311
+
312
+ Intercepts agent actions, evaluates each through NeuroVerse governance,
313
+ and only passes allowed/modified actions to the original step function.
314
+ """
315
+ if not isinstance(actions, dict):
316
+ return original_step(self, actions)
317
+
318
+ governed_actions = {}
319
+ for agent_id, action_data in actions.items():
320
+ stats["actions_seen"] += 1
321
+
322
+ # Extract action metadata for governance
323
+ action_type = "unknown"
324
+ description = f"Agent {agent_id} action"
325
+ magnitude = 0.5
326
+
327
+ if isinstance(action_data, dict):
328
+ action_type = action_data.get("action_type", action_data.get("type", "social_action"))
329
+ description = action_data.get("content", action_data.get("message", description))
330
+ # Estimate magnitude from action characteristics
331
+ magnitude = _estimate_magnitude(action_data)
332
+ elif isinstance(action_data, (list, tuple)) and len(action_data) >= 2:
333
+ action_type = str(action_data[1]) if len(action_data) > 1 else "social_action"
334
+ description = str(action_data[0]) if len(action_data) > 0 else description
335
+
336
+ verdict = evaluate_action(
337
+ action_type=action_type,
338
+ agent=str(agent_id),
339
+ payload_extra={
340
+ "description": description[:500],
341
+ "magnitude": magnitude,
342
+ "simulation_type": parsed.get("simulation", "social"),
343
+ },
344
+ )
345
+
346
+ decision = verdict.get("decision", verdict.get("status", "ALLOW")).upper()
347
+
348
+ if decision == "BLOCK" or decision == "PENALIZE":
349
+ stats["actions_intercepted"] += 1
350
+ # Replace with no-op action
351
+ if isinstance(action_data, dict):
352
+ governed_actions[agent_id] = {
353
+ **action_data,
354
+ "action_type": "idle",
355
+ "content": "",
356
+ "_nv_blocked": True,
357
+ "_nv_reason": verdict.get("reason", "governance"),
358
+ }
359
+ else:
360
+ governed_actions[agent_id] = action_data # pass through if we can't modify
361
+ continue
362
+
363
+ elif decision == "MODIFY":
364
+ stats["actions_intercepted"] += 1
365
+ modified = verdict.get("modified_action", {})
366
+ if isinstance(action_data, dict):
367
+ governed_actions[agent_id] = {
368
+ **action_data,
369
+ **modified,
370
+ "_nv_modified": True,
371
+ }
372
+ else:
373
+ governed_actions[agent_id] = action_data
374
+ continue
375
+
376
+ elif decision == "PAUSE":
377
+ stats["actions_intercepted"] += 1
378
+ # Delay execution — reduce urgency
379
+ if isinstance(action_data, dict):
380
+ governed_actions[agent_id] = {
381
+ **action_data,
382
+ "_nv_paused": True,
383
+ }
384
+ else:
385
+ governed_actions[agent_id] = action_data
386
+ continue
387
+
388
+ else:
389
+ # ALLOW or REWARD — pass through unchanged
390
+ governed_actions[agent_id] = action_data
391
+
392
+ return original_step(self, governed_actions)
393
+
394
+ # Apply the monkey-patch
395
+ Platform.step = governed_step
396
+ print(f" [NV] Platform.step() patched — all actions now governed")
397
+ return True
398
+
399
+
400
+ def _estimate_magnitude(action_data):
401
+ """Estimate action magnitude from OASIS action data."""
402
+ magnitude = 0.5
403
+
404
+ action_type = action_data.get("action_type", action_data.get("type", ""))
405
+
406
+ # High-magnitude actions
407
+ if action_type in ("create_post", "publish", "broadcast"):
408
+ magnitude = 0.6
409
+ elif action_type in ("repost", "retweet", "share", "amplify"):
410
+ magnitude = 0.55
411
+ elif action_type in ("like", "react", "upvote"):
412
+ magnitude = 0.2
413
+ elif action_type in ("follow", "subscribe"):
414
+ magnitude = 0.15
415
+ elif action_type in ("block_user", "report", "flag"):
416
+ magnitude = 0.7
417
+
418
+ # Content length increases magnitude
419
+ content = action_data.get("content", action_data.get("message", ""))
420
+ if isinstance(content, str) and len(content) > 500:
421
+ magnitude = min(1.0, magnitude + 0.15)
422
+
423
+ # Mentions/hashtags increase reach
424
+ if isinstance(content, str):
425
+ mentions = content.count("@")
426
+ hashtags = content.count("#")
427
+ if mentions + hashtags > 3:
428
+ magnitude = min(1.0, magnitude + 0.1)
429
+
430
+ return magnitude
431
+
432
+
433
+ # ============================================
434
+ # MODE 2: PROCESS WRAPPER
435
+ # ============================================
436
+
437
+ def run_process_wrapper(parsed):
438
+ """
439
+ Wrap MiroFish as a subprocess.
440
+ Parse stdout for structured JSON, evaluate actions through governance.
441
+ """
442
+ stats["mode"] = "process_wrapper"
443
+
444
+ simulation = parsed["simulation"]
445
+ script = MIROFISH_SCRIPT
446
+
447
+ # Map simulation names to common MiroFish scripts
448
+ script_map = {
449
+ "twitter": "run_twitter_simulation.py",
450
+ "reddit": "run_reddit_simulation.py",
451
+ "social": "run_simulation.py",
452
+ }
453
+ if simulation in script_map:
454
+ script = script_map[simulation]
455
+
456
+ cmd = [MIROFISH_BIN, script] + parsed["passthrough_args"]
457
+
458
+ print(f" [NV] Running as subprocess: {' '.join(cmd)}")
459
+ if ENFORCE_STREAMING:
460
+ print(f" [NV] Streaming governance active — each round evaluated\n")
461
+ else:
462
+ print(f" [NV] Entry gate only — subprocess runs ungoverned\n")
463
+
464
+ print(f" {'=' * 50}")
465
+
466
+ try:
467
+ proc = subprocess.Popen(
468
+ cmd,
469
+ stdout=subprocess.PIPE,
470
+ stderr=subprocess.PIPE,
471
+ text=True,
472
+ bufsize=1,
473
+ )
474
+ except FileNotFoundError:
475
+ print(f"\n ERROR: Could not execute: {' '.join(cmd)}")
476
+ print(f" Set MIROFISH_BIN and MIROFISH_SCRIPT environment variables")
477
+ print(f" Or use --mirofish-bin and --mirofish-script flags")
478
+ sys.exit(127)
479
+
480
+ # Read stderr in background
481
+ stderr_thread = threading.Thread(
482
+ target=_stream_stderr, args=(proc,), daemon=True
483
+ )
484
+ stderr_thread.start()
485
+
486
+ # Stream stdout — parse and govern
487
+ terminated_by_governance = False
488
+ try:
489
+ for line in iter(proc.stdout.readline, ""):
490
+ sys.stdout.write(line)
491
+ sys.stdout.flush()
492
+
493
+ if ENFORCE_STREAMING and proc.poll() is None:
494
+ _govern_mirofish_output(line, proc)
495
+ if proc.poll() is not None:
496
+ terminated_by_governance = True
497
+ break
498
+ except KeyboardInterrupt:
499
+ proc.terminate()
500
+ proc.wait(timeout=5)
501
+
502
+ exit_code = proc.wait()
503
+ return exit_code, terminated_by_governance
504
+
505
+
506
+ def _govern_mirofish_output(line, proc):
507
+ """
508
+ Parse a line of MiroFish output and evaluate for governance.
509
+
510
+ MiroFish/OASIS outputs vary by simulation type:
511
+ - JSON lines with round/step and agent_actions
512
+ - Log lines with agent action descriptions
513
+ - Status lines
514
+
515
+ We govern JSON-structured output only (same principle as ScienceClaw).
516
+ """
517
+ line = line.strip()
518
+ if not line or not line.startswith("{"):
519
+ return
520
+
521
+ try:
522
+ data = json.loads(line)
523
+ except json.JSONDecodeError:
524
+ return
525
+
526
+ # Look for round/step structure
527
+ round_num = data.get("round") or data.get("step") or data.get("cycle")
528
+ actions = (
529
+ data.get("agent_actions")
530
+ or data.get("actions")
531
+ or data.get("reactions")
532
+ or []
533
+ )
534
+
535
+ if round_num is not None:
536
+ stats["rounds_seen"] += 1
537
+
538
+ if not actions:
539
+ # Check if this is a single agent action
540
+ if data.get("agent_id") and (data.get("action") or data.get("action_type")):
541
+ actions = [data]
542
+ else:
543
+ return
544
+
545
+ for action in actions:
546
+ stats["actions_seen"] += 1
547
+ agent_id = action.get("agent_id") or action.get("id") or "unknown"
548
+ action_type = action.get("action_type") or action.get("action") or action.get("type") or "social_action"
549
+ description = action.get("content") or action.get("message") or action.get("description") or f"{action_type}"
550
+
551
+ verdict = evaluate_action(
552
+ action_type=action_type,
553
+ agent=str(agent_id),
554
+ payload_extra={
555
+ "description": str(description)[:500],
556
+ "round": round_num,
557
+ "magnitude": _estimate_magnitude(action) if isinstance(action, dict) else 0.5,
558
+ },
559
+ )
560
+
561
+ decision = verdict.get("decision", verdict.get("status", "ALLOW")).upper()
562
+
563
+ if decision in ("BLOCK", "PENALIZE") and ENFORCE_STREAMING:
564
+ reason = verdict.get("reason", "policy violation")
565
+ print(f"\n [NV] GOVERNANCE HALT at round {round_num}")
566
+ print(f" Agent: {agent_id}")
567
+ print(f" Action: {action_type}: {str(description)[:80]}")
568
+ print(f" Decision: {decision}")
569
+ print(f" Reason: {reason}")
570
+
571
+ if decision == "PENALIZE" and verdict.get("consequence"):
572
+ c = verdict["consequence"]
573
+ print(f" Consequence: {c.get('type', 'unknown')} for {c.get('rounds', 1)} round(s)")
574
+
575
+ print(f" Terminating MiroFish process...\n")
576
+
577
+ try:
578
+ proc.terminate()
579
+ proc.wait(timeout=5)
580
+ except Exception:
581
+ proc.kill()
582
+ return
583
+
584
+
585
+ def _stream_stderr(proc):
586
+ """Read stderr in a background thread."""
587
+ for line in iter(proc.stderr.readline, ""):
588
+ if line.strip():
589
+ sys.stderr.write(line)
590
+ sys.stderr.flush()
591
+
592
+
593
+ # ============================================
594
+ # MODE 3: BRIDGE ONLY
595
+ # ============================================
596
+
597
+ def run_bridge_only():
598
+ """
599
+ Don't run MiroFish. Just verify the governance server is running
600
+ and print instructions for using neuroverse_bridge.py.
601
+ """
602
+ stats["mode"] = "bridge_only"
603
+
604
+ print(f"\n NeuroVerse MiroFish Bridge Mode")
605
+ print(f" {'=' * 60}")
606
+ print(f" Governance server: {NEUROVERSE_URL}")
607
+
608
+ # Check if server is running
609
+ try:
610
+ resp = requests.get(f"{NEUROVERSE_URL}/api/session", timeout=3)
611
+ resp.raise_for_status()
612
+ session = resp.json()
613
+ print(f" Server status: RUNNING")
614
+ print(f" Evaluations: {session.get('totalEvaluations', 0)}")
615
+ except Exception:
616
+ print(f" Server status: NOT RUNNING")
617
+ print(f"\n Start the governance server first:")
618
+ print(f" npx nv-sim serve")
619
+ print(f"\n Then run this again.")
620
+ sys.exit(1)
621
+
622
+ print(f"\n {'=' * 60}")
623
+ print(f"""
624
+ HOW TO USE WITH MIROFISH:
625
+
626
+ 1. Copy the bridge into your MiroFish project:
627
+
628
+ cp bridges/neuroverse_bridge.py /path/to/mirofish/
629
+
630
+ 2. In your MiroFish agent code (e.g., agent_action.py), add:
631
+
632
+ from neuroverse_bridge import evaluate, is_allowed
633
+
634
+ # Before executing any action:
635
+ verdict = evaluate(
636
+ actor=str(self.agent_id),
637
+ action=action_type,
638
+ payload={{"message": message}},
639
+ )
640
+ if not is_allowed(verdict):
641
+ return {{"blocked": True, "reason": verdict["reason"]}}
642
+
643
+ 3. Or use the zero-code approach — run MiroFish through the wrapper:
644
+
645
+ python connectors/nv_mirofish_wrapper.py \\
646
+ --simulation twitter \\
647
+ --world social-media
648
+
649
+ Environment variables:
650
+ NV_GOVERNANCE_URL={NEUROVERSE_URL}
651
+ NV_WORLD={NV_WORLD}
652
+ NV_FAIL_OPEN=true # allow actions if server is down
653
+ NV_ENABLED=true # disable governance without removing code
654
+
655
+ {'=' * 60}
656
+ """)
657
+
658
+ # Keep running so user can test with curl
659
+ print(f" Bridge is ready. Test with:")
660
+ print(f" curl -X POST {NEUROVERSE_URL}/api/evaluate \\")
661
+ print(f' -H "Content-Type: application/json" \\')
662
+ print(f' -d \'{{"actor":"agent_1","action":"create_post","payload":{{"message":"test"}}}}\'')
663
+ print()
664
+ print(f" Press Ctrl+C to exit.\n")
665
+
666
+ try:
667
+ while True:
668
+ time.sleep(1)
669
+ except KeyboardInterrupt:
670
+ print(f"\n Bridge stopped.")
671
+
672
+
673
+ # ============================================
674
+ # PRINT SESSION REPORT
675
+ # ============================================
676
+
677
+ def print_report(exit_code=0, terminated_by_governance=False):
678
+ """Print the governance session summary."""
679
+ print(f"\n {'=' * 60}")
680
+ print(f" NeuroVerse Governance Report")
681
+ print(f" {'=' * 60}")
682
+ print(f" Mode: {stats['mode']}")
683
+
684
+ if terminated_by_governance:
685
+ print(f" Status: HALTED by governance")
686
+ elif exit_code == 0:
687
+ print(f" Status: Completed")
688
+ else:
689
+ print(f" Status: Exited with code {exit_code}")
690
+
691
+ print(f" Evaluations: {stats['evaluated']}")
692
+ print(f" Rounds: {stats['rounds_seen']}")
693
+ print(f" Actions seen: {stats['actions_seen']}")
694
+ print(f" Intercepted: {stats['actions_intercepted']}")
695
+ print(f" {'-' * 40}")
696
+ print(f" Allowed: {stats['allowed']}")
697
+ print(f" Blocked: {stats['blocked']}")
698
+ print(f" Modified: {stats['modified']}")
699
+ print(f" Paused: {stats['paused']}")
700
+ print(f" Penalized: {stats['penalized']}")
701
+ print(f" Rewarded: {stats['rewarded']}")
702
+
703
+ # Effectiveness summary
704
+ total = stats["evaluated"]
705
+ if total > 0:
706
+ governance_rate = ((stats["blocked"] + stats["modified"] + stats["penalized"]) / total) * 100
707
+ pass_rate = (stats["allowed"] / total) * 100
708
+ print(f" {'-' * 40}")
709
+ print(f" Governance rate: {governance_rate:.0f}% (actions affected by rules)")
710
+ print(f" Pass-through: {pass_rate:.0f}% (actions allowed unchanged)")
711
+
712
+ print(f" {'=' * 60}\n")
713
+
714
+ # Report completion to governance server
715
+ try:
716
+ requests.post(
717
+ EVALUATE_ENDPOINT,
718
+ json={
719
+ "actor": "nv_mirofish_wrapper",
720
+ "action": "simulation_complete",
721
+ "payload": {
722
+ "description": f"MiroFish simulation {'halted by governance' if terminated_by_governance else 'completed'}",
723
+ "exit_code": exit_code,
724
+ "halted_by_governance": terminated_by_governance,
725
+ "stats": stats,
726
+ },
727
+ "world": NV_WORLD,
728
+ },
729
+ timeout=3,
730
+ )
731
+ except Exception:
732
+ pass
733
+
734
+
735
+ # ============================================
736
+ # MAIN
737
+ # ============================================
738
+
739
+ def run():
740
+ parsed = parse_args(sys.argv)
741
+ NV_WORLD_LOCAL = parsed["world"]
742
+ global NV_WORLD
743
+ NV_WORLD = NV_WORLD_LOCAL
744
+
745
+ print(f"\n NeuroVerse MiroFish Connector")
746
+ print(f" {'=' * 60}")
747
+ print(f" Simulation: {parsed['simulation']}")
748
+ print(f" World: {NV_WORLD}")
749
+ print(f" Server: {NEUROVERSE_URL}")
750
+ print(f" Fail mode: {'open (ungoverned)' if NV_FAIL_OPEN else 'closed (all blocked)'}")
751
+ print(f" {'=' * 60}\n")
752
+
753
+ # Load custom rules if provided
754
+ if parsed["rules_file"]:
755
+ print(f" [NV] Loading rules from: {parsed['rules_file']}")
756
+ apply_rules_from_file(parsed["rules_file"])
757
+ print()
758
+
759
+ # Mode 3: Bridge only
760
+ if parsed["bridge_only"]:
761
+ run_bridge_only()
762
+ return
763
+
764
+ # Mode 1: Try runtime patch first
765
+ print(f" [1/3] Detecting MiroFish/OASIS installation...")
766
+ patched = try_runtime_patch(parsed)
767
+
768
+ if patched:
769
+ # Runtime patch succeeded — now run the simulation
770
+ print(f" [2/3] Launching governed simulation...\n")
771
+
772
+ # Entry gate: evaluate the simulation launch itself
773
+ verdict = evaluate_action(
774
+ action_type="launch_simulation",
775
+ agent="nv_mirofish_wrapper",
776
+ payload_extra={
777
+ "simulation_type": parsed["simulation"],
778
+ "description": f"Launch MiroFish {parsed['simulation']} simulation with governance",
779
+ },
780
+ )
781
+
782
+ decision = verdict.get("decision", verdict.get("status", "ALLOW")).upper()
783
+ if decision in ("BLOCK", "PENALIZE"):
784
+ print(f" [NV] Simulation launch BLOCKED: {verdict.get('reason', '')}")
785
+ sys.exit(1)
786
+
787
+ # Import and run the actual MiroFish simulation
788
+ try:
789
+ if parsed["simulation"] == "twitter":
790
+ sim_module = importlib.import_module("run_twitter_simulation")
791
+ elif parsed["simulation"] == "reddit":
792
+ sim_module = importlib.import_module("run_reddit_simulation")
793
+ else:
794
+ sim_module = importlib.import_module(f"run_{parsed['simulation']}_simulation")
795
+
796
+ if hasattr(sim_module, "main"):
797
+ sim_module.main()
798
+ elif hasattr(sim_module, "run"):
799
+ sim_module.run()
800
+ else:
801
+ print(f" [NV] WARNING: No main() or run() found in simulation module")
802
+ print(f" [NV] The module was imported and patched — if it runs on import, governance was active")
803
+
804
+ print_report(exit_code=0, terminated_by_governance=False)
805
+
806
+ except SystemExit as e:
807
+ print_report(exit_code=e.code or 0, terminated_by_governance=False)
808
+ except Exception as e:
809
+ print(f"\n [NV] Simulation error: {e}")
810
+ print_report(exit_code=1, terminated_by_governance=False)
811
+ return
812
+
813
+ # Mode 2: Fall back to process wrapper
814
+ print(f" [NV] OASIS not importable — falling back to process wrapper mode")
815
+ print(f" [2/3] Evaluating launch governance...")
816
+
817
+ verdict = evaluate_action(
818
+ action_type="launch_simulation",
819
+ agent="nv_mirofish_wrapper",
820
+ payload_extra={
821
+ "simulation_type": parsed["simulation"],
822
+ "description": f"Launch MiroFish {parsed['simulation']} simulation via subprocess",
823
+ },
824
+ )
825
+
826
+ decision = verdict.get("decision", verdict.get("status", "ALLOW")).upper()
827
+ if decision in ("BLOCK", "PENALIZE"):
828
+ print(f" [NV] Simulation launch BLOCKED: {verdict.get('reason', '')}")
829
+ sys.exit(1)
830
+ else:
831
+ print(f" [NV] Launch: {decision}")
832
+
833
+ print(f" [3/3] Starting MiroFish subprocess...\n")
834
+
835
+ exit_code, terminated = run_process_wrapper(parsed)
836
+ print_report(exit_code=exit_code, terminated_by_governance=terminated)
837
+ sys.exit(1 if terminated else exit_code)
838
+
839
+
840
+ if __name__ == "__main__":
841
+ run()