Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 +29 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The JSON extraction uses the first { and last } in the whole response, so any extra brace text before/after the actual JSON object can make parsing fail even when the model returned valid JSON. Parse the first decodable JSON object instead of slicing by global brace positions. [logic error]

Severity Level: Major ⚠️
- ❌ run-agent startup fails despite valid model output.
- ⚠️ Capability check becomes brittle to normal LLM formatting.
Suggested change
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])
raw_text = (raw_text or "").strip()
decoder = json.JSONDecoder()
for i, ch in enumerate(raw_text):
if ch != "{":
continue
try:
obj, _ = decoder.raw_decode(raw_text[i:])
except json.JSONDecodeError:
continue
if isinstance(obj, dict):
return obj
raise ValueError("No JSON object found in model response")
Steps of Reproduction ✅
1. Start through CLI entrypoint `neurorift_cli.py:39-53` (`main()`), which forwards args
into `neurorift_main.main()` at line 52.

2. Run autonomous mode command parsed at `neurorift_main.py:937-940` (`run-agent --model
...`), then `_async_main` executes model check at `neurorift_main.py:1015-1018`.

3. `verify_model_capabilities()` in `model_capability_check.py:35-58` calls Ollama and
passes `proc.stdout` into `_extract_json()` at line 58.

4. If stdout contains any non-payload brace text plus valid JSON, `_extract_json()`
(`model_capability_check.py:27-32`) slices first `{` to last `}`, causing
`json.loads(...)` failure; exception is converted to `invalid_capability_json` at
`model_capability_check.py:59-64`, and `run-agent` is blocked by
`neurorift_main.py:1018-1021`.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** model_capability_check.py
**Line:** 27:32
**Comment:**
	*Logic Error: The JSON extraction uses the first `{` and last `}` in the whole response, so any extra brace text before/after the actual JSON object can make parsing fail even when the model returned valid JSON. Parse the first decodable JSON object instead of slicing by global brace positions.

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 +72 to +73
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: Converting agent_ready with bool(...) treats non-empty strings like "false" as True, which can incorrectly mark an incapable model as ready. Require agent_ready to be a real boolean and then copy that value directly. [type error]

Severity Level: Critical 🚨
- ❌ Non-ready models can pass autonomous safety gate.
- ❌ run-agent may execute with invalid capability contract.
Suggested change
parsed["ok"] = bool(parsed.get("agent_ready"))
return parsed
if not isinstance(parsed.get("agent_ready"), bool):
return {
"ok": False,
"error": "capability_field_invalid_type",
"parsed": parsed,
"agent_ready": False,
}
parsed["ok"] = parsed["agent_ready"]
return parsed
Steps of Reproduction ✅
1. Invoke autonomous flow via `run-agent` (parser wiring at `neurorift_main.py:937-940`,
execution gate at `neurorift_main.py:1006-1021`).

2. In `verify_model_capabilities()` (`model_capability_check.py:67-83`), only key presence
is checked (`required.issubset(...)` at lines 67-75), not field types.

3. When model returns JSON like `"agent_ready": "false"` (string), line 82 computes
`bool("false") == True`, so `ok` is incorrectly marked true.

4. `neurorift_main.py:1018` tests `if not capability.get("agent_ready")`; string `"false"`
is truthy, so the rejection branch is skipped and orchestrated agent mode proceeds
(`neurorift_main.py:1066+`) even though readiness value is semantically false.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** model_capability_check.py
**Line:** 82:83
**Comment:**
	*Type Error: Converting `agent_ready` with `bool(...)` treats non-empty strings like `"false"` as `True`, which can incorrectly mark an incapable model as ready. Require `agent_ready` to be a real boolean and then copy that value directly.

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
if not src.exists():
raise FileNotFoundError(skill_name)
dst = self.cache_dir / skill_name
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst)
Comment on lines +14 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject traversal components in ClawHub skill names

fetch_skill joins untrusted skill_name directly into both source and destination paths, so inputs like ../../tmp/foo can escape source_dir and cache_dir; because the code calls shutil.rmtree(dst) before copy, this can delete directories outside the skill store. A malformed --clawhub value therefore has destructive filesystem impact beyond the intended sandbox.

Useful? React with 👍 / 👎.

Comment on lines +14 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The destination path is built from untrusted skill_name and then passed to rmtree/copytree, which allows path traversal to delete or overwrite directories outside the cache. Resolve and validate the destination path is contained within cache_dir before any filesystem mutation. [security]

Severity Level: Critical 🚨
- ❌ rmtree can delete ~/.neurorift/skills/installed unexpectedly.
- ❌ copytree can overwrite directories outside cache root.
Suggested change
dst = self.cache_dir / skill_name
if dst.exists(): shutil.rmtree(dst)
shutil.copytree(src, dst)
dst = (self.cache_dir / skill_name).resolve()
if self.cache_dir.resolve() not in dst.parents:
raise ValueError(f"Invalid skill name: {skill_name}")
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst)
Steps of Reproduction ✅
1. Install any skill once via `neurorift --clawhub recon_scanner` so
`~/.neurorift/skills/installed` exists (`SkillInstaller` paths created at
`neurorift/skills/installer.py:8-10`).

2. Run `neurorift --clawhub ../installed`; argument is accepted at `neurorift_main.py:914`
and passed into `SkillManager.install_clawhub()` at `neurorift_main.py:981-983`.

3. `fetch_skill()` computes `dst = self.cache_dir / skill_name`
(`neurorift/clawhub/clawhub_client.py:8`), so `../installed` targets sibling directory
outside cache.

4. Because line 9 executes `shutil.rmtree(dst)` and line 10 executes `shutil.copytree(src,
dst)`, paths outside `cache_dir` can be deleted/overwritten.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** neurorift/clawhub/clawhub_client.py
**Line:** 8:10
**Comment:**
	*Security: The destination path is built from untrusted `skill_name` and then passed to `rmtree`/`copytree`, which allows path traversal to delete or overwrite directories outside the cache. Resolve and validate the destination path is contained within `cache_dir` before any filesystem mutation.

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

Comment on lines +10 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Prevent path traversal/arbitrary delete via skill_name.

skill_name is used directly in path joins (Line 11 and Line 14). Absolute paths or traversal segments can escape source_dir/cache_dir, and the delete at Line 16 can then target unintended directories.

🔒 Suggested hardening patch
 def fetch_skill(self, skill_name: str, source_dir: Path) -> Path:
-        src = source_dir / skill_name
-        if not src.exists():
+        if not skill_name or Path(skill_name).name != skill_name:
+            raise ValueError(f"Invalid skill name: {skill_name!r}")
+
+        source_root = source_dir.resolve()
+        cache_root = self.cache_dir.resolve()
+        src = (source_root / skill_name).resolve()
+        dst = (cache_root / skill_name).resolve()
+
+        if source_root not in src.parents:
+            raise ValueError(f"Skill path escapes source_dir: {skill_name!r}")
+        if cache_root not in dst.parents:
+            raise ValueError(f"Skill path escapes cache_dir: {skill_name!r}")
+
+        if not src.is_dir():
             raise FileNotFoundError(skill_name)
-        dst = self.cache_dir / skill_name
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)
def fetch_skill(self, skill_name: str, source_dir: Path) -> Path:
if not skill_name or Path(skill_name).name != skill_name:
raise ValueError(f"Invalid skill name: {skill_name!r}")
source_root = source_dir.resolve()
cache_root = self.cache_dir.resolve()
src = (source_root / skill_name).resolve()
dst = (cache_root / skill_name).resolve()
if source_root not in src.parents:
raise ValueError(f"Skill path escapes source_dir: {skill_name!r}")
if cache_root not in dst.parents:
raise ValueError(f"Skill path escapes cache_dir: {skill_name!r}")
if not src.is_dir():
raise FileNotFoundError(skill_name)
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@neurorift/clawhub/clawhub_client.py` around lines 10 - 17, fetch_skill
currently uses skill_name directly to build src and dst (src = source_dir /
skill_name, dst = self.cache_dir / skill_name) which allows absolute paths or
traversal segments to escape source_dir/cache_dir and enables accidental or
malicious deletes; fix by validating skill_name is a simple basename (reject
path separators and absolute paths), then resolve both src and dst
(src.resolve() and dst.resolve()) and check that src is within
source_dir.resolve() and dst is within self.cache_dir.resolve() before
proceeding, and only perform shutil.rmtree(dst) after confirming dst is inside
the allowed cache_dir; raise a clear error (ValueError or FileNotFoundError) if
validation fails.

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)
Comment on lines +1 to +3
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fail-closed auth is required; current token check is bypassable.

bool(token) accepts any non-empty string, so arbitrary tokens validate successfully. If this path is wired into gateway authorization, it permits unauthorized access.

🔒 Proposed safe placeholder until real verification is implemented
 class AuthManager:
-    def validate(self, token: str):
-        return bool(token)
+    def validate(self, token: str) -> bool:
+        raise NotImplementedError(
+            "Token verification is not implemented. Wire a real verifier before enabling auth."
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@neurorift/gateway/auth_manager.py` around lines 1 - 3, The current validate
method in AuthManager (AuthManager.validate) uses bool(token) which accepts any
non-empty string — change it to fail-closed: add an expected token/configurable
verifier (e.g., an AuthManager.__init__ that reads a configured AUTH_TOKEN or
accepts a verification callable) and update validate to return True only when
the provided token exactly matches that expected token or the verifier
explicitly succeeds; otherwise always return False (do not accept arbitrary
non-empty strings). Ensure the method defaults to False if no expected
token/verifier is configured so the gateway remains locked until real
verification is implemented.

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
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: state is annotated as an enum, but sessions loaded from JSON pass a plain string into this dataclass, so the object can carry an invalid/non-enum state and break enum-based logic (for example code expecting enum behavior). Coerce and validate state in __post_init__ so deserialized sessions always have a real SessionState. [type error]

Severity Level: Major ⚠️
- ⚠️ Persisted sessions reload with string state, not enum.
- ⚠️ get_session returns inconsistent state type by source.
- ⚠️ Invalid state strings are accepted during deserialization.
Suggested change
state: SessionState = SessionState.CREATED
state: SessionState = SessionState.CREATED
def __post_init__(self):
if not isinstance(self.state, SessionState):
self.state = SessionState(self.state)
Steps of Reproduction ✅
1. Create a persisted session through `SessionManager.create_session()` at
`neurorift/sessions/session_manager.py:10-12`; it stores JSON via `SessionStore.save()` at
`neurorift/sessions/session_store.py:9`.

2. Load that same session through `SessionManager.get_session()` at
`neurorift/sessions/session_manager.py:13`, which calls `SessionStore.load()` at
`neurorift/sessions/session_store.py:10-12`.

3. `SessionStore.load()` reconstructs with `SessionContext(**json.loads(...))`
(`neurorift/sessions/session_store.py:12`), so `state` is passed as plain JSON string.

4. `SessionContext` currently has no `__post_init__` validation
(`neurorift/sessions/session_context.py:8-19`), so loaded objects can carry `state` as
`str` instead of `SessionState`, violating the dataclass contract immediately after
deserialization.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** neurorift/sessions/session_context.py
**Line:** 13:13
**Comment:**
	*Type Error: `state` is annotated as an enum, but sessions loaded from JSON pass a plain string into this dataclass, so the object can carry an invalid/non-enum state and break enum-based logic (for example code expecting enum behavior). Coerce and validate `state` in `__post_init__` so deserialized sessions always have a real `SessionState`.

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

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