Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions BOOT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# NeuroRift + OpenClaw Boot Sequence

This runbook initializes NeuroRift as a primary OpenClaw agent and loads proactive monitoring.

## 1) Preflight

1. Export provider and channel secrets:
- `OPENAI_API_KEY`
- `ANTHROPIC_API_KEY` or `CLAUDE_API_KEY`
- `ZAI_API_KEY` or `Z_AI_API_KEY`
- `OPENCLAW_DISCORD_WEBHOOK_URL` (optional)
- `OPENCLAW_TELEGRAM_BOT_TOKEN` + `OPENCLAW_TELEGRAM_CHAT_ID` (optional)
2. Confirm services:
- NeuroRift FastAPI bridge on `:8766`
- OpenClaw WebSocket gateway on `:18789`

## 2) Start NeuroRift FastAPI bridge

```bash
python3 modules/web/bridge_server.py
```

Health check:

```bash
curl -s http://127.0.0.1:8766/health
```

## 3) Start OpenClaw gateway

Use your OpenClaw runtime with the unified config:

```bash
openclaw gateway --config ./openclaw.json5
```

## 4) Start the adapter bridge

```bash
python3 integrations/openclaw/openclaw_gateway_adapter.py
```

The adapter maps NeuroRift internal calls into OpenClaw RPC methods:
- `run_terminal_cmd -> exec`
- `read_file -> read`
- `write_file -> write`
- `process_state -> process`

## 5) Load HEARTBEAT checklist

Review and execute `HEARTBEAT.md` at startup and once every shift.

## 6) Validate end-to-end flow

1. Send a recon request from Discord/Telegram/WhatsApp/Signal.
2. Verify the request opens an `isolated` session.
3. Confirm high-risk commands trigger approval forwarder messages.
4. Confirm scheduled job appears in CronService (`weekly-attack-surface-recon`).
41 changes: 41 additions & 0 deletions HEARTBEAT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# HEARTBEAT Checklist (Proactive Monitoring)

Run this checklist continuously while the gateway is active.

## Runtime Liveness

- [ ] OpenClaw WebSocket is reachable on `ws://127.0.0.1:18789/gateway`
- [ ] NeuroRift FastAPI bridge healthy on `http://127.0.0.1:8766/health`
- [ ] Adapter session started as `isolated`
- [ ] `yieldMs` push updates are flowing (no tight polling loops)

## Security Controls

- [ ] Docker sandbox image in use: `openclaw-sandbox:bookworm-slim`
- [ ] Sandboxed enforcement for `nmap`, `subfinder`, `httpx`
- [ ] High-risk command patterns are intercepted
- [ ] Approval timeout defaults to **deny**

## Channel & Workflow Routing

- [ ] Inbound triggers active for Discord/Telegram/WhatsApp/Signal
- [ ] Security prompts route to NeuroRift recon workflow
- [ ] Planner -> Manus Tool Selector -> Operator -> Analyst/Cursor pipeline executes in order

## Memory & Persona

- [ ] Session-memory hooks loaded under `neurorift-session-memory`
- [ ] Attack surface context persisted between sessions
- [ ] `SOUL.md` persona loaded for responses

## Scheduling

- [ ] CronService job `weekly-attack-surface-recon` registered
- [ ] `computeNextRunAtMs` is enabled
- [ ] Notifications are pushed on completion/failure

## Environment Normalization

- [ ] Anthropic key normalized (`ANTHROPIC_API_KEY`)
- [ ] OpenAI key normalized (`OPENAI_API_KEY`)
- [ ] Z.AI key normalized (`ZAI_API_KEY`)
17 changes: 17 additions & 0 deletions SOUL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# NeuroRift Persona (SOUL)

You are **NeuroRift**, a security intelligence agent operating through OpenClaw.

## Core behavior

- Prioritize legal, authorized reconnaissance and attack-surface discovery.
- Default to least-risk execution and ask for approval on high-risk actions.
- Preserve target context (assets, findings, timelines) across sessions.
- Communicate findings with concise severity, evidence, and remediation.

## Persistent memory anchors

- `target_profile`: domains, CIDRs, cloud assets, owners
- `scan_history`: last scans, diffs, regressions, unresolved findings
- `approval_journal`: who approved what, when, and why
- `channel_context`: source channel, analyst handoff notes
210 changes: 210 additions & 0 deletions integrations/openclaw/openclaw_gateway_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""OpenClaw Gateway adapter for NeuroRift.

Bridges NeuroRift FastAPI commands with OpenClaw RPC frames and adds:
- isolated session pipeline metadata
- high-risk execution approval forwarding
- environment key normalization
- push/yield async response handling
"""

from __future__ import annotations

import asyncio
import json
import os
import re
import time
import uuid
from dataclasses import dataclass
from typing import Any, Dict, Optional

import httpx
import websockets

BRIDGE_URL = os.getenv("NEURORIFT_BRIDGE_URL", "http://127.0.0.1:8766")
GATEWAY_WS_URL = os.getenv("OPENCLAW_WS_URL", "ws://127.0.0.1:18789/gateway")
YIELD_MS = int(os.getenv("OPENCLAW_YIELD_MS", "1500"))


def normalize_env() -> Dict[str, str]:
"""Normalize provider keys to avoid auth drift across providers."""
out = dict(os.environ)

anthropic = out.get("ANTHROPIC_API_KEY") or out.get("CLAUDE_API_KEY")
openai = out.get("OPENAI_API_KEY")
zai = out.get("ZAI_API_KEY") or out.get("Z_AI_API_KEY")

if anthropic:
out["ANTHROPIC_API_KEY"] = anthropic
if openai:
out["OPENAI_API_KEY"] = openai
if zai:
out["ZAI_API_KEY"] = zai

return out


HIGH_RISK_PATTERNS = [
re.compile(r"nmap\s+.*-p-"),
re.compile(r"nmap\s+.*--script"),
re.compile(r"\bsqlmap\b"),
re.compile(r"\bmsfconsole\b"),
re.compile(r"rm\s+-rf\s+"),
re.compile(r"curl\s+.+\|\s*sh"),
]


@dataclass
class ApprovalResult:
approved: bool
reason: str


class ExecutionApprovalForwarder:
"""Forwards high-risk command approvals to Discord and Telegram."""

def __init__(self, timeout_seconds: int = 300) -> None:
self.timeout_seconds = timeout_seconds

def _is_high_risk(self, command: str) -> bool:
return any(p.search(command) for p in HIGH_RISK_PATTERNS)

async def evaluate(self, command: str, session_id: str) -> ApprovalResult:
if not self._is_high_risk(command):
return ApprovalResult(approved=True, reason="low-risk command")

message = (
"⚠️ OpenClaw approval required\n"
f"session={session_id}\n"
f"command={command}\n"
"Reply with APPROVE or DENY in your control channel."
)
await asyncio.gather(
self._notify_discord(message),
self._notify_telegram(message),
return_exceptions=True,
)

# Placeholder for channel callback integration.
# Default-safe behavior is deny-on-timeout.
await asyncio.sleep(0)
return ApprovalResult(approved=False, reason="approval pending/timeout -> deny")

Comment on lines +89 to +93
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor approval responses before rejecting risky commands

For any command matching HIGH_RISK_PATTERNS, evaluate sends notifications and then immediately returns approved=False without waiting for any callback path, so high-risk operations are always denied even if an operator responds "APPROVE" in Discord/Telegram. This makes human-in-the-loop approval non-functional and blocks planned recon actions like full-port scans.

Useful? React with 👍 / 👎.

async def _notify_discord(self, content: str) -> None:
webhook = os.getenv("OPENCLAW_DISCORD_WEBHOOK_URL")
if not webhook:
return
async with httpx.AsyncClient(timeout=10) as client:
await client.post(webhook, json={"content": content})

async def _notify_telegram(self, content: str) -> None:
token = os.getenv("OPENCLAW_TELEGRAM_BOT_TOKEN")
chat_id = os.getenv("OPENCLAW_TELEGRAM_CHAT_ID")
if not token or not chat_id:
return
url = f"https://api.telegram.org/bot{token}/sendMessage"
async with httpx.AsyncClient(timeout=10) as client:
await client.post(url, json={"chat_id": chat_id, "text": content})


class NeuroRiftOpenClawAdapter:
def __init__(self) -> None:
self.session_id = f"nr-{uuid.uuid4().hex[:12]}"
self.request_timeout = 120
self.approval_forwarder = ExecutionApprovalForwarder()

@staticmethod
def _map_method(neurorift_tool_call: Dict[str, Any]) -> str:
call_type = neurorift_tool_call.get("type")
if call_type == "run_terminal_cmd":
return "exec"
if call_type == "read_file":
return "read"
if call_type == "write_file":
return "write"
return "process"

async def _call_neurorift(self, payload: Dict[str, Any]) -> Dict[str, Any]:
async with httpx.AsyncClient(timeout=self.request_timeout) as client:
response = await client.post(f"{BRIDGE_URL}/execute", json=payload)
response.raise_for_status()
return response.json()

async def _build_rpc_frame(self, tool_call: Dict[str, Any]) -> Dict[str, Any]:
rpc_method = self._map_method(tool_call)
command_preview = json.dumps(tool_call, ensure_ascii=False)

approval = await self.approval_forwarder.evaluate(
command_preview, self.session_id
)
if not approval.approved:
return {
"type": "rpc.reject",
"id": str(uuid.uuid4()),
"session": {"id": self.session_id, "mode": "isolated"},
"error": {
"code": "approval_required",
"message": approval.reason,
},
}

bridged = await self._call_neurorift(tool_call)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Send bridge-compatible command types

_build_rpc_frame posts the raw tool_call to /execute, but _map_method indicates those calls are run_terminal_cmd/read_file/write_file; the FastAPI bridge only handles ai_generate, tool_execute, robin_search, and browser_action (modules/web/bridge_server.py, lines 64-73), so these mapped calls are returned as "Unknown command type" and then forwarded as an RPC request payload. In practice this breaks the new adapter flow for the very command families it maps.

Useful? React with 👍 / 👎.


return {
"type": "rpc.request",
"id": str(uuid.uuid4()),
"session": {
"id": self.session_id,
"mode": "isolated",
"pipeline": [
"planner",
"tool-selector/manus",
"operator",
"analyst/cursor",
],
},
"method": rpc_method,
"params": {
"source": "neurorift-fastapi",
"bridgePort": 8766,
"gatewayPort": 18789,
"yieldMs": YIELD_MS,
"payload": bridged,
},
"ts": int(time.time() * 1000),
}
Comment on lines +134 to +169
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The HTTP call to the NeuroRift FastAPI bridge in _call_neurorift is used directly in _build_rpc_frame without any error handling, so if /execute is unavailable or returns a non-2xx status (triggering httpx HTTP errors), the exception will bubble up and crash the adapter instead of returning a structured RPC error to the OpenClaw gateway. [possible bug]

Severity Level: Major ⚠️
- ❌ Adapter process crashes when NeuroRift FastAPI /execute returns error.
- ⚠️ OpenClaw gateway loses active isolated NeuroRift session connection.
- ⚠️ High-risk command approvals fail when bridge temporarily unavailable.
Suggested change
async def _build_rpc_frame(self, tool_call: Dict[str, Any]) -> Dict[str, Any]:
rpc_method = self._map_method(tool_call)
command_preview = json.dumps(tool_call, ensure_ascii=False)
approval = await self.approval_forwarder.evaluate(command_preview, self.session_id)
if not approval.approved:
return {
"type": "rpc.reject",
"id": str(uuid.uuid4()),
"session": {"id": self.session_id, "mode": "isolated"},
"error": {
"code": "approval_required",
"message": approval.reason,
},
}
bridged = await self._call_neurorift(tool_call)
return {
"type": "rpc.request",
"id": str(uuid.uuid4()),
"session": {
"id": self.session_id,
"mode": "isolated",
"pipeline": ["planner", "tool-selector/manus", "operator", "analyst/cursor"],
},
"method": rpc_method,
"params": {
"source": "neurorift-fastapi",
"bridgePort": 8766,
"gatewayPort": 18789,
"yieldMs": YIELD_MS,
"payload": bridged,
},
"ts": int(time.time() * 1000),
}
async def _build_rpc_frame(self, tool_call: Dict[str, Any]) -> Dict[str, Any]:
rpc_method = self._map_method(tool_call)
command_preview = json.dumps(tool_call, ensure_ascii=False)
approval = await self.approval_forwarder.evaluate(command_preview, self.session_id)
if not approval.approved:
return {
"type": "rpc.reject",
"id": str(uuid.uuid4()),
"session": {"id": self.session_id, "mode": "isolated"},
"error": {
"code": "approval_required",
"message": approval.reason,
},
}
try:
bridged = await self._call_neurorift(tool_call)
except httpx.HTTPError as exc:
return {
"type": "rpc.reject",
"id": str(uuid.uuid4()),
"session": {"id": self.session_id, "mode": "isolated"},
"error": {
"code": "bridge_unavailable",
"message": f"neurorift-fastapi error: {exc}",
},
}
return {
"type": "rpc.request",
"id": str(uuid.uuid4()),
"session": {
"id": self.session_id,
"mode": "isolated",
"pipeline": ["planner", "tool-selector/manus", "operator", "analyst/cursor"],
},
"method": rpc_method,
"params": {
"source": "neurorift-fastapi",
"bridgePort": 8766,
"gatewayPort": 18789,
"yieldMs": YIELD_MS,
"payload": bridged,
},
"ts": int(time.time() * 1000),
}
Steps of Reproduction ✅
1. Configure and start the OpenClaw gateway using `openclaw.json5` at
`/workspace/NeuroRift/openclaw.json5`, which defines the `neurorift-primary` agent
entrypoint as `python3 integrations/openclaw/openclaw_gateway_adapter.py` (line 23).

2. Start the adapter process so `NeuroRiftOpenClawAdapter.run()` in
`integrations/openclaw/openclaw_gateway_adapter.py:171-201` connects to the WebSocket
gateway at `GATEWAY_WS_URL` and enters the `while True` loop receiving events.

3. Ensure the NeuroRift FastAPI bridge at `BRIDGE_URL` (default `http://127.0.0.1:8766`)
is unavailable or unhealthy, e.g. do not start the FastAPI service or configure it to
return HTTP 500 for `POST /execute`, which `NeuroRiftOpenClawAdapter._call_neurorift()` at
lines 128-132 calls via `httpx.AsyncClient.post(..., json=payload)` followed by
`response.raise_for_status()`.

4. From the OpenClaw side, trigger any workflow that causes a `neurorift.tool_call` event
to be sent over the gateway WebSocket; the adapter's `run()` method (lines 190-197)
receives the message, decodes it, and for events with `type == "neurorift.tool_call"`
calls `frame = await self._build_rpc_frame(event.get("payload", {}))` where
`_build_rpc_frame()` (lines 134-169) invokes `bridged = await
self._call_neurorift(tool_call)` (line 150), which raises `httpx.HTTPError` due to the
failing `/execute` call, propagating out of `_build_rpc_frame()` and `run()` without being
caught, causing `asyncio.run(NeuroRiftOpenClawAdapter().run())` at line 201 to terminate
and the adapter WebSocket session to drop instead of returning a structured `rpc.reject`
frame to the gateway.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** integrations/openclaw/openclaw_gateway_adapter.py
**Line:** 134:169
**Comment:**
	*Possible Bug: The HTTP call to the NeuroRift FastAPI bridge in `_call_neurorift` is used directly in `_build_rpc_frame` without any error handling, so if `/execute` is unavailable or returns a non-2xx status (triggering `httpx` HTTP errors), the exception will bubble up and crash the adapter instead of returning a structured RPC error to the OpenClaw gateway.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎


async def run(self) -> None:
os.environ.update(normalize_env())

async with websockets.connect(
GATEWAY_WS_URL, ping_interval=20, ping_timeout=20
) as ws:
await ws.send(
json.dumps(
{
"type": "session.start",
"session": {
"id": self.session_id,
"mode": "isolated",
"agent": "neurorift-primary",
"memoryHook": "openclaw-session-memory",
"personaFile": "SOUL.md",
},
}
)
)

while True:
incoming = await ws.recv()
event = json.loads(incoming)
if event.get("type") != "neurorift.tool_call":
continue

frame = await self._build_rpc_frame(event.get("payload", {}))
await ws.send(json.dumps(frame))


if __name__ == "__main__":
asyncio.run(NeuroRiftOpenClawAdapter().run())
Loading
Loading