mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-10 10: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>
150 lines
4.6 KiB
Python
150 lines
4.6 KiB
Python
# launcher.py
|
|
"""Dedicated entrypoint for the standalone Windows portable launcher.
|
|
|
|
Handles:
|
|
- Immediate GUI splash screen creation using tkinter.
|
|
- Suppressing console stream crashes in windowed GUI mode via NullWriter.
|
|
- Spawning system tray icon via pystray and Pillow (lazy-loaded).
|
|
- Auto-opening default browser pointing to the running backend.
|
|
- Launching the FastAPI server (importing and running app.py).
|
|
"""
|
|
import os
|
|
import sys
|
|
import threading
|
|
import time
|
|
import webbrowser
|
|
|
|
# PyInstaller multiprocessing children re-enter this executable with a private
|
|
# bootstrap argument. Consume it before splash/UI or application imports so a
|
|
# spawn-based worker does not relaunch the full desktop application.
|
|
if __name__ == "__main__":
|
|
import multiprocessing
|
|
multiprocessing.freeze_support()
|
|
|
|
# Define a dummy NullWriter to suppress standard stream crashes (isatty etc.) in GUI mode
|
|
class NullWriter:
|
|
def write(self, text):
|
|
pass
|
|
def flush(self):
|
|
pass
|
|
def isatty(self):
|
|
return False
|
|
|
|
if sys.stdout is None:
|
|
sys.stdout = NullWriter()
|
|
if sys.stderr is None:
|
|
sys.stderr = NullWriter()
|
|
|
|
|
|
splash_root = None
|
|
|
|
# If running from a frozen PyInstaller bundle, launch the splash screen IMMEDIATELY
|
|
if getattr(sys, 'frozen', False):
|
|
import tkinter as tk
|
|
|
|
def show_splash_instantly():
|
|
global splash_root
|
|
try:
|
|
splash_root = tk.Tk()
|
|
splash_root.title("Odysseus")
|
|
splash_root.overrideredirect(True)
|
|
splash_root.configure(bg="#1a1c23")
|
|
|
|
# Accented borders
|
|
splash_root.config(highlightbackground="#e06c75", highlightcolor="#e06c75", highlightthickness=1)
|
|
|
|
w, h = 360, 160
|
|
ws = splash_root.winfo_screenwidth()
|
|
hs = splash_root.winfo_screenheight()
|
|
x = (ws - w) // 2
|
|
y = (hs - h) // 2
|
|
splash_root.geometry(f"{w}x{h}+{x}+{y}")
|
|
|
|
tk.Label(splash_root, text="⛵ Odysseus", font=("Segoe UI", 22, "bold"), bg="#1a1c23", fg="#e06c75").pack(pady=(22, 2))
|
|
tk.Label(splash_root, text="Launching background services...", font=("Segoe UI", 10), bg="#1a1c23", fg="#d1d4e0").pack(pady=2)
|
|
tk.Label(splash_root, text="Please wait, this will take a few seconds.", font=("Segoe UI", 8, "italic"), bg="#1a1c23", fg="#5c6370").pack(pady=(12, 0))
|
|
|
|
splash_root.attributes("-topmost", True)
|
|
splash_root.mainloop()
|
|
except Exception:
|
|
pass
|
|
|
|
# Launch the GUI splash screen immediately on a background thread
|
|
threading.Thread(target=show_splash_instantly, daemon=True).start()
|
|
|
|
|
|
def create_tray_image():
|
|
# Generate a beautiful 64x64 icon matching Odysseus brand red accent (#e06c75)
|
|
from PIL import Image, ImageDraw
|
|
image = Image.new('RGBA', (64, 64), (0, 0, 0, 0))
|
|
dc = ImageDraw.Draw(image)
|
|
accent_red = (224, 108, 117, 255)
|
|
light_red = (224, 108, 117, 150)
|
|
|
|
# Draw premium sailing boat
|
|
dc.polygon([(32, 10), (32, 45), (12, 45)], fill=accent_red)
|
|
dc.polygon([(32, 18), (32, 45), (48, 45)], fill=light_red)
|
|
dc.polygon([(8, 48), (56, 48), (44, 56), (20, 56)], fill=accent_red)
|
|
return image
|
|
|
|
|
|
def on_open_browser(icon, item, url):
|
|
webbrowser.open(url)
|
|
|
|
|
|
def on_exit(icon, item):
|
|
icon.stop()
|
|
os._exit(0)
|
|
|
|
|
|
def setup_system_tray(url):
|
|
try:
|
|
import pystray
|
|
icon_img = create_tray_image()
|
|
menu = (
|
|
pystray.MenuItem('Open Odysseus', lambda icon, item: on_open_browser(icon, item, url), default=True),
|
|
pystray.MenuItem('Exit', on_exit)
|
|
)
|
|
tray_icon = pystray.Icon(
|
|
"Odysseus",
|
|
icon_img,
|
|
"Odysseus",
|
|
menu
|
|
)
|
|
tray_icon.run()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def open_browser(url):
|
|
# Allow uvicorn and app lifecycles to complete warmups
|
|
time.sleep(3.5)
|
|
|
|
# Safely close the splash screen
|
|
try:
|
|
global splash_root
|
|
if splash_root:
|
|
splash_root.after(0, splash_root.destroy)
|
|
except Exception:
|
|
pass
|
|
|
|
webbrowser.open(url)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
# Import the FastAPI app from app.py
|
|
from app import app
|
|
|
|
bind_host = os.getenv("APP_BIND", "127.0.0.1")
|
|
bind_port = int(os.getenv("APP_PORT", "7000"))
|
|
url = f"http://{bind_host}:{bind_port}"
|
|
|
|
if getattr(sys, 'frozen', False):
|
|
# Start browser manager thread
|
|
threading.Thread(target=open_browser, args=(url,), daemon=True).start()
|
|
# Start system tray manager thread
|
|
threading.Thread(target=setup_system_tray, args=(url,), daemon=True).start()
|
|
|
|
uvicorn.run(app, host=bind_host, port=bind_port, log_level="info")
|