diff --git a/model_capability_check.py b/model_capability_check.py new file mode 100644 index 0000000..dae638e --- /dev/null +++ b/model_capability_check.py @@ -0,0 +1,99 @@ +"""Ollama model capability verification for autonomous NeuroRift agent mode.""" + +from __future__ import annotations + +import json +import subprocess +from typing import Any + +CAPABILITY_PROMPT = """Evaluate whether you can reliably perform the following tasks: + +* structured tool invocation +* Linux command generation +* file creation and modification +* interpreting tool outputs +* multi-step reasoning for autonomous agents + +Return a JSON response: +{ +"tool_usage": true/false, +"command_generation": true/false, +"filesystem_operations": true/false, +"multi_step_reasoning": true/false, +"agent_ready": true/false +}""" + + +def _extract_json(raw_text: str) -> dict[str, Any]: + raw_text = (raw_text or "").strip() + start = raw_text.find("{") + end = raw_text.rfind("}") + if start == -1 or end == -1 or end <= start: + raise ValueError("No JSON object found in model response") + return json.loads(raw_text[start : end + 1]) + + +def verify_model_capabilities(model_name: str) -> dict[str, Any]: + try: + proc = subprocess.run( + ["ollama", "run", model_name, CAPABILITY_PROMPT], + capture_output=True, + text=True, + timeout=120, + check=False, + ) + except FileNotFoundError: + return {"ok": False, "error": "ollama_missing", "agent_ready": False} + except Exception as exc: # defensive for unstable environments + return { + "ok": False, + "error": f"ollama_exec_error:{type(exc).__name__}", + "agent_ready": False, + } + + if proc.returncode != 0: + return { + "ok": False, + "error": f"ollama_returned_{proc.returncode}", + "stderr": proc.stderr, + "agent_ready": False, + } + + try: + parsed = _extract_json(proc.stdout) + except Exception as exc: + return { + "ok": False, + "error": f"invalid_capability_json:{type(exc).__name__}", + "raw": proc.stdout[:1000], + "agent_ready": False, + } + + required = { + "tool_usage", + "command_generation", + "filesystem_operations", + "multi_step_reasoning", + "agent_ready", + } + if not required.issubset(parsed): + return { + "ok": False, + "error": "capability_fields_missing", + "parsed": parsed, + "agent_ready": False, + } + + parsed["ok"] = bool(parsed.get("agent_ready")) + return parsed + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Check Ollama model capability for NeuroRift agent mode" + ) + parser.add_argument("--model", required=True) + args = parser.parse_args() + print(json.dumps(verify_model_capabilities(args.model), indent=2)) diff --git a/neurorift/__init__.py b/neurorift/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/channels/__init__.py b/neurorift/channels/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/channels/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/channels/channel_router.py b/neurorift/channels/channel_router.py new file mode 100644 index 0000000..97c12e6 --- /dev/null +++ b/neurorift/channels/channel_router.py @@ -0,0 +1,3 @@ +class ChannelRouter: + def route(self, msg: dict): + return msg diff --git a/neurorift/channels/cli_channel.py b/neurorift/channels/cli_channel.py new file mode 100644 index 0000000..7597ccc --- /dev/null +++ b/neurorift/channels/cli_channel.py @@ -0,0 +1,12 @@ +class Channel: + def connect(self): + return True + + def receive_message(self, payload=None): + return payload or {} + + def send_message(self, message): + return message + + def close(self): + return True diff --git a/neurorift/channels/discord_channel.py b/neurorift/channels/discord_channel.py new file mode 100644 index 0000000..7597ccc --- /dev/null +++ b/neurorift/channels/discord_channel.py @@ -0,0 +1,12 @@ +class Channel: + def connect(self): + return True + + def receive_message(self, payload=None): + return payload or {} + + def send_message(self, message): + return message + + def close(self): + return True diff --git a/neurorift/channels/telegram_channel.py b/neurorift/channels/telegram_channel.py new file mode 100644 index 0000000..7597ccc --- /dev/null +++ b/neurorift/channels/telegram_channel.py @@ -0,0 +1,12 @@ +class Channel: + def connect(self): + return True + + def receive_message(self, payload=None): + return payload or {} + + def send_message(self, message): + return message + + def close(self): + return True diff --git a/neurorift/channels/web_channel.py b/neurorift/channels/web_channel.py new file mode 100644 index 0000000..7597ccc --- /dev/null +++ b/neurorift/channels/web_channel.py @@ -0,0 +1,12 @@ +class Channel: + def connect(self): + return True + + def receive_message(self, payload=None): + return payload or {} + + def send_message(self, message): + return message + + def close(self): + return True diff --git a/neurorift/clawhub/__init__.py b/neurorift/clawhub/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/clawhub/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/clawhub/clawhub_api.py b/neurorift/clawhub/clawhub_api.py new file mode 100644 index 0000000..0d0cd7a --- /dev/null +++ b/neurorift/clawhub/clawhub_api.py @@ -0,0 +1,2 @@ +class ClawHubAPI: + base_url = "https://clawhub.example/api" diff --git a/neurorift/clawhub/clawhub_client.py b/neurorift/clawhub/clawhub_client.py new file mode 100644 index 0000000..2c4f3af --- /dev/null +++ b/neurorift/clawhub/clawhub_client.py @@ -0,0 +1,18 @@ +from pathlib import Path +import shutil + + +class ClawHubClient: + def __init__(self, cache_dir: Path): + self.cache_dir = cache_dir + cache_dir.mkdir(parents=True, exist_ok=True) + + def fetch_skill(self, skill_name: str, source_dir: Path) -> Path: + src = source_dir / skill_name + if not src.exists(): + raise FileNotFoundError(skill_name) + dst = self.cache_dir / skill_name + if dst.exists(): + shutil.rmtree(dst) + shutil.copytree(src, dst) + return dst diff --git a/neurorift/clawhub/clawhub_resolver.py b/neurorift/clawhub/clawhub_resolver.py new file mode 100644 index 0000000..c0e4d87 --- /dev/null +++ b/neurorift/clawhub/clawhub_resolver.py @@ -0,0 +1,3 @@ +class ClawHubResolver: + def resolve(self, skill_name: str) -> str: + return f"https://clawhub.example/skills/{skill_name}" diff --git a/neurorift/cli/__init__.py b/neurorift/cli/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/cli/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/cli/neurorift_cli.py b/neurorift/cli/neurorift_cli.py new file mode 100644 index 0000000..2213f6c --- /dev/null +++ b/neurorift/cli/neurorift_cli.py @@ -0,0 +1,7 @@ +"""Package-level CLI helper that delegates to global entrypoint.""" + +from __future__ import annotations + +from neurorift_cli import build_parser, install_global_wrapper, main + +__all__ = ["build_parser", "install_global_wrapper", "main"] diff --git a/neurorift/config/__init__.py b/neurorift/config/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/config/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/config/channel_config.py b/neurorift/config/channel_config.py new file mode 100644 index 0000000..e76e64f --- /dev/null +++ b/neurorift/config/channel_config.py @@ -0,0 +1 @@ +CHANNELS = ["cli", "websocket", "discord", "telegram", "api"] diff --git a/neurorift/config/model_config.py b/neurorift/config/model_config.py new file mode 100644 index 0000000..c63dafc --- /dev/null +++ b/neurorift/config/model_config.py @@ -0,0 +1 @@ +MODEL_PROVIDERS = ["deepseek", "mistral", "llama", "openai"] diff --git a/neurorift/config/settings.py b/neurorift/config/settings.py new file mode 100644 index 0000000..9f072af --- /dev/null +++ b/neurorift/config/settings.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class Settings: + data_dir: Path = Path.home() / ".neurorift" diff --git a/neurorift/core/__init__.py b/neurorift/core/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/core/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/core/agent_loop.py b/neurorift/core/agent_loop.py new file mode 100644 index 0000000..68ad02a --- /dev/null +++ b/neurorift/core/agent_loop.py @@ -0,0 +1,3 @@ +class AgentLoop: + async def handle_message(self, session, message: str) -> dict: + return {"success": True, "response": f"Session {session.session_id}: {message}"} diff --git a/neurorift/core/agent_manager.py b/neurorift/core/agent_manager.py new file mode 100644 index 0000000..ee4c879 --- /dev/null +++ b/neurorift/core/agent_manager.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass +from neurorift.core.agent_loop import AgentLoop +from neurorift.sessions.session_manager import SessionManager + + +@dataclass +class AgentHandle: + session_id: str + user_id: str + channel: str + + +class AgentManager: + def __init__(self, session_manager: SessionManager, agent_loop: AgentLoop): + self.session_manager = session_manager + self.agent_loop = agent_loop + self.handles: dict[str, AgentHandle] = {} diff --git a/neurorift/core/planner.py b/neurorift/core/planner.py new file mode 100644 index 0000000..4fc9548 --- /dev/null +++ b/neurorift/core/planner.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass + + +@dataclass +class PlannedStep: + order: int + action: str + + +class Planner: + def create_plan(self, objective: str) -> list[PlannedStep]: + return ( + [PlannedStep(order=1, action=f"Investigate: {objective}")] + if objective + else [] + ) diff --git a/neurorift/core/task_router.py b/neurorift/core/task_router.py new file mode 100644 index 0000000..2fe1b7b --- /dev/null +++ b/neurorift/core/task_router.py @@ -0,0 +1,3 @@ +class TaskRouter: + def route(self, task: str) -> dict: + return {"task": task, "route": "agent_loop"} diff --git a/neurorift/execution/__init__.py b/neurorift/execution/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/execution/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/execution/command_runner.py b/neurorift/execution/command_runner.py new file mode 100644 index 0000000..35ca49f --- /dev/null +++ b/neurorift/execution/command_runner.py @@ -0,0 +1,12 @@ +import subprocess + + +class CommandRunner: + def run(self, cmd: list[str]): + p = subprocess.run(cmd, capture_output=True, text=True, check=False) + return { + "success": p.returncode == 0, + "stdout": p.stdout, + "stderr": p.stderr, + "exit_code": p.returncode, + } diff --git a/neurorift/execution/resource_limits.py b/neurorift/execution/resource_limits.py new file mode 100644 index 0000000..8960eea --- /dev/null +++ b/neurorift/execution/resource_limits.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass +class ResourceLimits: + timeout_seconds: int = 30 + memory_limit_mb: int = 512 + cpu_seconds: int = 20 diff --git a/neurorift/execution/retry_manager.py b/neurorift/execution/retry_manager.py new file mode 100644 index 0000000..f054b0e --- /dev/null +++ b/neurorift/execution/retry_manager.py @@ -0,0 +1,9 @@ +import time + + +class RetryManager: + def __init__(self, retries=2): + self.retries = retries + + def wait(self, attempt): + time.sleep(2**attempt) diff --git a/neurorift/execution/sandbox_runner.py b/neurorift/execution/sandbox_runner.py new file mode 100644 index 0000000..3c93489 --- /dev/null +++ b/neurorift/execution/sandbox_runner.py @@ -0,0 +1,5 @@ +from neurorift.execution.command_runner import CommandRunner + + +class SandboxRunner(CommandRunner): + pass diff --git a/neurorift/gateway/__init__.py b/neurorift/gateway/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/gateway/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/gateway/api_server.py b/neurorift/gateway/api_server.py new file mode 100644 index 0000000..d74378c --- /dev/null +++ b/neurorift/gateway/api_server.py @@ -0,0 +1,3 @@ +class APIServer: + def start(self): + return True diff --git a/neurorift/gateway/auth_manager.py b/neurorift/gateway/auth_manager.py new file mode 100644 index 0000000..53ee19b --- /dev/null +++ b/neurorift/gateway/auth_manager.py @@ -0,0 +1,3 @@ +class AuthManager: + def validate(self, token: str): + return bool(token) diff --git a/neurorift/gateway/websocket_gateway.py b/neurorift/gateway/websocket_gateway.py new file mode 100644 index 0000000..aac9d46 --- /dev/null +++ b/neurorift/gateway/websocket_gateway.py @@ -0,0 +1,3 @@ +class WebSocketGateway: + def start(self): + return True diff --git a/neurorift/memory/__init__.py b/neurorift/memory/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/memory/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/memory/long_term_memory.py b/neurorift/memory/long_term_memory.py new file mode 100644 index 0000000..d4910d7 --- /dev/null +++ b/neurorift/memory/long_term_memory.py @@ -0,0 +1,6 @@ +class LongTermMemory: + def __init__(self): + self.data = {} + + def store(self, user_id: str, key: str, value): + self.data.setdefault(user_id, {})[key] = value diff --git a/neurorift/memory/memory_compaction.py b/neurorift/memory/memory_compaction.py new file mode 100644 index 0000000..9e3efb3 --- /dev/null +++ b/neurorift/memory/memory_compaction.py @@ -0,0 +1,3 @@ +class MemoryCompaction: + def compact(self, messages: list[str]): + return {"summary": " ".join(messages[:10]), "kept": messages[-10:]} diff --git a/neurorift/memory/short_term_memory.py b/neurorift/memory/short_term_memory.py new file mode 100644 index 0000000..1178461 --- /dev/null +++ b/neurorift/memory/short_term_memory.py @@ -0,0 +1,9 @@ +from collections import defaultdict, deque + + +class ShortTermMemory: + def __init__(self, limit: int = 30): + self.buffers = defaultdict(lambda: deque(maxlen=limit)) + + def add(self, sid: str, msg: str): + self.buffers[sid].append(msg) diff --git a/neurorift/memory/vector_memory.py b/neurorift/memory/vector_memory.py new file mode 100644 index 0000000..3f8c744 --- /dev/null +++ b/neurorift/memory/vector_memory.py @@ -0,0 +1,6 @@ +class VectorMemory: + def semantic_search(self, user_id: str, query: str, top_k: int = 5): + return [] + + def time_based_retrieval(self, user_id: str, limit: int = 10): + return [] diff --git a/neurorift/models/__init__.py b/neurorift/models/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/models/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/models/context_builder.py b/neurorift/models/context_builder.py new file mode 100644 index 0000000..4786a1e --- /dev/null +++ b/neurorift/models/context_builder.py @@ -0,0 +1,3 @@ +class ContextBuilder: + def build(self, session, memories): + return {"session": session.session_id, "memories": memories} diff --git a/neurorift/models/model_failover.py b/neurorift/models/model_failover.py new file mode 100644 index 0000000..d0c860b --- /dev/null +++ b/neurorift/models/model_failover.py @@ -0,0 +1,3 @@ +class ModelFailover: + def pick(self, providers): + return providers[0] if providers else "fallback" diff --git a/neurorift/models/model_router.py b/neurorift/models/model_router.py new file mode 100644 index 0000000..841a02f --- /dev/null +++ b/neurorift/models/model_router.py @@ -0,0 +1,8 @@ +class ModelRouter: + async def generate(self, prompt: str): + return { + "response": prompt, + "model": "openai", + "tokens": len(prompt.split()), + "cost": 0.0, + } diff --git a/neurorift/models/prompt_builder.py b/neurorift/models/prompt_builder.py new file mode 100644 index 0000000..ce499f1 --- /dev/null +++ b/neurorift/models/prompt_builder.py @@ -0,0 +1,3 @@ +class PromptBuilder: + def build(self, message: str, context: dict): + return f"{context}\n{message}" diff --git a/neurorift/sessions/__init__.py b/neurorift/sessions/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/sessions/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/sessions/session_context.py b/neurorift/sessions/session_context.py new file mode 100644 index 0000000..7d74f0d --- /dev/null +++ b/neurorift/sessions/session_context.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass, field +from enum import Enum +from datetime import datetime, timezone + + +class SessionState(str, Enum): + CREATED = "CREATED" + ACTIVE = "ACTIVE" + IDLE = "IDLE" + PAUSED = "PAUSED" + CLOSED = "CLOSED" + + +@dataclass +class SessionContext: + session_id: str + user_id: str + channel: str + state: SessionState = SessionState.CREATED + message_history: list[dict] = field(default_factory=list) + tool_usage: list[dict] = field(default_factory=list) + context_window: list[str] = field(default_factory=list) + memory_references: list[str] = field(default_factory=list) + created_at: str = field( + default_factory=lambda: datetime.now(timezone.utc).isoformat() + ) + updated_at: str = field( + default_factory=lambda: datetime.now(timezone.utc).isoformat() + ) diff --git a/neurorift/sessions/session_manager.py b/neurorift/sessions/session_manager.py new file mode 100644 index 0000000..23f7471 --- /dev/null +++ b/neurorift/sessions/session_manager.py @@ -0,0 +1,22 @@ +import uuid +from pathlib import Path +from neurorift.sessions.session_context import SessionContext, SessionState +from neurorift.sessions.session_store import SessionStore + + +class SessionManager: + def __init__(self, root: Path): + self.store = SessionStore(root) + self.sessions: dict[str, SessionContext] = {} + + def create_session(self, user_id: str, channel: str) -> SessionContext: + sid = str(uuid.uuid4()) + s = SessionContext( + session_id=sid, user_id=user_id, channel=channel, state=SessionState.ACTIVE + ) + self.sessions[sid] = s + self.store.save(s) + return s + + def get_session(self, sid: str): + return self.sessions.get(sid) or self.store.load(sid) diff --git a/neurorift/sessions/session_pruner.py b/neurorift/sessions/session_pruner.py new file mode 100644 index 0000000..4a5384e --- /dev/null +++ b/neurorift/sessions/session_pruner.py @@ -0,0 +1,3 @@ +class SessionPruner: + def prune(self, sessions: dict, max_idle_hours: int = 24) -> list[str]: + return [] diff --git a/neurorift/sessions/session_store.py b/neurorift/sessions/session_store.py new file mode 100644 index 0000000..1b07b61 --- /dev/null +++ b/neurorift/sessions/session_store.py @@ -0,0 +1,25 @@ +import json +from pathlib import Path +from neurorift.sessions.session_context import SessionContext + + +class SessionStore: + def __init__(self, root: Path): + self.root = root + self.root.mkdir(parents=True, exist_ok=True) + + def path(self, sid: str) -> Path: + return self.root / f"{sid}.json" + + def save(self, session: SessionContext): + self.path(session.session_id).write_text( + json.dumps(session.__dict__, default=str), encoding="utf-8" + ) + + def load(self, sid: str): + p = self.path(sid) + return ( + SessionContext(**json.loads(p.read_text(encoding="utf-8"))) + if p.exists() + else None + ) diff --git a/neurorift/skill_store/__init__.py b/neurorift/skill_store/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/skill_store/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/skill_store/cache/__init__.py b/neurorift/skill_store/cache/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/skill_store/cache/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/skill_store/installed/__init__.py b/neurorift/skill_store/installed/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/skill_store/installed/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/skill_store/installed/recon_scanner/README.md b/neurorift/skill_store/installed/recon_scanner/README.md new file mode 100644 index 0000000..ce9665d --- /dev/null +++ b/neurorift/skill_store/installed/recon_scanner/README.md @@ -0,0 +1 @@ +# recon_scanner diff --git a/neurorift/skill_store/installed/recon_scanner/__init__.py b/neurorift/skill_store/installed/recon_scanner/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/skill_store/installed/recon_scanner/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/skill_store/installed/recon_scanner/requirements.txt b/neurorift/skill_store/installed/recon_scanner/requirements.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/neurorift/skill_store/installed/recon_scanner/requirements.txt @@ -0,0 +1 @@ +requests diff --git a/neurorift/skill_store/installed/recon_scanner/skill.json b/neurorift/skill_store/installed/recon_scanner/skill.json new file mode 100644 index 0000000..9468bdd --- /dev/null +++ b/neurorift/skill_store/installed/recon_scanner/skill.json @@ -0,0 +1,8 @@ +{ + "name": "recon_scanner", + "version": "1.0", + "description": "basic reconnaissance scanner", + "entrypoint": "skill.py", + "command": "scan", + "dependencies": ["nmap"] +} diff --git a/neurorift/skill_store/installed/recon_scanner/skill.py b/neurorift/skill_store/installed/recon_scanner/skill.py new file mode 100644 index 0000000..15b4985 --- /dev/null +++ b/neurorift/skill_store/installed/recon_scanner/skill.py @@ -0,0 +1,2 @@ +def run(target: str = "127.0.0.1"): + return {"action": "scan", "target": target, "status": "simulated"} diff --git a/neurorift/skills/__init__.py b/neurorift/skills/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/skills/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/skills/installer.py b/neurorift/skills/installer.py new file mode 100644 index 0000000..5899d99 --- /dev/null +++ b/neurorift/skills/installer.py @@ -0,0 +1,35 @@ +import json, shutil +from pathlib import Path +from neurorift.skills.skill_validator import SkillValidator +from neurorift.skills.registry import SkillRegistry + + +class SkillInstaller: + def __init__(self, base: Path): + self.base = base + self.installed = base / "installed" + self.cache = base / "cache" + self.installed.mkdir(parents=True, exist_ok=True) + self.cache.mkdir(parents=True, exist_ok=True) + self.validator = SkillValidator() + self.registry = SkillRegistry(base) + + def install(self, pkg: Path): + ok, missing = self.validator.validate(pkg) + if not ok: + return {"success": False, "error": f"missing:{missing}"} + meta = json.loads((pkg / "skill.json").read_text(encoding="utf-8")) + name = meta["name"] + dst = self.installed / name + if dst.exists(): + shutil.rmtree(dst) + shutil.copytree(pkg, dst) + self.registry.add(name) + return {"success": True, "name": name, "path": str(dst)} + + def uninstall(self, name: str): + dst = self.installed / name + if dst.exists(): + shutil.rmtree(dst) + self.registry.remove(name) + return {"success": True, "name": name} diff --git a/neurorift/skills/loader.py b/neurorift/skills/loader.py new file mode 100644 index 0000000..ddb9a53 --- /dev/null +++ b/neurorift/skills/loader.py @@ -0,0 +1,12 @@ +import importlib.util, json +from pathlib import Path + + +class SkillLoader: + def run(self, skill_dir: Path, **kwargs): + meta = json.loads((skill_dir / "skill.json").read_text(encoding="utf-8")) + entry = skill_dir / meta["entrypoint"] + spec = importlib.util.spec_from_file_location(f"skill_{meta['name']}", entry) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod.run(**kwargs) if hasattr(mod, "run") else {"error": "missing run()"} diff --git a/neurorift/skills/registry.py b/neurorift/skills/registry.py new file mode 100644 index 0000000..f1b1d08 --- /dev/null +++ b/neurorift/skills/registry.py @@ -0,0 +1,32 @@ +import json +from pathlib import Path + + +class SkillRegistry: + def __init__(self, root: Path): + self.path = root / "registry.json" + root.mkdir(parents=True, exist_ok=True) + if not self.path.exists(): + self.path.write_text( + json.dumps({"installed_skills": []}, indent=2), encoding="utf-8" + ) + + def read(self): + return json.loads(self.path.read_text(encoding="utf-8")) + + def list(self): + return self.read().get("installed_skills", []) + + def add(self, name): + data = self.read() + skills = data.setdefault("installed_skills", []) + if name not in skills: + skills.append(name) + self.path.write_text(json.dumps(data, indent=2), encoding="utf-8") + + def remove(self, name): + data = self.read() + data["installed_skills"] = [ + s for s in data.get("installed_skills", []) if s != name + ] + self.path.write_text(json.dumps(data, indent=2), encoding="utf-8") diff --git a/neurorift/skills/skill_manager.py b/neurorift/skills/skill_manager.py new file mode 100644 index 0000000..602135d --- /dev/null +++ b/neurorift/skills/skill_manager.py @@ -0,0 +1,28 @@ +from pathlib import Path +from neurorift.clawhub.clawhub_client import ClawHubClient +from neurorift.skills.installer import SkillInstaller +from neurorift.skills.loader import SkillLoader + + +class SkillManager: + def __init__(self, home: Path): + self.base = home / "skills" + self.installer = SkillInstaller(self.base) + self.loader = SkillLoader() + self.client = ClawHubClient(self.installer.cache) + self.examples = ( + Path(__file__).resolve().parents[1] / "skill_store" / "installed" + ) + + def install_clawhub(self, skill_name: str): + pkg = self.client.fetch_skill(skill_name, self.examples) + return self.installer.install(pkg) + + def list(self): + return self.installer.registry.list() + + def run(self, skill_name: str, **kwargs): + return self.loader.run(self.installer.installed / skill_name, **kwargs) + + def uninstall(self, skill_name: str): + return self.installer.uninstall(skill_name) diff --git a/neurorift/skills/skill_validator.py b/neurorift/skills/skill_validator.py new file mode 100644 index 0000000..77ade78 --- /dev/null +++ b/neurorift/skills/skill_validator.py @@ -0,0 +1,9 @@ +from pathlib import Path + + +class SkillValidator: + required = ["skill.json", "skill.py", "README.md"] + + def validate(self, path: Path): + missing = [f for f in self.required if not (path / f).exists()] + return (len(missing) == 0, missing) diff --git a/neurorift/storage/__init__.py b/neurorift/storage/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/storage/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/storage/database.py b/neurorift/storage/database.py new file mode 100644 index 0000000..13d273e --- /dev/null +++ b/neurorift/storage/database.py @@ -0,0 +1,7 @@ +from pathlib import Path + + +class Database: + def __init__(self, root: Path): + self.root = root + root.mkdir(parents=True, exist_ok=True) diff --git a/neurorift/storage/logs_db.py b/neurorift/storage/logs_db.py new file mode 100644 index 0000000..0e483b4 --- /dev/null +++ b/neurorift/storage/logs_db.py @@ -0,0 +1,5 @@ +from neurorift.storage.database import Database + + +class DB(Database): + pass diff --git a/neurorift/storage/memory_db.py b/neurorift/storage/memory_db.py new file mode 100644 index 0000000..0e483b4 --- /dev/null +++ b/neurorift/storage/memory_db.py @@ -0,0 +1,5 @@ +from neurorift.storage.database import Database + + +class DB(Database): + pass diff --git a/neurorift/storage/session_db.py b/neurorift/storage/session_db.py new file mode 100644 index 0000000..0e483b4 --- /dev/null +++ b/neurorift/storage/session_db.py @@ -0,0 +1,5 @@ +from neurorift.storage.database import Database + + +class DB(Database): + pass diff --git a/neurorift/tools/__init__.py b/neurorift/tools/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/tools/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/tools/tool_executor.py b/neurorift/tools/tool_executor.py new file mode 100644 index 0000000..df4c18e --- /dev/null +++ b/neurorift/tools/tool_executor.py @@ -0,0 +1,3 @@ +class ToolExecutor: + def execute(self, tool_call: dict): + return {"success": True, "tool_call": tool_call} diff --git a/neurorift/tools/tool_registry.py b/neurorift/tools/tool_registry.py new file mode 100644 index 0000000..0abfb7c --- /dev/null +++ b/neurorift/tools/tool_registry.py @@ -0,0 +1,6 @@ +class ToolRegistry: + def __init__(self): + self.tools = {} + + def register(self, name, meta): + self.tools[name] = meta diff --git a/neurorift/tools/tool_sandbox.py b/neurorift/tools/tool_sandbox.py new file mode 100644 index 0000000..e493fff --- /dev/null +++ b/neurorift/tools/tool_sandbox.py @@ -0,0 +1,3 @@ +class ToolSandbox: + def wrap(self, command: list[str]) -> list[str]: + return command diff --git a/neurorift/tools/tool_validator.py b/neurorift/tools/tool_validator.py new file mode 100644 index 0000000..90e2e66 --- /dev/null +++ b/neurorift/tools/tool_validator.py @@ -0,0 +1,3 @@ +class ToolValidator: + def validate(self, tool_call: dict): + return bool(tool_call.get("name")), None diff --git a/neurorift/utils/__init__.py b/neurorift/utils/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/neurorift/utils/__init__.py @@ -0,0 +1 @@ + diff --git a/neurorift/utils/logger.py b/neurorift/utils/logger.py new file mode 100644 index 0000000..512527b --- /dev/null +++ b/neurorift/utils/logger.py @@ -0,0 +1,5 @@ +import logging + + +def get_logger(name="neurorift"): + return logging.getLogger(name) diff --git a/neurorift/utils/serializer.py b/neurorift/utils/serializer.py new file mode 100644 index 0000000..075d5d4 --- /dev/null +++ b/neurorift/utils/serializer.py @@ -0,0 +1,5 @@ +import json + + +def dumps(v): + return json.dumps(v, indent=2, default=str) diff --git a/neurorift/utils/time_utils.py b/neurorift/utils/time_utils.py new file mode 100644 index 0000000..d58d5da --- /dev/null +++ b/neurorift/utils/time_utils.py @@ -0,0 +1,5 @@ +from datetime import datetime, timezone + + +def utc_now_iso(): + return datetime.now(timezone.utc).isoformat() diff --git a/neurorift/utils/validator.py b/neurorift/utils/validator.py new file mode 100644 index 0000000..bb43522 --- /dev/null +++ b/neurorift/utils/validator.py @@ -0,0 +1,2 @@ +def validate_message(msg): + return isinstance(msg, str) and bool(msg.strip()) diff --git a/neurorift_cli.py b/neurorift_cli.py new file mode 100644 index 0000000..2ee6c25 --- /dev/null +++ b/neurorift_cli.py @@ -0,0 +1,68 @@ +"""Global NeuroRift CLI entrypoint with optional /usr/local/bin wrapper installer.""" + +from __future__ import annotations + +import argparse +import os +import stat +import subprocess +import sys +from pathlib import Path + +import neurorift_main + + +def install_global_wrapper() -> int: + wrapper_path = Path("/usr/local/bin/neurorift") + main_path = (Path(__file__).resolve().parent / "neurorift_main.py").resolve() + script = f'#!/usr/bin/env bash\npython3 {main_path} "$@"\n' + try: + wrapper_path.write_text(script, encoding="utf-8") + wrapper_path.chmod( + wrapper_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + ) + except PermissionError: + print("Permission denied writing /usr/local/bin/neurorift. Re-run with sudo.") + return 1 + except Exception as exc: + print(f"Failed to install wrapper: {exc}") + return 1 + + print(f"Installed global wrapper at {wrapper_path}") + return 0 + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="NeuroRift global CLI launcher") + parser.add_argument( + "--install-global-wrapper", + action="store_true", + help="Install /usr/local/bin/neurorift wrapper", + ) + parser.add_argument( + "args", + nargs=argparse.REMAINDER, + help="Arguments forwarded to neurorift_main.py", + ) + return parser + + +def main() -> int: + parser = build_parser() + ns = parser.parse_args() + + if ns.install_global_wrapper: + return install_global_wrapper() + + forwarded = ns.args or [] + if forwarded and forwarded[0] == "--": + forwarded = forwarded[1:] + + # Route into existing app entrypoint; keep compatibility with current parser. + sys.argv = ["neurorift"] + forwarded + neurorift_main.main() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/neurorift_launch.sh b/neurorift_launch.sh new file mode 100755 index 0000000..92afb86 --- /dev/null +++ b/neurorift_launch.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +MODEL_NAME="${1:-}" +BOOT_PROMPT="You are NeuroRift, an autonomous cybersecurity agent. Boot in safe authorized mode, load skills, verify tools, and await tasking." + +if ! command -v ollama >/dev/null 2>&1; then + echo "āŒ Ollama is not installed. Install it first: https://ollama.com/download" + exit 1 +fi + +if ! command -v neurorift >/dev/null 2>&1; then + echo "āŒ neurorift command not found in PATH. Install package and wrapper first." + exit 1 +fi + +if [ -z "$MODEL_NAME" ]; then + MODEL_NAME="$(ollama list 2>/dev/null | awk 'NR==2{print $1}')" +fi + +if [ -z "$MODEL_NAME" ]; then + echo "āŒ No local Ollama model found. Pull one first (example: ollama pull llama3)." + exit 1 +fi + +echo "[1/5] Verifying runtime environment..." +python3 runtime_environment_check.py || true + +echo "[2/5] Verifying model capabilities for $MODEL_NAME..." +CAP_JSON="$(python3 model_capability_check.py --model "$MODEL_NAME")" +echo "$CAP_JSON" +if ! echo "$CAP_JSON" | grep -q '"agent_ready": true'; then + echo "āŒ Model is not agent-ready. Aborting NeuroRift startup." + exit 1 +fi + +echo "[3/5] Bootstrapping model session..." +ollama run "$MODEL_NAME" "$BOOT_PROMPT" >/dev/null || true + +echo "[4/5] Starting NeuroRift agent runtime..." +neurorift run-agent --target "local-environment" --mode defensive + +echo "[5/5] NeuroRift launch sequence complete." diff --git a/neurorift_main.py b/neurorift_main.py index b17797a..2f55dbd 100755 --- a/neurorift_main.py +++ b/neurorift_main.py @@ -18,6 +18,7 @@ import argparse import subprocess import time +import shutil from datetime import datetime from pathlib import Path import logging @@ -29,7 +30,7 @@ # Load environment variables load_dotenv() -from typing import Optional +from typing import Optional, List # SECURITY: Import security utilities from utils.security_utils import ( @@ -37,13 +38,13 @@ RateLimiter, FilePermissionManager, validate_target, - sanitize_filename + sanitize_filename, ) from utils.auth import get_auth_manager, Permission from modules.recon.recon_module import EnhancedReconModule from modules.ai.ai_integration import AIAnalyzer, OllamaClient -from modules.ai.ai_orchestrator import AIOrchestrator # New Import +from modules.ai.ai_orchestrator import AIOrchestrator # New Import import modules.darkweb as darkweb_module from modules.ai.agent import NeuroRiftAgent from modules.web.web_module import WebModule @@ -52,10 +53,17 @@ from modules.session.session_manager import SessionManager from modules.session.autosave_service import AutoSaveService from modules.session.session_cli import SessionCLI, setup_session_parser -from modules.orchestration.execution_manager import ExecutionManager, ScanRequest, SessionContext +from modules.orchestration.execution_manager import ( + ExecutionManager, + ScanRequest, + SessionContext, +) from modules.ai.agents import NRPlanner, NROperator, NRAnalyst, NRScribe from modules.tools.base import ToolMode from modules.config.config_wizard import ConfigWizard +from neurorift.skills.skill_manager import SkillManager +from model_capability_check import verify_model_capabilities +from runtime_environment_check import verify_runtime_environment class NeuroRift: @@ -64,13 +72,14 @@ def __init__(self): self.base_dir = Path.home() / ".neurorift" self.results_dir = self.base_dir / "results" self.tools_dir = self.base_dir / "tools" + self.setup_directories() self.setup_logging() self.console = Console() # Initialize Session Management self.session_manager = SessionManager() self.auto_save = AutoSaveService(self.session_manager) - + # Initialize AI components self.ollama = OllamaClient() self.ai_analyzer = AIAnalyzer(self.ollama) @@ -79,7 +88,7 @@ def __init__(self): self.web_module = WebModule(self.base_dir, self.ai_analyzer) self.exploit_module = ExploitModule(self.base_dir, self.ai_analyzer) self.scan_module = ScanModule(self.base_dir, self.ai_analyzer) - + # Orchestration Components self.execution_manager = ExecutionManager(self.session_manager) self.planner = NRPlanner(self.ollama) @@ -95,6 +104,7 @@ def setup_directories(self): def setup_logging(self): """Setup logging configuration""" + self.base_dir.mkdir(parents=True, exist_ok=True) log_file = self.base_dir / "neurorift.log" logging.basicConfig( level=logging.INFO, @@ -119,7 +129,7 @@ def banner(self): self.console.print(Panel(banner_text, style="bold blue")) def check_tools(self): - """Check if required tools are installed""" + """Check if required tools are installed and executable.""" required_tools = [ "nmap", "subfinder", @@ -133,66 +143,492 @@ def check_tools(self): missing_tools = [] for tool in required_tools: - if not self.is_tool_installed(tool): + if not self.is_tool_installed(tool, verify_execution=True): missing_tools.append(tool) if missing_tools: - self.logger.warning("Missing tools: %s", ", ".join(missing_tools)) + self.logger.warning( + "tool_check_failed", + extra={"missing_tools": missing_tools, "count": len(missing_tools)}, + ) + self.console.print( + "[bold yellow]Missing or unusable tools:[/bold yellow] " + + ", ".join(missing_tools) + ) return False + + self.logger.info( + "tool_check_succeeded", extra={"checked_tools": required_tools} + ) return True - def is_tool_installed(self, tool): - """Check if a tool is installed""" + # SAFETY CHECKS ADDED: hardened, centralized command runner with sandboxing, + # resource limits, retries, and output validation. + def _get_restricted_env(self) -> dict: + """Create a minimal execution environment for subprocesses.""" + return { + "PATH": os.environ.get("PATH", "/usr/bin:/bin"), + "HOME": str(self.base_dir), + "LANG": "C.UTF-8", + "LC_ALL": "C.UTF-8", + "PYTHONUNBUFFERED": "1", + } + + def _is_safe_argument(self, value: str) -> bool: + """Reject arguments containing control chars often used in injection payloads.""" + if not isinstance(value, str) or not value.strip(): + return False + blocked_chars = {"\x00", "\n", "\r"} + return not any(char in value for char in blocked_chars) + + def _is_safe_workdir(self, cwd: Optional[Path]) -> bool: + """Ensure command working directory is controlled and not system-critical.""" + if cwd is None: + return True try: - subprocess.run( - ["which", tool], - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) + resolved = cwd.expanduser().resolve() + except (OSError, RuntimeError): + return False + + critical_roots = { + Path("/"), + Path("/etc"), + Path("/usr"), + Path("/bin"), + Path("/sbin"), + Path("/lib"), + Path("/lib64"), + Path("/boot"), + } + if resolved in critical_roots: + return False + + return str(resolved).startswith(str(self.base_dir.resolve())) or str( + resolved + ).startswith("/tmp") + + def _build_preexec_limits( + self, cpu_time_limit: int, memory_limit_mb: int, max_fds: int + ): + """Build a POSIX pre-exec function for process resource limits.""" + if os.name != "posix": + return None + + try: + import resource + except ImportError: + self.logger.warning("resource_module_unavailable") + return None + + def _apply_limits(): + try: + if cpu_time_limit > 0: + resource.setrlimit( + resource.RLIMIT_CPU, (cpu_time_limit, cpu_time_limit + 1) + ) + if memory_limit_mb > 0: + memory_bytes = memory_limit_mb * 1024 * 1024 + resource.setrlimit(resource.RLIMIT_AS, (memory_bytes, memory_bytes)) + if max_fds > 0: + resource.setrlimit(resource.RLIMIT_NOFILE, (max_fds, max_fds)) + except Exception: + # Avoid failing parent flow if hard limit setting is blocked by runtime. + pass + + return _apply_limits + + def _sanitize_output(self, output: str, max_output_len: int) -> str: + """Sanitize process output and cap size to avoid parser / memory abuse.""" + text = output or "" + text = text.replace("\x00", "") + if len(text) > max_output_len: + return text[:max_output_len] + return text + + def _validate_tool_output( + self, stdout: str, stderr: str, expect_json: bool = False + ) -> dict: + """Validate and sanitize subprocess output before downstream usage.""" + validation_passed = True + error_type = None + + if stdout is None: + stdout = "" + if stderr is None: + stderr = "" + + if not isinstance(stdout, str) or not isinstance(stderr, str): + validation_passed = False + error_type = "encoding_error" + stdout = str(stdout) + stderr = str(stderr) + + if not stdout.strip() and not stderr.strip(): + validation_passed = False + error_type = error_type or "empty_output" + + if expect_json and stdout.strip(): + try: + json.loads(stdout) + except json.JSONDecodeError: + validation_passed = False + error_type = "malformed_json" + + return { + "validation_passed": validation_passed, + "error_type": error_type, + "stdout": stdout, + "stderr": stderr, + } + + def _should_retry( + self, error_type: Optional[str], stderr: str, exit_code: Optional[int] + ) -> bool: + """Retry only for transient errors and never for permanent argument/permission failures.""" + if error_type in { + "permission_denied", + "binary_not_found", + "invalid_arguments", + "invalid_workdir", + }: + return False + + stderr_lc = (stderr or "").lower() + transient_markers = ( + "tempor", + "timeout", + "connection reset", + "network", + "try again", + "i/o timeout", + ) + + if error_type in {"timeout", "transient_failure", "resource_limit_exceeded"}: + return True + if exit_code in {75, 110, 111}: return True - except subprocess.CalledProcessError: + return any(marker in stderr_lc for marker in transient_markers) + + def _run_command_secure( + self, + command: List[str], + *, + timeout: int = 30, + cwd: Optional[Path] = None, + expect_json: bool = False, + retries: int = 2, + memory_limit_mb: int = 512, + cpu_time_limit: int = 20, + max_fds: int = 256, + max_output_len: int = 200_000, + use_linux_sandbox: bool = False, + ) -> dict: + """Secure, fault-tolerant subprocess runner for all tool execution.""" + start = time.time() + + if not isinstance(command, list) or not command: + return { + "success": False, + "stdout": "", + "stderr": "Invalid command format", + "exit_code": -1, + "error_type": "invalid_arguments", + "validation_passed": False, + } + + if any(not self._is_safe_argument(part) for part in command): + return { + "success": False, + "stdout": "", + "stderr": "Unsafe command arguments detected", + "exit_code": -1, + "error_type": "invalid_arguments", + "validation_passed": False, + } + + if cwd is not None and not self._is_safe_workdir(cwd): + return { + "success": False, + "stdout": "", + "stderr": f"Unsafe working directory: {cwd}", + "exit_code": -1, + "error_type": "invalid_workdir", + "validation_passed": False, + } + + base_cmd = list(command) + if use_linux_sandbox and os.name == "posix": + firejail = shutil.which("firejail") + if firejail: + base_cmd = [firejail, "--quiet", "--noprofile", "--private"] + base_cmd + + attempt = 0 + max_attempts = max(1, retries + 1) + last_result = None + + while attempt < max_attempts: + attempt += 1 + self.logger.info( + "tool_execution_start", + extra={"tool": base_cmd[0], "attempt": attempt, "command": base_cmd}, + ) + + try: + proc = subprocess.run( + base_cmd, + shell=False, + check=False, + timeout=timeout, + capture_output=True, + text=True, + env=self._get_restricted_env(), + cwd=str(cwd) if cwd else str(self.base_dir), + preexec_fn=self._build_preexec_limits( + cpu_time_limit, memory_limit_mb, max_fds + ), + ) + + stdout = self._sanitize_output(proc.stdout, max_output_len) + stderr = self._sanitize_output(proc.stderr, max_output_len) + validation = self._validate_tool_output( + stdout, stderr, expect_json=expect_json + ) + success = proc.returncode == 0 and validation["validation_passed"] + + last_result = { + "success": success, + "stdout": validation["stdout"], + "stderr": validation["stderr"], + "exit_code": proc.returncode, + "error_type": validation["error_type"] if not success else None, + "validation_passed": validation["validation_passed"], + } + + duration_ms = int((time.time() - start) * 1000) + self.logger.info( + "tool_execution_complete", + extra={ + "tool": base_cmd[0], + "attempt": attempt, + "duration_ms": duration_ms, + "exit_code": proc.returncode, + "success": success, + }, + ) + + if success: + return last_result + + retryable = self._should_retry( + last_result["error_type"], stderr, proc.returncode + ) + if attempt < max_attempts and retryable: + delay = 2**attempt + self.logger.warning( + "tool_execution_retry", + extra={ + "tool": base_cmd[0], + "attempt": attempt, + "next_delay_s": delay, + }, + ) + time.sleep(delay) + continue + return last_result + + except FileNotFoundError as exc: + last_result = { + "success": False, + "stdout": "", + "stderr": str(exc), + "exit_code": 127, + "error_type": "binary_not_found", + "validation_passed": False, + } + except PermissionError as exc: + last_result = { + "success": False, + "stdout": "", + "stderr": str(exc), + "exit_code": 126, + "error_type": "permission_denied", + "validation_passed": False, + } + except subprocess.TimeoutExpired as exc: + stdout = self._sanitize_output(exc.stdout or "", max_output_len) + stderr = self._sanitize_output(exc.stderr or "", max_output_len) + last_result = { + "success": False, + "stdout": stdout, + "stderr": stderr or f"Command timed out after {timeout}s", + "exit_code": 124, + "error_type": "timeout", + "validation_passed": False, + } + except OSError as exc: + last_result = { + "success": False, + "stdout": "", + "stderr": str(exc), + "exit_code": -1, + "error_type": "os_error", + "validation_passed": False, + } + + self.logger.error( + "tool_execution_failed", + extra={ + "tool": base_cmd[0] if base_cmd else "unknown", + "attempt": attempt, + "error_type": last_result["error_type"], + "stderr": last_result["stderr"], + }, + ) + + if attempt < max_attempts and self._should_retry( + last_result.get("error_type"), + last_result.get("stderr", ""), + last_result.get("exit_code"), + ): + delay = 2**attempt + self.logger.warning( + "tool_execution_retry", + extra={ + "tool": base_cmd[0], + "attempt": attempt, + "next_delay_s": delay, + }, + ) + time.sleep(delay) + continue + + return last_result + + return last_result or { + "success": False, + "stdout": "", + "stderr": "Unknown execution error", + "exit_code": -1, + "error_type": "unknown", + "validation_passed": False, + } + + def _verify_tool_capability(self, tool: str) -> bool: + """Run a safe command to verify executable capability and environment support.""" + result = self._run_command_secure([tool, "--help"], timeout=10, retries=0) + if not result["success"] and result["exit_code"] not in (1, 2): + self.logger.warning( + "tool_capability_check_failed", + extra={ + "tool": tool, + "error_type": result.get("error_type"), + "stderr": result.get("stderr"), + }, + ) + return False + return True + + def is_tool_installed(self, tool: str, verify_execution: bool = False) -> bool: + """Check if a tool is installed, executable, and optionally usable.""" + if not isinstance(tool, str) or not tool.strip(): + self.logger.error("invalid_tool_name", extra={"tool": tool}) + return False + + tool = tool.strip() + resolved_path = shutil.which(tool) + if not resolved_path: + self.logger.warning("tool_not_found", extra={"tool": tool}) + return False + + if not os.access(resolved_path, os.X_OK): + self.logger.warning( + "tool_not_executable", extra={"tool": tool, "path": resolved_path} + ) + return False + + if verify_execution and not self._verify_tool_capability(tool): + return False + + self.logger.info("tool_verified", extra={"tool": tool, "path": resolved_path}) + return True + + def _validate_dependency(self, package_manager: str) -> bool: + """Validate that a required package manager is available and executable.""" + if not self.is_tool_installed(package_manager, verify_execution=False): + self.console.print( + f"[bold red]Missing dependency:[/bold red] '{package_manager}' is required. " + f"Install it first (for Debian/Ubuntu: sudo apt install {package_manager})." + ) return False + return True def install_missing_tools(self): - """Install missing tools""" + """Install missing tools with fail-safe behavior and clear diagnostics.""" self.logger.info("Installing missing tools...") - # Update package list - try: - subprocess.run(["sudo", "apt", "update"], check=True) - except subprocess.CalledProcessError as e: - self.logger.error("Failed to update package list: %s", e) + if not self._validate_dependency("sudo") or not self._validate_dependency( + "apt" + ): + return False + + update_result = self._run_command_secure( + ["sudo", "apt", "update"], timeout=180, retries=1 + ) + if not update_result["success"]: + self.logger.error( + "apt_update_failed", extra={"stderr": update_result.get("stderr")} + ) return False - # Install Go tools go_tools = { "subfinder": "github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest", "httpx": "github.com/projectdiscovery/httpx/cmd/httpx@latest", "nuclei": "github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest", } + if not self._validate_dependency("go"): + self.console.print( + "[bold yellow]Skipping Go-based tools installation because Go is missing.[/bold yellow]" + ) + return False + + all_successful = True for tool, package in go_tools.items(): if not self.is_tool_installed(tool): self.logger.info("Installing %s...", tool) - try: - # SECURITY FIX: Use full executable path and handle subprocess failures - result = subprocess.run(["/usr/bin/go", "install", package], - capture_output=True, text=True, check=True) - self.logger.info("Successfully installed %s", tool) - except subprocess.CalledProcessError as e: - self.logger.error("Failed to install %s: %s", tool, e) - self.logger.error("stderr: %s", e.stderr) - return False - except FileNotFoundError: - self.logger.error("Go not found. Please install Go first.") - return False + install_result = self._run_command_secure( + ["go", "install", package], + timeout=300, + retries=1, + memory_limit_mb=1024, + cpu_time_limit=120, + ) + if install_result["success"]: + self.logger.info( + "go_tool_installed", extra={"tool": tool, "package": package} + ) + else: + all_successful = False + self.console.print( + f"[bold yellow]Failed to install {tool}.[/bold yellow] " + "Continuing with remaining tools." + ) + self.logger.error( + "go_tool_install_failed", + extra={ + "tool": tool, + "package": package, + "stderr": install_result.get("stderr"), + }, + ) - return True + return all_successful async def run_recon(self, target: str, output_dir: Optional[Path] = None): """Run reconnaissance on target""" - recon = EnhancedReconModule(self.base_dir, self.ai_analyzer, config_path="configs/tools.json") + recon = EnhancedReconModule( + self.base_dir, self.ai_analyzer, config_path="configs/tools.json" + ) return await recon.run_recon(target, output_dir) def ask_ai(self, question: str): @@ -211,9 +647,9 @@ def ask_ai(self, question: str): ) @RateLimiter(max_calls=5, time_window=60) - def generate_tool(self, description: str, identifier: str = 'default'): + def generate_tool(self, description: str, identifier: str = "default"): """Generate a custom tool using AI and save it to custom_tools directory. - + Args: description: Tool description identifier: Rate limit identifier (username/session) @@ -223,12 +659,12 @@ def generate_tool(self, description: str, identifier: str = 'default'): if not description or not isinstance(description, str): self.logger.error("Invalid tool description") return - + # SECURITY: Limit description length if len(description) > 500: self.logger.error("Tool description too long (max 500 chars)") return - + # Use os.path.expanduser to properly handle home directory tool_dir = Path.home() / ".neurorift" / "custom_tools" self.console.print( @@ -262,10 +698,14 @@ def generate_tool(self, description: str, identifier: str = 'default'): base_name = re.sub(r"[^a-zA-Z0-9]+", "_", description.strip().lower())[ :32 ].strip("_") - filename = sanitize_filename(f"{base_name or 'custom_tool'}_{int(time.time())}.py") - + filename = sanitize_filename( + f"{base_name or 'custom_tool'}_{int(time.time())}.py" + ) + # SECURITY: Validate path to prevent traversal - tool_path = SecurityValidator.sanitize_path(str(tool_dir / filename), base_dir=tool_dir) + tool_path = SecurityValidator.sanitize_path( + str(tool_dir / filename), base_dir=tool_dir + ) if not tool_path: self.logger.error("Invalid tool path") return @@ -273,7 +713,7 @@ def generate_tool(self, description: str, identifier: str = 'default'): # Write the generated tool to file with open(tool_path, "w", encoding="utf-8") as f: f.write(response) - + # SECURITY: Set secure file permissions (0o600) for generated tool files FilePermissionManager.set_secure_permissions(tool_path, mode=0o600) self.console.print( @@ -319,13 +759,13 @@ def list_custom_tools(self): try: # SECURITY: Use Path and validate directory tool_dir = Path.home() / ".neurorift" / "custom_tools" - + # SECURITY: Validate path tool_dir = SecurityValidator.sanitize_path(str(tool_dir)) if not tool_dir: self.logger.error("Invalid tool directory path") return - + metadata_path = tool_dir / "metadata.json" if not tool_dir.exists(): @@ -370,7 +810,6 @@ def list_custom_tools(self): self.logger.error("Unexpected error in list_custom_tools: %s", e) return [] ->>>>>>> main async def dev_mode_shell(vf, session_dir): console = Console() @@ -462,7 +901,7 @@ def get_parser(): For detailed documentation, visit: https://github.com/demonking369/NeuroRift """, - formatter_class=argparse.RawDescriptionHelpFormatter + formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument("--target", "-t", help="Target domain or IP") parser.add_argument( @@ -498,89 +937,146 @@ def get_parser(): "--stealth", "-s", action="store_true", help="Enable stealth mode" ) parser.add_argument( - "--ai-pipeline", action="store_true", help="Enable the advanced multi-prompt AI pipeline." + "--ai-pipeline", + action="store_true", + help="Enable the advanced multi-prompt AI pipeline.", ) parser.add_argument( - "--prompt-dir", help="Directory for the AI pipeline prompts.", default="prompts/system_prompts" + "--prompt-dir", + help="Directory for the AI pipeline prompts.", + default="prompts/system_prompts", ) parser.add_argument( - "--uninstall", action="store_true", help="Uninstall NeuroRift and its components" + "--uninstall", + action="store_true", + help="Uninstall NeuroRift and its components", ) parser.add_argument( - "--webmod", action="store_true", help="Launch NeuroRift web interface (Streamlit UI)" + "--webmod", + action="store_true", + help="Launch NeuroRift web interface (Streamlit UI)", ) parser.add_argument( - "--web-host", default="localhost", help="Web interface host (default: localhost)" + "--web-host", + default="localhost", + help="Web interface host (default: localhost)", ) parser.add_argument( "--web-port", type=int, default=8501, help="Web interface port (default: 8501)" ) parser.add_argument( - "--agentic", "--ai-agent", action="store_true", help="Enable simple agentic AI mode (deprecated, use --orchestrated)" + "--agentic", + "--ai-agent", + action="store_true", + help="Enable simple agentic AI mode (deprecated, use --orchestrated)", ) parser.add_argument( - "--orchestrated", action="store_true", help="šŸ†• Enable NeuroRift Orchestrated Intelligence Mode (multi-agent)" + "--orchestrated", + action="store_true", + help="šŸ†• Enable NeuroRift Orchestrated Intelligence Mode (multi-agent)", ) parser.add_argument( "--mode", choices=["offensive", "defensive"], - help="šŸ†• NeuroRift operational mode: 'offensive' (discovery) or 'defensive' (analysis/mitigation)" + help="šŸ†• NeuroRift operational mode: 'offensive' (discovery) or 'defensive' (analysis/mitigation)", ) parser.add_argument( - "--resume", metavar="TASK_ID", help="šŸ†• Resume a previously interrupted NeuroRift task" + "--resume", + metavar="TASK_ID", + help="šŸ†• Resume a previously interrupted NeuroRift task", ) parser.add_argument( - "--analyze", metavar="FILE", help="šŸ†• Analyze existing scan results (DEFENSIVE mode)" + "--analyze", + metavar="FILE", + help="šŸ†• Analyze existing scan results (DEFENSIVE mode)", ) parser.add_argument( "--no-ai", action="store_true", help="Disable AI analysis for the current mode" ) parser.add_argument( - "--configure", action="store_true", help="šŸ†• Launch interactive configuration wizard" + "--configure", + action="store_true", + help="šŸ†• Launch interactive configuration wizard", + ) + parser.add_argument("--clawhub", help="Install a skill from ClawHub by name") + parser.add_argument( + "--skill", + nargs="+", + help="Skill operations: list | run | uninstall ", ) # Add subparsers for commands - subparsers = parser.add_subparsers(dest='command', help='Available commands') - + subparsers = parser.add_subparsers(dest="command", help="Available commands") + # Ask AI command - ask_ai_parser = subparsers.add_parser('ask-ai', help='Ask the AI assistant a question') - ask_ai_parser.add_argument('question', help='The question to ask the AI') - ask_ai_parser.add_argument('--verbose', action='store_true', help='Show detailed model logs') - ask_ai_parser.add_argument('--dangerous', action='store_true', help='Enable dangerous mode') - ask_ai_parser.add_argument('--confirm-danger', action='store_true', help='Confirm dangerous mode') - + ask_ai_parser = subparsers.add_parser( + "ask-ai", help="Ask the AI assistant a question" + ) + ask_ai_parser.add_argument("question", help="The question to ask the AI") + ask_ai_parser.add_argument( + "--verbose", action="store_true", help="Show detailed model logs" + ) + ask_ai_parser.add_argument( + "--dangerous", action="store_true", help="Enable dangerous mode" + ) + ask_ai_parser.add_argument( + "--confirm-danger", action="store_true", help="Confirm dangerous mode" + ) + # Generate tool command - generate_tool_parser = subparsers.add_parser('generate-tool', help='Generate a custom tool') - generate_tool_parser.add_argument('description', help='Description of the tool to generate') - generate_tool_parser.add_argument('--verbose', action='store_true', help='Show detailed generation logs') - + generate_tool_parser = subparsers.add_parser( + "generate-tool", help="Generate a custom tool" + ) + generate_tool_parser.add_argument( + "description", help="Description of the tool to generate" + ) + generate_tool_parser.add_argument( + "--verbose", action="store_true", help="Show detailed generation logs" + ) + # List tools command - list_tools_parser = subparsers.add_parser('list-tools', help='List all custom tools') - list_tools_parser.add_argument('--verbose', action='store_true', help='Show detailed tool information') + list_tools_parser = subparsers.add_parser( + "list-tools", help="List all custom tools" + ) + list_tools_parser.add_argument( + "--verbose", action="store_true", help="Show detailed tool information" + ) + + # Run autonomous agent command + run_agent_parser = subparsers.add_parser( + "run-agent", help="Start NeuroRift autonomous agent runtime" + ) + run_agent_parser.add_argument("--target", help="Agent target/context") + run_agent_parser.add_argument( + "--mode", choices=["offensive", "defensive"], default="defensive" + ) + run_agent_parser.add_argument("--model", help="Ollama model for capability check") # Dark web OSINT command (Robin integration) darkweb_parser = subparsers.add_parser( - 'darkweb', help='Run the Robin dark web OSINT workflow' + "darkweb", help="Run the Robin dark web OSINT workflow" ) - darkweb_parser.add_argument('--query', '-q', required=True, help='Dark web search query') darkweb_parser.add_argument( - '--model', - '-m', + "--query", "-q", required=True, help="Dark web search query" + ) + darkweb_parser.add_argument( + "--model", + "-m", choices=darkweb_module.get_robin_model_choices(), default=darkweb_module.ROBIN_DEFAULT_MODEL, - help='LLM model to use for refinement/filtering', + help="LLM model to use for refinement/filtering", ) darkweb_parser.add_argument( - '--threads', - '-t', + "--threads", + "-t", type=int, default=5, - help='Number of concurrent requests for search/scrape', + help="Number of concurrent requests for search/scrape", ) darkweb_parser.add_argument( - '--output', - '-o', - help='Optional output file or directory for the markdown report', + "--output", + "-o", + help="Optional output file or directory for the markdown report", ) # Session management commands @@ -589,11 +1085,76 @@ def get_parser(): args = parser.parse_args() return parser, args + async def _async_main(args): # Initialize NeuroRift vf = NeuroRift() vf.banner() + # Modular skill system integration (ClawHub/local registry) + # Merge-conflict resolution: maintain a single authoritative skill-handling path here. + skill_manager = SkillManager(Path.home() / ".neurorift") + + if args.clawhub: + result = skill_manager.install_clawhub(args.clawhub.strip()) + if result.get("success"): + print(f"āœ“ Installed skill from ClawHub: {result.get('name')}") + else: + print(f"āœ— Failed to install skill: {result.get('error')}") + return + + if args.skill: + action = (args.skill[0] or "").lower() + if action == "list": + print(json.dumps({"installed_skills": skill_manager.list()}, indent=2)) + return + if action == "run" and len(args.skill) > 1: + skill_name = args.skill[1] + run_result = skill_manager.run( + skill_name, target=args.target or "127.0.0.1" + ) + print(json.dumps({"skill": skill_name, "result": run_result}, indent=2)) + return + if action == "uninstall" and len(args.skill) > 1: + skill_name = args.skill[1] + print(json.dumps(skill_manager.uninstall(skill_name), indent=2)) + return + print( + "Invalid --skill usage. Examples: --skill list | --skill run recon_scanner | --skill uninstall recon_scanner" + ) + return + + if args.command == "run-agent": + env_report = verify_runtime_environment(Path.home() / ".neurorift") + if not env_report.get("ok"): + print("āŒ Runtime environment check failed.") + for check in env_report.get("tools", []): + if not check.get("ok"): + print( + f"- {check.get('tool')}: {check.get('error')} | {check.get('install_hint')}" + ) + return + + model_name = args.model or os.getenv("OLLAMA_MODEL") + if model_name: + capability = verify_model_capabilities(model_name) + if not capability.get("agent_ready"): + print( + "āŒ Selected model is not agent-ready for autonomous NeuroRift execution." + ) + print(json.dumps(capability, indent=2)) + return + else: + print( + "āš ļø No model specified (--model or OLLAMA_MODEL). Skipping model capability check." + ) + + args.orchestrated = True + if not args.target: + args.target = "local-environment" + if not args.mode: + args.mode = "defensive" + # Set logging level based on verbose flag if args.verbose: vf.logger.setLevel(logging.DEBUG) @@ -607,23 +1168,23 @@ async def _async_main(args): # Handle Session commands if args.command == "session": session_cli = SessionCLI(vf.session_manager) - if args.session_command == 'new': + if args.session_command == "new": session_cli.cmd_new(args) - elif args.session_command == 'save': + elif args.session_command == "save": session_cli.cmd_save(args) - elif args.session_command == 'list': + elif args.session_command == "list": session_cli.cmd_list(args) - elif args.session_command == 'load': + elif args.session_command == "load": session_cli.cmd_load(args) - elif args.session_command == 'resume': + elif args.session_command == "resume": session_cli.cmd_resume(args) - elif args.session_command == 'delete': + elif args.session_command == "delete": session_cli.cmd_delete(args) - elif args.session_command == 'rename': + elif args.session_command == "rename": session_cli.cmd_rename(args) - elif args.session_command == 'status': + elif args.session_command == "status": session_cli.cmd_status(args) - elif args.session_command == 'export': + elif args.session_command == "export": session_cli.cmd_export(args) return @@ -635,78 +1196,86 @@ async def _async_main(args): if not vf.session_manager.current_session_id: session_name = f"Scan: {args.target}" if args.target else "New Operation" vf.session_manager.create_session( - name=session_name, - mode=args.mode or "offensive" + name=session_name, mode=args.mode or "offensive" ) # Handle Orchestrated Mode if args.orchestrated: - vf.console.print(Panel("[bold green]NeuroRift Orchestrated Intelligence Mode[/bold green]", style="bold blue")) - + vf.console.print( + Panel( + "[bold green]NeuroRift Orchestrated Intelligence Mode[/bold green]", + style="bold blue", + ) + ) + target = args.target if not target: # Try to get from session session = vf.session_manager.get_current_session() if session: target = session.get("task_state", {}).get("target") - + if not target: target = input("Enter target: ").strip() - + if not target: - print("Target required.") - return + print("Target required.") + return # Setup context - tool_mode = ToolMode.OFFENSIVE if args.mode == "offensive" else ToolMode.DEFENSIVE - + tool_mode = ( + ToolMode.OFFENSIVE if args.mode == "offensive" else ToolMode.DEFENSIVE + ) + # Ensure session exists if not vf.session_manager.current_session_id: - vf.session_manager.create_session(name=f"Assessment on {target}", mode=tool_mode.value) - + vf.session_manager.create_session( + name=f"Assessment on {target}", mode=tool_mode.value + ) + context = SessionContext( session_id=vf.session_manager.current_session_id, mode=tool_mode, - target=target + target=target, ) - + task_desc = f"Perform a {tool_mode.value} security assessment on {target}" vf.console.print(f"[bold]Task:[/bold] {task_desc}") - + # 1. Plan available_tools = vf.execution_manager.list_tools() vf.console.print("[bold blue]Planning execution...[/bold blue]") requests = await vf.planner.create_plan(task_desc, available_tools) - + if not requests: vf.console.print("[red]Failed to generate plan.[/red]") return - + vf.console.print(f"[green]Plan generated with {len(requests)} steps.[/green]") for i, req in enumerate(requests): - print(f"{i+1}. {req.tool_name} {req.args}") - - if input("\nApprove plan? (Y/n): ").lower() == 'n': + print(f"{i+1}. {req.tool_name} {req.args}") + + if input("\nApprove plan? (Y/n): ").lower() == "n": print("Aborted.") return # 2. Execute vf.console.print("\n[bold blue]Executing plan...[/bold blue]") results = await vf.operator.execute_plan(requests, context) - + # 3. Analyze vf.console.print("\n[bold blue]Analyzing results...[/bold blue]") findings = await vf.analyst.analyze_results(results) - + # 4. Report vf.console.print("\n[bold blue]Generating report...[/bold blue]") report = await vf.scribe.generate_report(task_desc, findings) - + # Save report report_path = vf.results_dir / f"report_{context.session_id}.md" with open(report_path, "w") as f: f.write(report) - + vf.console.print(Panel(report[:1000] + "\\n...", title="Report Preview")) vf.console.print(f"[green]Full report saved to {report_path}[/green]") return @@ -715,11 +1284,11 @@ async def _async_main(args): if args.uninstall: script_dir = Path(__file__).parent uninstall_script = script_dir / "uninstall_script.sh" - + if not uninstall_script.exists(): print(f"Error: Uninstall script not found at {uninstall_script}") return - + try: subprocess.run([str(uninstall_script)], check=True) except subprocess.CalledProcessError as e: @@ -731,14 +1300,16 @@ async def _async_main(args): # Handle AI Pipeline Mode if args.ai_pipeline: if not args.target: - print("Error: A target is required for AI pipeline mode, e.g., --target 'scan example.com'") + print( + "Error: A target is required for AI pipeline mode, e.g., --target 'scan example.com'" + ) return - + prompt_path = Path(args.prompt_dir) if not prompt_path.exists(): print(f"Error: Prompt directory not found at '{prompt_path}'") return - + orchestrator = AIOrchestrator(prompt_path) orchestrator.execute_task(f"Perform a security scan on {args.target}") return @@ -755,8 +1326,10 @@ async def _async_main(args): vf.console.print("\n[bold cyan]--- Agentic Action Plan ---[/bold cyan]") vf.console.print(json.dumps(result, indent=2)) else: - vf.console.print("\n[bold yellow]Agentic mode enabled. Ready for instructions...[/bold yellow]") - + vf.console.print( + "\n[bold yellow]Agentic mode enabled. Ready for instructions...[/bold yellow]" + ) + # If we are not in web mode, we might want an interactive CLI loop here # For now, we'll just continue to respect other flags. @@ -781,9 +1354,11 @@ async def _async_main(args): # Check if Robin is available if not darkweb_module.ROBIN_AVAILABLE: print("āŒ Error: Robin module dependencies not installed.") - print("Install with: pip install langchain-core langchain-openai langchain-ollama") + print( + "Install with: pip install langchain-core langchain-openai langchain-ollama" + ) return - + darkweb_module.run_darkweb_osint( args.query, model=args.model, @@ -814,7 +1389,7 @@ async def _async_main(args): "Use --target to specify a domain or IP address you own or have authorization to test" ) return - + # SECURITY: Validate target input if not validate_target(args.target): print(f"Error: Invalid target format: {args.target}") @@ -837,11 +1412,11 @@ async def _async_main(args): # SECURITY: Create session directory with secure permissions timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - + # SECURITY: Sanitize target for use in path safe_target = sanitize_filename(args.target) session_dir = vf.base_dir / "sessions" / safe_target / timestamp - + # SECURITY: Create with restricted permissions if not FilePermissionManager.create_secure_directory(session_dir, mode=0o700): print("Error: Failed to create secure session directory") @@ -896,36 +1471,44 @@ async def _async_main(args): elif args.operation_mode == "scan": print(f"\nšŸ“” Starting port scan on {args.target}") - + results = await vf.scan_module.run_scan(args.target, session_dir, use_ai=False) console = Console() console.print("\n[bold green]Scan Complete![/bold green]") console.print(f"Found {len(results['ports'])} open ports") - if results['ports']: + if results["ports"]: from rich.table import Table + table = Table(title=f"Open Ports on {args.target}") table.add_column("Port", style="cyan") table.add_column("State", style="green") table.add_column("Service", style="magenta") table.add_column("Version", style="yellow") - for p in results['ports']: + for p in results["ports"]: version = f"{p['product']} {p['version']}".strip() or "N/A" - table.add_row(f"{p['number']}/{p['protocol']}", p['state'], p['service'], version) - + table.add_row( + f"{p['number']}/{p['protocol']}", p["state"], p["service"], version + ) + console.print(table) # AI Analysis Interaction if not args.no_ai: - if input("\nšŸ¤– Do you want an AI analysis of these results? (y/N): ").lower() == 'y': + if ( + input( + "\nšŸ¤– Do you want an AI analysis of these results? (y/N): " + ).lower() + == "y" + ): console.print("\n[bold cyan]Generating AI Analysis...[/bold cyan]") # We reuse AIAnalyzer.analyze_nmap_output via ScanModule - nmap_str = vf.scan_module._format_nmap_results(results['ports']) + nmap_str = vf.scan_module._format_nmap_results(results["ports"]) analysis = await vf.ai_analyzer.analyze_nmap_output(nmap_str) results["ai_analysis"] = analysis - + # Update saved results vf.scan_module._save_results(results, session_dir) @@ -933,11 +1516,17 @@ async def _async_main(args): console.print("\n[bold cyan]AI Security Insights:[/bold cyan]") if isinstance(analysis, dict): if "summary" in analysis: - console.print(f"\n[bold]Summary:[/bold]\n{analysis['summary']}") + console.print( + f"\n[bold]Summary:[/bold]\n{analysis['summary']}" + ) if "potential_vulnerabilities" in analysis: - console.print("\n[bold yellow]Potential Vulnerabilities:[/bold yellow]") + console.print( + "\n[bold yellow]Potential Vulnerabilities:[/bold yellow]" + ) for v in analysis["potential_vulnerabilities"]: - console.print(f"- [bold]{v.get('type')}[/bold]: {v.get('description')} (Severity: {v.get('severity')})") + console.print( + f"- [bold]{v.get('type')}[/bold]: {v.get('description')} (Severity: {v.get('severity')})" + ) else: console.print(analysis) else: @@ -949,9 +1538,11 @@ async def _async_main(args): print(f"\n🌐 Starting web discovery on {args.target}") if args.ai_only: print("Running in AI-only mode - AI will make all decisions") - + # Run discovery without AI initially to allow for interactive prompt at the end - results = await vf.web_module.run_web_discovery(args.target, session_dir, use_ai=False) + results = await vf.web_module.run_web_discovery( + args.target, session_dir, use_ai=False + ) # Display summary console = Console() @@ -961,11 +1552,16 @@ async def _async_main(args): # Interactive AI Analysis Prompt if not args.no_ai: - if input("\nšŸ¤– Do you want an AI analysis of these results? (y/N): ").lower() == 'y': + if ( + input( + "\nšŸ¤– Do you want an AI analysis of these results? (y/N): " + ).lower() + == "y" + ): console.print("\n[bold cyan]Generating AI Analysis...[/bold cyan]") analysis = await vf.web_module.analyze_with_ai(args.target, results) results["ai_analysis"] = analysis - + # Update saved results with AI analysis vf.web_module.save_results(results, session_dir) @@ -973,10 +1569,12 @@ async def _async_main(args): console.print("\n[bold cyan]AI Analysis Summary:[/bold cyan]") st = analysis.get("tech_stack_assessment", "N/A") console.print(f"[bold]Tech Stack:[/bold] {st}") - + interesting = analysis.get("interesting_findings", []) if interesting: - console.print("\n[bold yellow]Interesting Findings:[/bold yellow]") + console.print( + "\n[bold yellow]Interesting Findings:[/bold yellow]" + ) for finding in interesting: console.print(f"- {finding}") else: @@ -990,45 +1588,56 @@ async def _async_main(args): elif args.operation_mode == "exploit": print(f"\nšŸ’„ Starting exploitation on {args.target}") - + # To run exploit mode, we need recon data # Let's check if there's a recent recon scan for this target recon_data = {} recon_results_path = session_dir / "recon_results.json" web_results_path = session_dir / "web_discovery_results.json" - + if recon_results_path.exists(): - with open(recon_results_path, 'r') as f: + with open(recon_results_path, "r") as f: recon_data = json.load(f) elif web_results_path.exists(): - with open(web_results_path, 'r') as f: + with open(web_results_path, "r") as f: web_data = json.load(f) # Map web data to a format exploit module understands recon_data = { "target": args.target, - "services": [{"name": tech.get("raw"), "version": ""} for tech in web_data.get("technologies", [])] + "services": [ + {"name": tech.get("raw"), "version": ""} + for tech in web_data.get("technologies", []) + ], } else: - print("[yellow]No reconnaissance data found for this target in the current session.[/yellow]") + print( + "[yellow]No reconnaissance data found for this target in the current session.[/yellow]" + ) print("Exploit mode works best when preceded by 'recon' or 'web' mode.") # We can still try with basic info if provided recon_data = {"target": args.target, "services": []} - results = await vf.exploit_module.run_exploit_pipeline(args.target, recon_data, session_dir, use_ai=not args.no_ai) + results = await vf.exploit_module.run_exploit_pipeline( + args.target, recon_data, session_dir, use_ai=not args.no_ai + ) console = Console() console.print("\n[bold green]Exploit Pipeline Complete![/bold green]") - console.print(f"Mapped {len(results['vulnerabilities'])} potential vulnerabilities") + console.print( + f"Mapped {len(results['vulnerabilities'])} potential vulnerabilities" + ) console.print(f"Generated {len(results['exploits'])} exploits") - if results['exploits']: + if results["exploits"]: console.print("\n[bold cyan]Generated Exploits:[/bold cyan]") - for exploit in results['exploits']: + for exploit in results["exploits"]: if "error" not in exploit: console.print(f"- [green]{exploit.get('file_path')}[/green]") if exploit.get("validation", {}).get("issues"): - console.print(f" [yellow]Validation Issues:[/yellow] {', '.join(exploit['validation']['issues'])}") - + console.print( + f" [yellow]Validation Issues:[/yellow] {', '.join(exploit['validation']['issues'])}" + ) + # Start dev mode shell if requested if args.dev_mode: await dev_mode_shell(vf, session_dir) @@ -1042,39 +1651,44 @@ def main(): if args.webmod: # Check if Robin module is available (Optional now) if not darkweb_module.ROBIN_AVAILABLE: - print("āš ļø Warning: Robin module dependencies not installed. Dark Web OSINT features may be unavailable.") + print( + "āš ļø Warning: Robin module dependencies not installed. Dark Web OSINT features may be unavailable." + ) - print("🌐 Launching NeuroRift Web Interface...") print(f"šŸ“ Access the UI at: http://{args.web_host}:{args.web_port}") print("āš ļø Press Ctrl+C to stop the server\n") - + try: # Import streamlit CLI from streamlit.web import cli as stcli import sys - + # Get the UI file path ui_file = Path(__file__).parent / "modules" / "web" / "dashboard.py" - + if not ui_file.exists(): print(f"āŒ Error: Web UI file not found at {ui_file}") return - + # Prepare streamlit arguments sys.argv = [ "streamlit", "run", str(ui_file), - "--server.port", str(args.web_port), - "--server.address", args.web_host, - "--server.headless", "true", - "--browser.gatherUsageStats", "false" + "--server.port", + str(args.web_port), + "--server.address", + args.web_host, + "--server.headless", + "true", + "--browser.gatherUsageStats", + "false", ] - + # Launch streamlit sys.exit(stcli.main()) - + except ImportError: print("āŒ Error: Streamlit is not installed.") return @@ -1085,5 +1699,6 @@ def main(): # Run the main async pipeline asyncio.run(_async_main(args)) + if __name__ == "__main__": main() diff --git a/neurorift b/neurorift_wrapper.sh old mode 100755 new mode 100644 similarity index 100% rename from neurorift rename to neurorift_wrapper.sh diff --git a/runtime_environment_check.py b/runtime_environment_check.py new file mode 100644 index 0000000..81b79d9 --- /dev/null +++ b/runtime_environment_check.py @@ -0,0 +1,70 @@ +"""Runtime environment verification for NeuroRift startup.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +from pathlib import Path + +REQUIRED_TOOLS = ("python3", "curl", "nmap") + + +def check_tool(tool: str) -> dict: + resolved = shutil.which(tool) + if not resolved: + return { + "tool": tool, + "ok": False, + "error": "missing", + "install_hint": f"Install {tool} using your package manager (e.g., sudo apt install {tool}).", + } + + if not os.access(resolved, os.X_OK): + return { + "tool": tool, + "ok": False, + "error": "not_executable", + "install_hint": f"Grant execute permission: chmod +x {resolved}", + } + + try: + probe = subprocess.run( + [tool, "--help"], capture_output=True, text=True, timeout=8, check=False + ) + if probe.returncode not in (0, 1, 2): + return { + "tool": tool, + "ok": False, + "error": f"probe_failed_exit_{probe.returncode}", + "install_hint": f"Reinstall or verify {tool} works from shell.", + } + except Exception as exc: # defensive startup guard + return { + "tool": tool, + "ok": False, + "error": f"probe_exception:{type(exc).__name__}", + "install_hint": f"Verify {tool} manually: {tool} --help", + } + + return {"tool": tool, "ok": True, "path": resolved} + + +def verify_runtime_environment(workdir: Path | None = None) -> dict: + workdir = workdir or (Path.home() / ".neurorift") + workdir.mkdir(parents=True, exist_ok=True) + + checks = [check_tool(tool) for tool in REQUIRED_TOOLS] + all_ok = all(c["ok"] for c in checks) + + return { + "ok": all_ok, + "workdir": str(workdir), + "tools": checks, + } + + +if __name__ == "__main__": + import json + + print(json.dumps(verify_runtime_environment(), indent=2)) diff --git a/setup.py b/setup.py index bb0bf16..18ab965 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,18 @@ setup( name="neurorift", version="1.0.0", - packages=find_packages(include=['modules', 'modules.*', 'utils', 'utils.*', 'ai_wrapper', 'ai_wrapper.*']), + packages=find_packages( + include=[ + "modules", + "modules.*", + "utils", + "utils.*", + "ai_wrapper", + "ai_wrapper.*", + "neurorift", + "neurorift.*", + ] + ), py_modules=[ # Top-level modules used by the CLI "neurorift_main", @@ -11,11 +22,14 @@ "ai_integration", "ai_orchestrator", "ai_controller", + "runtime_environment_check", + "model_capability_check", + "neurorift_cli", ], package_data={ - 'modules': ['**/*.json', '**/*.md'], - 'prompts': ['**/*.md', '**/*.txt'], - 'configs': ['*.json'], + "modules": ["**/*.json", "**/*.md"], + "prompts": ["**/*.md", "**/*.txt"], + "configs": ["*.json"], }, include_package_data=True, install_requires=[ @@ -46,7 +60,7 @@ ], entry_points={ "console_scripts": [ - "neurorift=neurorift_main:main", + "neurorift=neurorift_cli:main", ], }, author="demonking369", @@ -76,4 +90,4 @@ ], python_requires=">=3.10", keywords="security penetration-testing bug-bounty ai multi-agent reconnaissance vulnerability-scanning", -) \ No newline at end of file +)