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.
Files changed (27) hide show
  1. omni_cortex-1.11.3.data/data/share/omni-cortex/dashboard/backend/.env.example +12 -0
  2. omni_cortex-1.11.3.data/data/share/omni-cortex/dashboard/backend/backfill_summaries.py +280 -0
  3. {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/chat_service.py +59 -32
  4. {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/database.py +305 -18
  5. {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/image_service.py +35 -16
  6. {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/logging_config.py +34 -4
  7. {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/main.py +451 -13
  8. {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/models.py +64 -12
  9. omni_cortex-1.11.3.data/data/share/omni-cortex/dashboard/backend/prompt_security.py +111 -0
  10. omni_cortex-1.11.3.data/data/share/omni-cortex/dashboard/backend/security.py +104 -0
  11. {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/uv.lock +414 -1
  12. {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/websocket_manager.py +24 -2
  13. omni_cortex-1.11.3.data/data/share/omni-cortex/hooks/post_tool_use.py +429 -0
  14. {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/hooks/pre_tool_use.py +52 -2
  15. omni_cortex-1.11.3.data/data/share/omni-cortex/hooks/session_utils.py +186 -0
  16. {omni_cortex-1.2.0.dist-info → omni_cortex-1.11.3.dist-info}/METADATA +237 -8
  17. omni_cortex-1.11.3.dist-info/RECORD +25 -0
  18. omni_cortex-1.2.0.data/data/share/omni-cortex/hooks/post_tool_use.py +0 -160
  19. omni_cortex-1.2.0.dist-info/RECORD +0 -20
  20. {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/project_config.py +0 -0
  21. {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/project_scanner.py +0 -0
  22. {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/dashboard/backend/pyproject.toml +0 -0
  23. {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/hooks/stop.py +0 -0
  24. {omni_cortex-1.2.0.data → omni_cortex-1.11.3.data}/data/share/omni-cortex/hooks/subagent_stop.py +0 -0
  25. {omni_cortex-1.2.0.dist-info → omni_cortex-1.11.3.dist-info}/WHEEL +0 -0
  26. {omni_cortex-1.2.0.dist-info → omni_cortex-1.11.3.dist-info}/entry_points.txt +0 -0
  27. {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 FilterParams(BaseModel):
109
- """Query filter parameters."""
163
+ class MemoryCreateRequest(BaseModel):
164
+ """Create request for a new memory."""
110
165
 
111
- memory_type: Optional[str] = None
112
- status: Optional[str] = None
113
- tags: Optional[list[str]] = None
114
- search: Optional[str] = None
115
- min_importance: Optional[int] = None
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
+ }