mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-10 18:22: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>
73 lines
2.3 KiB
Python
73 lines
2.3 KiB
Python
# tests/test_launcher.py
|
|
import sys
|
|
import os
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
import pytest
|
|
|
|
from launcher import NullWriter, create_tray_image, on_open_browser, on_exit, open_browser
|
|
|
|
|
|
def test_frozen_multiprocessing_bootstrap_precedes_gui_and_app_imports():
|
|
source = Path("launcher.py").read_text(encoding="utf-8")
|
|
|
|
freeze = source.index("multiprocessing.freeze_support()")
|
|
splash = source.index("if getattr(sys, 'frozen', False):")
|
|
app_import = source.index("from app import app")
|
|
assert freeze < splash < app_import
|
|
|
|
|
|
def test_null_writer():
|
|
writer = NullWriter()
|
|
# writing and flushing should not raise any exceptions
|
|
writer.write("hello")
|
|
writer.flush()
|
|
assert writer.isatty() is False
|
|
|
|
|
|
def test_create_tray_image():
|
|
try:
|
|
from PIL import Image
|
|
img = create_tray_image()
|
|
assert isinstance(img, Image.Image)
|
|
assert img.size == (64, 64)
|
|
except ImportError:
|
|
pytest.skip("Pillow/PIL not installed in test environment")
|
|
|
|
|
|
def test_on_open_browser():
|
|
with mock.patch("webbrowser.open") as mock_open:
|
|
icon_mock = mock.Mock()
|
|
item_mock = mock.Mock()
|
|
url = "http://127.0.0.1:7000"
|
|
on_open_browser(icon_mock, item_mock, url)
|
|
mock_open.assert_called_once_with(url)
|
|
|
|
|
|
def test_on_exit():
|
|
with mock.patch("os._exit") as mock_exit:
|
|
icon_mock = mock.Mock()
|
|
item_mock = mock.Mock()
|
|
on_exit(icon_mock, item_mock)
|
|
icon_mock.stop.assert_called_once()
|
|
mock_exit.assert_called_once_with(0)
|
|
|
|
|
|
def test_open_browser():
|
|
with mock.patch("webbrowser.open") as mock_open, \
|
|
mock.patch("time.sleep") as mock_sleep:
|
|
|
|
# Test when splash_root is None
|
|
with mock.patch("launcher.splash_root", None):
|
|
open_browser("http://127.0.0.1:7000")
|
|
mock_open.assert_called_once_with("http://127.0.0.1:7000")
|
|
mock_sleep.assert_called_once_with(3.5)
|
|
|
|
with mock.patch("webbrowser.open") as mock_open, \
|
|
mock.patch("time.sleep") as mock_sleep:
|
|
# Test when splash_root is present and gets destroyed
|
|
mock_splash = mock.Mock()
|
|
with mock.patch("launcher.splash_root", mock_splash):
|
|
open_browser("http://127.0.0.1:7000")
|
|
mock_splash.after.assert_called_once()
|