@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.
- package/README.md +187 -535
- package/connectors/nv_mirofish_wrapper.py +841 -0
- package/connectors/nv_scienceclaw_wrapper.py +453 -0
- package/dist/adapters/scienceclaw.js +52 -2
- package/dist/assets/index-CH_VswRM.css +1 -0
- package/dist/assets/index-sT4b_z7w.js +686 -0
- package/dist/assets/{reportEngine-D2ZrMny8.js → reportEngine-Bu8bB5Yq.js} +1 -1
- package/dist/connectors/nv-scienceclaw-post.js +363 -0
- package/dist/engine/aiProvider.js +82 -3
- package/dist/engine/analyzer.js +12 -24
- package/dist/engine/cli.js +89 -114
- package/dist/engine/dynamicsGovernance.js +4 -0
- package/dist/engine/fullGovernedLoop.js +16 -1
- package/dist/engine/goalEngine.js +3 -4
- package/dist/engine/governance.js +18 -0
- package/dist/engine/index.js +19 -28
- package/dist/engine/intentTranslator.js +281 -0
- package/dist/engine/liveAdapter.js +100 -18
- package/dist/engine/liveVisualizer.js +2071 -1023
- package/dist/engine/primeRadiant.js +2 -8
- package/dist/engine/reasoningEngine.js +2 -7
- package/dist/engine/scenarioCapsule.js +5 -5
- package/dist/engine/swarmSimulation.js +1 -9
- package/dist/engine/universalAdapter.js +371 -0
- package/dist/engine/worldBridge.js +22 -8
- package/dist/index.html +2 -2
- package/dist/lib/reasoningEngine.js +17 -1
- package/dist/lib/simulationAdapter.js +11 -11
- package/dist/lib/swarmParser.js +1 -1
- package/dist/runtime/govern.js +160 -7
- package/dist/runtime/index.js +1 -4
- package/dist/runtime/types.js +91 -0
- package/package.json +23 -6
- package/dist/adapters/mirofish.js +0 -461
- package/dist/assets/index-B64NuIXu.css +0 -1
- package/dist/assets/index-BMkPevVr.js +0 -532
- package/dist/assets/mirotir-logo-DUexumBH.svg +0 -185
- 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()
|