Skip to content
Open
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
99 changes: 99 additions & 0 deletions model_capability_check.py
Original file line number Diff line number Diff line change
@@ -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])
Comment on lines +28 to +32
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: JSON extraction is brittle because it slices from the first { to the last } in the entire response. If the model adds explanatory text containing braces before or after the actual JSON object, parsing fails even when a valid JSON object is present. Decode from each { position until a valid JSON object is found. [logic error]

Severity Level: Major ⚠️
- ⚠️ run-agent startup can fail despite valid capability JSON.
- ⚠️ Model readiness check becomes fragile to response formatting.
Suggested change
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])
decoder = json.JSONDecoder()
for idx, char in enumerate(raw_text):
if char != "{":
continue
try:
parsed, _ = decoder.raw_decode(raw_text[idx:])
except json.JSONDecodeError:
continue
if isinstance(parsed, dict):
return parsed
raise ValueError("No JSON object found in model response")
Steps of Reproduction ✅
1. Run `neurorift run-agent --model <model>`; CLI forwards to `neurorift_main.main()` via
`neurorift_cli.py:50-53`.

2. `run-agent` branch in `neurorift_main.py:1008-1020` calls `verify_model_capabilities()`
(`model_capability_check.py:35`).

3. If Ollama output contains valid JSON plus extra brace text, `_extract_json()`
(`model_capability_check.py:26-32`) slices first `{` to last `}`, creating invalid JSON.

4. `verify_model_capabilities()` catches parse failure at
`model_capability_check.py:57-64`, returns `invalid_capability_json`, and
`neurorift_main.py:1020-1023` blocks agent startup.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** model_capability_check.py
**Line:** 28:32
**Comment:**
	*Logic Error: JSON extraction is brittle because it slices from the first `{` to the last `}` in the entire response. If the model adds explanatory text containing braces before or after the actual JSON object, parsing fails even when a valid JSON object is present. Decode from each `{` position until a valid JSON object is found.

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.
👍 | 👎



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
Comment on lines +67 to +83
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: Capability fields are only checked for presence, not type, so a response like "agent_ready": "false" is treated as truthy by callers and can incorrectly pass readiness checks. Validate that all required fields are booleans and compute ok from the normalized boolean value. [type error]

Severity Level: Critical 🚨
- ❌ Non-agent-ready models can bypass autonomous safety gate.
- ⚠️ run-agent reliability degrades with loosely formatted model JSON.
Suggested change
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
required = {
"tool_usage",
"command_generation",
"filesystem_operations",
"multi_step_reasoning",
"agent_ready",
}
if not isinstance(parsed, dict) or not required.issubset(parsed):
return {
"ok": False,
"error": "capability_fields_missing",
"parsed": parsed,
"agent_ready": False,
}
for field in required:
if not isinstance(parsed.get(field), bool):
return {
"ok": False,
"error": f"capability_field_not_boolean:{field}",
"parsed": parsed,
"agent_ready": False,
}
parsed["ok"] = parsed["agent_ready"]
return parsed
Steps of Reproduction ✅
1. Invoke `neurorift run-agent --model <model>`; parser defines this command in
`neurorift_main.py:938-942`.

2. Execution reaches capability gate in `neurorift_main.py:1019-1023`, which checks `if
not capability.get("agent_ready")`.

3. If model returns JSON like `"agent_ready": "false"`, current code
(`model_capability_check.py:67-83`) accepts field presence only and keeps string value.

4. Non-empty string is truthy, so `neurorift_main.py:1020` treats model as ready and
continues autonomous run incorrectly.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** model_capability_check.py
**Line:** 67:83
**Comment:**
	*Type Error: Capability fields are only checked for presence, not type, so a response like `"agent_ready": "false"` is treated as truthy by callers and can incorrectly pass readiness checks. Validate that all required fields are booleans and compute `ok` from the normalized boolean value.

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.
👍 | 👎



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))
1 change: 1 addition & 0 deletions neurorift/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions neurorift/channels/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

3 changes: 3 additions & 0 deletions neurorift/channels/channel_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class ChannelRouter:
def route(self, msg: dict):
return msg
12 changes: 12 additions & 0 deletions neurorift/channels/cli_channel.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions neurorift/channels/discord_channel.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions neurorift/channels/telegram_channel.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions neurorift/channels/web_channel.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions neurorift/clawhub/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

2 changes: 2 additions & 0 deletions neurorift/clawhub/clawhub_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class ClawHubAPI:
base_url = "https://clawhub.example/api"
18 changes: 18 additions & 0 deletions neurorift/clawhub/clawhub_client.py
Original file line number Diff line number Diff line change
@@ -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
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: skill_name is used directly in path joins, so values like ../... or absolute paths can escape source_dir/cache_dir, allowing unintended directory deletion/copy outside the skill store. Validate and normalize the name before building paths. [security]

Severity Level: Critical 🚨
-`--clawhub` can access paths outside skill store.
- ❌ Arbitrary directory removal possible via destination override.
- ⚠️ Skill installation trust boundary is bypassed.
Suggested change
src = source_dir / skill_name
skill_path = Path(skill_name)
if skill_path.is_absolute() or ".." in skill_path.parts or skill_path.name != skill_name:
raise ValueError("Invalid skill name")
src = source_dir / skill_path
Steps of Reproduction ✅
1. Run CLI with ClawHub install path: `neurorift --clawhub /tmp` (entry argument defined
at `neurorift_main.py:915` and handled at `neurorift_main.py:983-984`).

2. `SkillManager.install_clawhub()` forwards raw user input unchanged at
`neurorift/skills/skill_manager.py:13-14`.

3. `ClawHubClient.fetch_skill()` joins unsanitized `skill_name` directly into paths at
`neurorift/clawhub/clawhub_client.py:6` and `:8`; absolute input overrides
`source_dir`/`cache_dir`.

4. Existing destination is deleted by `shutil.rmtree(dst)` at
`neurorift/clawhub/clawhub_client.py:9`, so unintended filesystem paths can be
removed/copied outside intended skill store.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** neurorift/clawhub/clawhub_client.py
**Line:** 6:6
**Comment:**
	*Security: `skill_name` is used directly in path joins, so values like `../...` or absolute paths can escape `source_dir`/`cache_dir`, allowing unintended directory deletion/copy outside the skill store. Validate and normalize the name before building paths.

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.
👍 | 👎

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
3 changes: 3 additions & 0 deletions neurorift/clawhub/clawhub_resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class ClawHubResolver:
def resolve(self, skill_name: str) -> str:
return f"https://clawhub.example/skills/{skill_name}"
1 change: 1 addition & 0 deletions neurorift/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

7 changes: 7 additions & 0 deletions neurorift/cli/neurorift_cli.py
Original file line number Diff line number Diff line change
@@ -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"]
1 change: 1 addition & 0 deletions neurorift/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions neurorift/config/channel_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CHANNELS = ["cli", "websocket", "discord", "telegram", "api"]
1 change: 1 addition & 0 deletions neurorift/config/model_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
MODEL_PROVIDERS = ["deepseek", "mistral", "llama", "openai"]
7 changes: 7 additions & 0 deletions neurorift/config/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from dataclasses import dataclass
from pathlib import Path


@dataclass
class Settings:
data_dir: Path = Path.home() / ".neurorift"
1 change: 1 addition & 0 deletions neurorift/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

3 changes: 3 additions & 0 deletions neurorift/core/agent_loop.py
Original file line number Diff line number Diff line change
@@ -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}"}
17 changes: 17 additions & 0 deletions neurorift/core/agent_manager.py
Original file line number Diff line number Diff line change
@@ -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] = {}
16 changes: 16 additions & 0 deletions neurorift/core/planner.py
Original file line number Diff line number Diff line change
@@ -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 []
)
3 changes: 3 additions & 0 deletions neurorift/core/task_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class TaskRouter:
def route(self, task: str) -> dict:
return {"task": task, "route": "agent_loop"}
1 change: 1 addition & 0 deletions neurorift/execution/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

12 changes: 12 additions & 0 deletions neurorift/execution/command_runner.py
Original file line number Diff line number Diff line change
@@ -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,
}
8 changes: 8 additions & 0 deletions neurorift/execution/resource_limits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from dataclasses import dataclass


@dataclass
class ResourceLimits:
timeout_seconds: int = 30
memory_limit_mb: int = 512
cpu_seconds: int = 20
9 changes: 9 additions & 0 deletions neurorift/execution/retry_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import time


class RetryManager:
def __init__(self, retries=2):
self.retries = retries

def wait(self, attempt):
time.sleep(2**attempt)
5 changes: 5 additions & 0 deletions neurorift/execution/sandbox_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from neurorift.execution.command_runner import CommandRunner


class SandboxRunner(CommandRunner):
pass
1 change: 1 addition & 0 deletions neurorift/gateway/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

3 changes: 3 additions & 0 deletions neurorift/gateway/api_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class APIServer:
def start(self):
return True
3 changes: 3 additions & 0 deletions neurorift/gateway/auth_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class AuthManager:
def validate(self, token: str):
return bool(token)
3 changes: 3 additions & 0 deletions neurorift/gateway/websocket_gateway.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class WebSocketGateway:
def start(self):
return True
1 change: 1 addition & 0 deletions neurorift/memory/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

6 changes: 6 additions & 0 deletions neurorift/memory/long_term_memory.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions neurorift/memory/memory_compaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class MemoryCompaction:
def compact(self, messages: list[str]):
return {"summary": " ".join(messages[:10]), "kept": messages[-10:]}
9 changes: 9 additions & 0 deletions neurorift/memory/short_term_memory.py
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 6 additions & 0 deletions neurorift/memory/vector_memory.py
Original file line number Diff line number Diff line change
@@ -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 []
1 change: 1 addition & 0 deletions neurorift/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

3 changes: 3 additions & 0 deletions neurorift/models/context_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class ContextBuilder:
def build(self, session, memories):
return {"session": session.session_id, "memories": memories}
3 changes: 3 additions & 0 deletions neurorift/models/model_failover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class ModelFailover:
def pick(self, providers):
return providers[0] if providers else "fallback"
8 changes: 8 additions & 0 deletions neurorift/models/model_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class ModelRouter:
async def generate(self, prompt: str):
return {
"response": prompt,
"model": "openai",
"tokens": len(prompt.split()),
"cost": 0.0,
}
3 changes: 3 additions & 0 deletions neurorift/models/prompt_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class PromptBuilder:
def build(self, message: str, context: dict):
return f"{context}\n{message}"
1 change: 1 addition & 0 deletions neurorift/sessions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

29 changes: 29 additions & 0 deletions neurorift/sessions/session_context.py
Original file line number Diff line number Diff line change
@@ -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()
)
Loading