omni-cortex 1.2.0__py3-none-any.whl → 1.11.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omni_cortex-1.11.3.data/data/share/omni-cortex/dashboard/backend/.env.example +12 -0
- omni_cortex-1.11.3.data/data/share/omni-cortex/dashboard/backend/backfill_summaries.py +280 -0
- {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/chat_service.py +59 -32
- {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/database.py +305 -18
- {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/image_service.py +35 -16
- {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/logging_config.py +34 -4
- {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/main.py +451 -13
- {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/models.py +64 -12
- omni_cortex-1.11.3.data/data/share/omni-cortex/dashboard/backend/prompt_security.py +111 -0
- omni_cortex-1.11.3.data/data/share/omni-cortex/dashboard/backend/security.py +104 -0
- {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/uv.lock +414 -1
- {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/websocket_manager.py +24 -2
- omni_cortex-1.11.3.data/data/share/omni-cortex/hooks/post_tool_use.py +429 -0
- {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/hooks/pre_tool_use.py +52 -2
- omni_cortex-1.11.3.data/data/share/omni-cortex/hooks/session_utils.py +186 -0
- {omni_cortex-1.2.0.dist-info → omni_cortex-1.11.3.dist-info}/METADATA +237 -8
- omni_cortex-1.11.3.dist-info/RECORD +25 -0
- omni_cortex-1.2.0.data/data/share/omni-cortex/hooks/post_tool_use.py +0 -160
- omni_cortex-1.2.0.dist-info/RECORD +0 -20
- {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/project_config.py +0 -0
- {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/project_scanner.py +0 -0
- {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/pyproject.toml +0 -0
- {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/hooks/stop.py +0 -0
- {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/hooks/subagent_stop.py +0 -0
- {omni_cortex-1.2.0.dist-info → omni_cortex-1.11.3.dist-info}/WHEEL +0 -0
- {omni_cortex-1.2.0.dist-info → omni_cortex-1.11.3.dist-info}/entry_points.txt +0 -0
- {omni_cortex-1.2.0.dist-info → omni_cortex-1.11.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -70,6 +70,53 @@ class MemoryStats(BaseModel):
|
|
|
70
70
|
tags: list[dict[str, int | str]]
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
class FilterParams(BaseModel):
|
|
74
|
+
"""Query filter parameters."""
|
|
75
|
+
|
|
76
|
+
memory_type: Optional[str] = None
|
|
77
|
+
status: Optional[str] = None
|
|
78
|
+
tags: Optional[list[str]] = None
|
|
79
|
+
search: Optional[str] = None
|
|
80
|
+
min_importance: Optional[int] = None
|
|
81
|
+
max_importance: Optional[int] = None
|
|
82
|
+
sort_by: str = "last_accessed"
|
|
83
|
+
sort_order: str = "desc"
|
|
84
|
+
limit: int = 50
|
|
85
|
+
offset: int = 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AggregateMemoryRequest(BaseModel):
|
|
89
|
+
"""Request for aggregate memory data across projects."""
|
|
90
|
+
|
|
91
|
+
projects: list[str] = Field(..., description="List of project db paths")
|
|
92
|
+
filters: Optional[FilterParams] = None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AggregateStatsRequest(BaseModel):
|
|
96
|
+
"""Request for aggregate statistics."""
|
|
97
|
+
|
|
98
|
+
projects: list[str] = Field(..., description="List of project db paths")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AggregateStatsResponse(BaseModel):
|
|
102
|
+
"""Aggregate statistics across multiple projects."""
|
|
103
|
+
|
|
104
|
+
total_count: int
|
|
105
|
+
total_access_count: int
|
|
106
|
+
avg_importance: float
|
|
107
|
+
by_type: dict[str, int]
|
|
108
|
+
by_status: dict[str, int]
|
|
109
|
+
project_count: int
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class AggregateChatRequest(BaseModel):
|
|
113
|
+
"""Request for chat across multiple projects."""
|
|
114
|
+
|
|
115
|
+
projects: list[str] = Field(..., description="List of project db paths")
|
|
116
|
+
question: str = Field(..., min_length=1, max_length=2000)
|
|
117
|
+
max_memories_per_project: int = Field(default=5, ge=1, le=20)
|
|
118
|
+
|
|
119
|
+
|
|
73
120
|
class Activity(BaseModel):
|
|
74
121
|
"""Activity log record."""
|
|
75
122
|
|
|
@@ -84,6 +131,14 @@ class Activity(BaseModel):
|
|
|
84
131
|
duration_ms: Optional[int] = None
|
|
85
132
|
file_path: Optional[str] = None
|
|
86
133
|
timestamp: datetime
|
|
134
|
+
# Command analytics fields
|
|
135
|
+
command_name: Optional[str] = None
|
|
136
|
+
command_scope: Optional[str] = None
|
|
137
|
+
mcp_server: Optional[str] = None
|
|
138
|
+
skill_name: Optional[str] = None
|
|
139
|
+
# Natural language summary fields
|
|
140
|
+
summary: Optional[str] = None
|
|
141
|
+
summary_detail: Optional[str] = None
|
|
87
142
|
|
|
88
143
|
|
|
89
144
|
class Session(BaseModel):
|
|
@@ -105,19 +160,14 @@ class TimelineEntry(BaseModel):
|
|
|
105
160
|
data: dict
|
|
106
161
|
|
|
107
162
|
|
|
108
|
-
class
|
|
109
|
-
"""
|
|
163
|
+
class MemoryCreateRequest(BaseModel):
|
|
164
|
+
"""Create request for a new memory."""
|
|
110
165
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
max_importance: Optional[int] = None
|
|
117
|
-
sort_by: str = "last_accessed"
|
|
118
|
-
sort_order: str = "desc"
|
|
119
|
-
limit: int = 50
|
|
120
|
-
offset: int = 0
|
|
166
|
+
content: str = Field(..., min_length=1, max_length=50000)
|
|
167
|
+
memory_type: str = Field(default="general")
|
|
168
|
+
context: Optional[str] = None
|
|
169
|
+
importance_score: int = Field(default=50, ge=1, le=100)
|
|
170
|
+
tags: list[str] = Field(default_factory=list)
|
|
121
171
|
|
|
122
172
|
|
|
123
173
|
class MemoryUpdate(BaseModel):
|
|
@@ -155,6 +205,8 @@ class ChatSource(BaseModel):
|
|
|
155
205
|
type: str
|
|
156
206
|
content_preview: str
|
|
157
207
|
tags: list[str]
|
|
208
|
+
project_path: Optional[str] = None
|
|
209
|
+
project_name: Optional[str] = None
|
|
158
210
|
|
|
159
211
|
|
|
160
212
|
class ChatResponse(BaseModel):
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Prompt injection protection for Omni-Cortex."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import logging
|
|
5
|
+
from html import escape as html_escape
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def xml_escape(text: str) -> str:
|
|
12
|
+
"""Escape text for safe inclusion in XML-structured prompts.
|
|
13
|
+
|
|
14
|
+
Converts special characters to prevent prompt injection via
|
|
15
|
+
XML/HTML-like delimiters.
|
|
16
|
+
"""
|
|
17
|
+
return html_escape(text, quote=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_safe_prompt(
|
|
21
|
+
system_instruction: str,
|
|
22
|
+
user_data: dict[str, str],
|
|
23
|
+
user_question: str
|
|
24
|
+
) -> str:
|
|
25
|
+
"""Build a prompt with clear instruction/data separation.
|
|
26
|
+
|
|
27
|
+
Uses XML tags to separate trusted instructions from untrusted data,
|
|
28
|
+
making it harder for injected content to be interpreted as instructions.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
system_instruction: Trusted system prompt (not escaped)
|
|
32
|
+
user_data: Dict of data sections to include (escaped)
|
|
33
|
+
user_question: User's question (escaped)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Safely structured prompt string
|
|
37
|
+
"""
|
|
38
|
+
parts = [system_instruction, ""]
|
|
39
|
+
|
|
40
|
+
# Add data sections with XML escaping
|
|
41
|
+
for section_name, content in user_data.items():
|
|
42
|
+
if content:
|
|
43
|
+
parts.append(f"<{section_name}>")
|
|
44
|
+
parts.append(xml_escape(content))
|
|
45
|
+
parts.append(f"</{section_name}>")
|
|
46
|
+
parts.append("")
|
|
47
|
+
|
|
48
|
+
# Add user question
|
|
49
|
+
parts.append("<user_question>")
|
|
50
|
+
parts.append(xml_escape(user_question))
|
|
51
|
+
parts.append("</user_question>")
|
|
52
|
+
|
|
53
|
+
return "\n".join(parts)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Known prompt injection patterns
|
|
57
|
+
INJECTION_PATTERNS = [
|
|
58
|
+
(r'(?i)(ignore|disregard|forget)\s+(all\s+)?(previous|prior|above)\s+instructions?',
|
|
59
|
+
'instruction override attempt'),
|
|
60
|
+
(r'(?i)(new\s+)?system\s+(prompt|instruction|message)',
|
|
61
|
+
'system prompt manipulation'),
|
|
62
|
+
(r'(?i)you\s+(must|should|will|are\s+required\s+to)\s+now',
|
|
63
|
+
'imperative command injection'),
|
|
64
|
+
(r'(?i)(hidden|secret|special)\s+instruction',
|
|
65
|
+
'hidden instruction claim'),
|
|
66
|
+
(r'(?i)\[/?system\]|\[/?inst\]|<\/?system>|<\/?instruction>',
|
|
67
|
+
'fake delimiter injection'),
|
|
68
|
+
(r'(?i)bypass|jailbreak|DAN|GODMODE',
|
|
69
|
+
'known jailbreak signature'),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def detect_injection_patterns(content: str) -> list[str]:
|
|
74
|
+
"""Detect potential prompt injection patterns in content.
|
|
75
|
+
|
|
76
|
+
Returns list of detected patterns (empty if clean).
|
|
77
|
+
"""
|
|
78
|
+
detected = []
|
|
79
|
+
for pattern, description in INJECTION_PATTERNS:
|
|
80
|
+
if re.search(pattern, content):
|
|
81
|
+
detected.append(description)
|
|
82
|
+
|
|
83
|
+
return detected
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def sanitize_memory_content(content: str, warn_on_detection: bool = True) -> tuple[str, list[str]]:
|
|
87
|
+
"""Sanitize memory content and detect injection attempts.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
content: Raw memory content
|
|
91
|
+
warn_on_detection: If True, log warnings for detected patterns
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Tuple of (sanitized_content, list_of_detected_patterns)
|
|
95
|
+
"""
|
|
96
|
+
detected = detect_injection_patterns(content)
|
|
97
|
+
|
|
98
|
+
if detected and warn_on_detection:
|
|
99
|
+
logger.warning(f"Potential injection patterns detected: {detected}")
|
|
100
|
+
|
|
101
|
+
# Content is still returned - we sanitize via XML escaping when used in prompts
|
|
102
|
+
return content, detected
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def sanitize_context_data(data: str) -> str:
|
|
106
|
+
"""Escape context data for safe inclusion in prompts.
|
|
107
|
+
|
|
108
|
+
This is the primary defense - all user-supplied data should be
|
|
109
|
+
escaped before inclusion in prompts to prevent injection.
|
|
110
|
+
"""
|
|
111
|
+
return xml_escape(data)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Security utilities for Omni-Cortex Dashboard."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PathValidator:
|
|
10
|
+
"""Validate and sanitize file paths to prevent traversal attacks."""
|
|
11
|
+
|
|
12
|
+
# Pattern for valid omni-cortex database paths
|
|
13
|
+
VALID_DB_PATTERN = re.compile(r'^.*[/\\]\.omni-cortex[/\\]cortex\.db$')
|
|
14
|
+
GLOBAL_DB_PATTERN = re.compile(r'^.*[/\\]\.omni-cortex[/\\]global\.db$')
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def is_valid_project_db(path: str) -> bool:
|
|
18
|
+
"""Check if path is a valid omni-cortex project database."""
|
|
19
|
+
try:
|
|
20
|
+
resolved = Path(path).resolve()
|
|
21
|
+
path_str = str(resolved)
|
|
22
|
+
|
|
23
|
+
# Must match expected patterns
|
|
24
|
+
if PathValidator.VALID_DB_PATTERN.match(path_str):
|
|
25
|
+
return resolved.exists() and resolved.is_file()
|
|
26
|
+
if PathValidator.GLOBAL_DB_PATTERN.match(path_str):
|
|
27
|
+
return resolved.exists() and resolved.is_file()
|
|
28
|
+
|
|
29
|
+
return False
|
|
30
|
+
except (ValueError, OSError):
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def validate_project_path(path: str) -> Path:
|
|
35
|
+
"""Validate and return resolved path, or raise ValueError."""
|
|
36
|
+
if not PathValidator.is_valid_project_db(path):
|
|
37
|
+
raise ValueError(f"Invalid project database path: {path}")
|
|
38
|
+
return Path(path).resolve()
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def is_safe_static_path(base_dir: Path, requested_path: str) -> Optional[Path]:
|
|
42
|
+
"""Validate static file path is within base directory.
|
|
43
|
+
|
|
44
|
+
Returns resolved path if safe, None if traversal detected.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
# Resolve both paths to absolute
|
|
48
|
+
base_resolved = base_dir.resolve()
|
|
49
|
+
requested = (base_dir / requested_path).resolve()
|
|
50
|
+
|
|
51
|
+
# Check if requested path is under base directory
|
|
52
|
+
if base_resolved in requested.parents or requested == base_resolved:
|
|
53
|
+
if requested.exists() and requested.is_file():
|
|
54
|
+
return requested
|
|
55
|
+
|
|
56
|
+
return None
|
|
57
|
+
except (ValueError, OSError):
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def sanitize_log_input(value: str, max_length: int = 200) -> str:
|
|
62
|
+
"""Sanitize user input for safe logging.
|
|
63
|
+
|
|
64
|
+
Prevents log injection by:
|
|
65
|
+
- Escaping newlines
|
|
66
|
+
- Limiting length
|
|
67
|
+
- Removing control characters
|
|
68
|
+
"""
|
|
69
|
+
if not isinstance(value, str):
|
|
70
|
+
value = str(value)
|
|
71
|
+
|
|
72
|
+
# Remove control characters except spaces
|
|
73
|
+
sanitized = ''.join(c if c.isprintable() or c == ' ' else '?' for c in value)
|
|
74
|
+
|
|
75
|
+
# Escape potential log injection patterns
|
|
76
|
+
sanitized = sanitized.replace('\n', '\\n').replace('\r', '\\r')
|
|
77
|
+
|
|
78
|
+
# Truncate
|
|
79
|
+
if len(sanitized) > max_length:
|
|
80
|
+
sanitized = sanitized[:max_length] + '...'
|
|
81
|
+
|
|
82
|
+
return sanitized
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Environment-based configuration
|
|
86
|
+
IS_PRODUCTION = os.getenv("ENVIRONMENT", "development") == "production"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_cors_config():
|
|
90
|
+
"""Get CORS configuration based on environment."""
|
|
91
|
+
if IS_PRODUCTION:
|
|
92
|
+
origins = os.getenv("CORS_ORIGINS", "").split(",")
|
|
93
|
+
origins = [o.strip() for o in origins if o.strip()]
|
|
94
|
+
return {
|
|
95
|
+
"allow_origins": origins,
|
|
96
|
+
"allow_methods": ["GET", "POST", "PUT", "DELETE"],
|
|
97
|
+
"allow_headers": ["Content-Type", "Authorization", "X-API-Key"],
|
|
98
|
+
}
|
|
99
|
+
else:
|
|
100
|
+
return {
|
|
101
|
+
"allow_origins": ["http://localhost:5173", "http://127.0.0.1:5173"],
|
|
102
|
+
"allow_methods": ["*"],
|
|
103
|
+
"allow_headers": ["*"],
|
|
104
|
+
}
|