mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-14 12:12:20 +02:00
* fix(security): keep agent file tools out of the app state directory
The agent's read tools (read_file, grep, glob, ls) resolved model-supplied
paths against a root list whose first entry was the whole data directory.
That directory holds the session store, the auth database, the app
encryption key and the settings file, so prompt-injected content could ask
for any of them. No approval prompt stood in the way: reads are classified
read_workspace and pass the untrusted-context gate untouched, which is
correct for reading a workspace and wrong for reading the app's own state.
The agent gets data/agent_workspace/ instead, and the subprocess cwd and
HOME move with it so bash and read_file agree on where scratch files live.
The deny itself is a property of the path, not of the root it arrived
through, because three routes reach the same bytes and closing only the
first leaves the other two working:
- the default root list
- a workspace bound at or above the data directory, which vet_workspace
accepted and chat_routes auto-binds from a path named in the message
- a tool_path_extra_roots setting covering the data directory
_resolve_search_root also returned the workspace root unchecked when the
path was empty, so a bare ls enumerated the directory whatever the deny
list said. It now resolves that case through the same guards.
A containment rule rather than a filename deny list, so state files added
later are covered without anyone remembering to list them, and so a user's
own settings.json or app.db inside a real workspace is not caught.
Four directories of user content stay readable, because the application
hands their paths to the model and tells it to open them: the chat upload
manifest, downloaded mail attachments, personal docs (which covers the
runbook) and personal uploads.
* fix: enforce state deny during recursive file search
* fix: bound protected filesystem searches
* fix(security): reject inode aliases and workspace redirects
* fix(security): harden partitioned agent searches
* fix(security): report fallback worker exits promptly
* fix(security): clean up search readers and retain relative data roots
---------
Co-authored-by: RaresKeY <158580472+RaresKeY@users.noreply.github.com>
155 lines
6.4 KiB
Python
155 lines
6.4 KiB
Python
# src/app_initializer.py
|
|
"""Initialize all application components and dependencies."""
|
|
import os
|
|
import logging
|
|
import stat
|
|
from typing import Dict, Any
|
|
|
|
from src.constants import (
|
|
DATA_DIR, PERSONAL_DIR, RUNBOOK_DIR, UPLOAD_DIR, AGENT_WORKSPACE_DIR,
|
|
SESSIONS_FILE, DEFAULT_HOST, OPENAI_API_KEY
|
|
)
|
|
from src.memory import MemoryManager
|
|
from src.memory_provider import MemoryProviderRegistry, NativeMemoryProvider
|
|
from services.memory.skills import SkillsManager
|
|
from core.session_manager import SessionManager
|
|
from core.models import set_session_manager
|
|
from src.personal_docs import PersonalDocsManager
|
|
from src.api_key_manager import APIKeyManager
|
|
from src.preset_manager import PresetManager
|
|
from src.chat_processor import ChatProcessor
|
|
from src.model_discovery import ModelDiscovery
|
|
from src.chat_handler import ChatHandler
|
|
from src.research_handler import ResearchHandler
|
|
from src.upload_handler import UploadHandler
|
|
from src.tool_utils import set_upload_handler
|
|
from src.search import update_search_config
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def create_directories():
|
|
"""Create necessary directories if they don't exist."""
|
|
for directory in (DATA_DIR, PERSONAL_DIR, RUNBOOK_DIR, UPLOAD_DIR):
|
|
os.makedirs(directory, exist_ok=True)
|
|
|
|
# The model-controlled workspace must be a real child of DATA_DIR. Never
|
|
# follow a pre-existing symlink here: it would silently move the default
|
|
# native-file root outside the application volume before any resolver runs.
|
|
data_root = os.path.realpath(os.path.abspath(os.path.expanduser(DATA_DIR)))
|
|
workspace = os.path.abspath(os.path.expanduser(AGENT_WORKSPACE_DIR))
|
|
expected_workspace = os.path.join(data_root, "agent_workspace")
|
|
# Validate the real parent so a supported DATA_DIR bind/symlink works, but
|
|
# require the fixed internal carve-out name and reject a link at the model-
|
|
# controlled workspace entry itself.
|
|
if (
|
|
os.path.basename(workspace) != "agent_workspace"
|
|
or os.path.realpath(os.path.dirname(workspace)) != data_root
|
|
):
|
|
raise RuntimeError("agent workspace must be the canonical child of DATA_DIR")
|
|
if os.path.lexists(workspace):
|
|
mode = os.lstat(workspace).st_mode
|
|
if stat.S_ISLNK(mode) or not stat.S_ISDIR(mode):
|
|
raise RuntimeError("agent workspace must be a real directory")
|
|
else:
|
|
os.mkdir(workspace, 0o700)
|
|
resolved_workspace = os.path.realpath(workspace)
|
|
if resolved_workspace != expected_workspace:
|
|
raise RuntimeError("agent workspace must be the canonical child of DATA_DIR")
|
|
try:
|
|
os.chmod(workspace, 0o700)
|
|
except OSError:
|
|
pass
|
|
|
|
def initialize_managers(base_dir: str, rag_manager=None) -> Dict[str, Any]:
|
|
"""
|
|
Initialize all manager and handler instances.
|
|
|
|
Args:
|
|
base_dir: Base directory path
|
|
rag_manager: RAG manager instance (optional)
|
|
Returns:
|
|
Dictionary containing all initialized components
|
|
"""
|
|
# Create directories first
|
|
create_directories()
|
|
|
|
# Initialize core managers
|
|
memory_manager = MemoryManager(DATA_DIR)
|
|
skills_manager = SkillsManager(DATA_DIR)
|
|
session_manager = SessionManager(SESSIONS_FILE)
|
|
set_session_manager(session_manager) # Enable Session.add_message() persistence
|
|
upload_handler = UploadHandler(base_dir, UPLOAD_DIR)
|
|
session_manager.upload_handler = upload_handler
|
|
set_upload_handler(upload_handler)
|
|
personal_docs_manager = PersonalDocsManager(PERSONAL_DIR, rag_manager)
|
|
api_key_manager = APIKeyManager(DATA_DIR)
|
|
preset_manager = PresetManager(DATA_DIR)
|
|
|
|
# Initialize memory vector store (share embedding model with RAG if available)
|
|
memory_vector = None
|
|
try:
|
|
from src.memory_vector import MemoryVectorStore
|
|
embedding_model = getattr(rag_manager, '_model', None) if rag_manager else None
|
|
memory_vector = MemoryVectorStore(DATA_DIR, embedding_model=embedding_model)
|
|
if memory_vector.healthy:
|
|
# Rebuild index from existing memories if empty
|
|
if memory_vector.count() == 0:
|
|
existing = memory_manager.load()
|
|
if existing:
|
|
memory_vector.rebuild(existing)
|
|
logger.info(f"Rebuilt memory vector index from {len(existing)} existing entries")
|
|
logger.info("MemoryVectorStore initialized")
|
|
else:
|
|
# Keep the unhealthy object (do NOT reset to None): consumers gate on
|
|
# `.healthy`, and service_health.chromadb_health() needs a present
|
|
# object to report DEGRADED/DOWN instead of DISABLED ("not configured").
|
|
logger.warning("MemoryVectorStore DEGRADED: ChromaDB vector memory unavailable")
|
|
except Exception as e:
|
|
logger.warning(f"MemoryVectorStore DEGRADED: {e}")
|
|
memory_vector = None
|
|
|
|
memory_provider_registry = MemoryProviderRegistry([
|
|
NativeMemoryProvider(memory_manager, memory_vector),
|
|
])
|
|
|
|
# Initialize processors
|
|
chat_processor = ChatProcessor(memory_manager, personal_docs_manager, memory_vector=memory_vector, skills_manager=skills_manager)
|
|
research_handler = ResearchHandler()
|
|
|
|
# Initialize chat handler with all dependencies
|
|
chat_handler = ChatHandler(
|
|
session_manager=session_manager,
|
|
memory_manager=memory_manager,
|
|
chat_processor=chat_processor,
|
|
research_handler=research_handler,
|
|
preset_manager=preset_manager,
|
|
upload_handler=upload_handler,
|
|
)
|
|
|
|
# Initialize model discovery
|
|
model_discovery = ModelDiscovery(DEFAULT_HOST, OPENAI_API_KEY)
|
|
|
|
# Load and apply saved API keys
|
|
saved_keys = api_key_manager.load()
|
|
if "brave" in saved_keys:
|
|
update_search_config(api_key=saved_keys["brave"])
|
|
logger.info("Loaded Brave API key from saved configuration")
|
|
|
|
return {
|
|
"memory_manager": memory_manager,
|
|
"memory_vector": memory_vector,
|
|
"memory_provider_registry": memory_provider_registry,
|
|
"skills_manager": skills_manager,
|
|
"session_manager": session_manager,
|
|
"upload_handler": upload_handler,
|
|
"personal_docs_manager": personal_docs_manager,
|
|
"api_key_manager": api_key_manager,
|
|
"preset_manager": preset_manager,
|
|
"chat_processor": chat_processor,
|
|
"research_handler": research_handler,
|
|
"chat_handler": chat_handler,
|
|
"model_discovery": model_discovery,
|
|
"current_presets": preset_manager.presets,
|
|
"PERSONAL_INDEX": personal_docs_manager.index
|
|
}
|