Commit Graph

24 Commits

Author SHA1 Message Date
pewdiepie-archdaemon
6861c41580 Reapply "Merge branch 'main' of github.com:pewdiepie-archdaemon/odysseus"
This reverts commit cc8fe2f6e3.
2026-06-03 22:47:00 +09:00
pewdiepie-archdaemon
cc8fe2f6e3 Revert "Merge branch 'main' of github.com:pewdiepie-archdaemon/odysseus"
This reverts commit 8161c1253d, reversing
changes made to 8c2705b42a.
2026-06-03 22:46:19 +09:00
Alexandre Teixeira
145f4fd2b4 feat(models): support pinned endpoint model IDs 2026-06-03 13:00:07 +01:00
ghreprimand
3eed73e11e Guard session message persistence after delete (#1451)
Co-authored-by: ghreprimand <203024559+ghreprimand@users.noreply.github.com>
2026-06-03 14:24:01 +09:00
Afonso Coutinho
56123e052b fix: compacting a chat with image attachments destroys the attachment (#1710) 2026-06-03 13:34:47 +09:00
Afonso Coutinho
926a4c59cb fix: 2FA bypassed when enabled but TOTP secret is missing (fail-open) (#1286)
* fix: fail closed when 2FA is enabled but the TOTP secret is missing

* test: totp_verify fails closed when secret missing, passes when 2FA off
2026-06-03 01:26:47 +09:00
PrabinDevkota
6b7dd4ea28 fix(auth): case-insensitive owner migration on username rename (#1183)
Use func.lower() when updating SQL owner columns, match prefs keys
case-insensitively, and normalize session usernames before comparing
during rename. Prevents silently skipping legacy mixed-case owner data.

Fixes #1165
2026-06-02 23:18:15 +09:00
Robin Fröhlich
096468a29f fix: persist and display multimodal messages (image/audio attachments) (#1159)
Multimodal content (list of {type, text/image_url} blocks) couldn't be
stored in the DB Text column, causing silent persist failures. On reload
the frontend fell back to String() on the array, rendering
[object Object],[object Object] in the chat.

- Serialize list content as JSON in _persist_message()
- Deserialize back to list in _db_to_session() via _parse_msg_content()
- Extract text parts from multimodal arrays in sessions.js instead of
  String() coercion
2026-06-02 22:37:48 +09:00
Shaw
db10c8d95b Sessions: allow deleting memory-only ghost sessions
A session that exists only in the in-memory SessionManager — never persisted,
or whose DB row was removed out-of-band — was listed by GET /api/sessions (the
list is built from the in-memory manager) but 404'd on every per-session
operation, so it could never be deleted.

Two causes, both fixed:

1. _verify_session_owner() only consulted the DB and raised 404 when no row
   existed. It now falls back to the in-memory session's owner when (and only
   when) a session_manager is supplied and the caller actually owns the ghost.
   The DB row stays authoritative when present, and a ghost owned by another
   user still 404s, so the ownership/security model is unchanged. The new
   parameter defaults to None, preserving behavior for all other callers.

2. SessionManager.delete_session() only removed the in-memory entry when a DB
   row was found, so memory-only ghosts survived. It now drops the in-memory
   copy regardless and reports success when either the DB row or the in-memory
   entry was removed.

Added tests/test_session_ghost_delete.py covering both layers, including the
cross-owner 404, the unauthenticated 403, DB-row-wins precedence, and backward
compatibility when no manager is passed.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 20:51:26 +09:00
mechramc
8efd7b3df6 Windows: improve Git Bash detection 2026-06-02 20:45:48 +09:00
Tatlatat
8ad436d25a DB: enable SQLite foreign key cascades
* fix(db): enable SQLite foreign keys so ondelete cascades actually fire

core/database.py declares DB-level FK actions throughout
(ondelete="CASCADE" / "SET NULL"), but SQLite disables foreign-key
enforcement per connection by default and the engine had no connect-event
listener turning it on. So every one of those ondelete actions was dead.

Concrete impact: cleanup_old_sessions() in src/cleanup_service.py removes
old sessions with a bulk `query(Session).delete()`, which bypasses the
ORM-level relationship cascade and relies solely on the DB-level
ondelete="CASCADE" on ChatMessage.session_id. With foreign keys off, the
messages are never deleted — they pile up as orphaned rows on every
cleanup cycle.

Add the standard SQLAlchemy connect listener issuing `PRAGMA
foreign_keys=ON`, guarded by `isinstance(conn, sqlite3.Connection)` so it
only affects SQLite and leaves other backends untouched.

tests/test_sqlite_foreign_keys.py inserts a Session + ChatMessage, deletes
the Session via bulk `query().delete()`, and asserts the ChatMessage is
cascade-deleted. Fails before this change (orphan remains).

* docs(db): clarify FK pragma scope per review; trim test comments

Address review feedback on the foreign_keys PRAGMA change:
- Note that the class-level connect listener fires for every Engine in the
  process and is a no-op on non-SQLite backends (isinstance guard).
- Warn near init_db() that FK enforcement is now global, so a migration
  that temporarily violates FK constraints must disable foreign_keys around
  that work.
- Drop the step-by-step narration comments from the regression test.

No behavior change.
2026-06-02 20:36:13 +09:00
mechramc
9d0a18a5b5 Email: add explicit SMTP security mode 2026-06-02 13:15:06 +09:00
Alexandre Teixeira
5dd5847d4b Revoke stale sessions after password change
After a successful password change, revoke all browser sessions for the
same user except the one that submitted the request. This prevents stale
sessions on other devices from remaining valid after credentials are
updated.

Keep API-token behavior unchanged. The current browser session is
preserved so the user can continue from the tab that changed the
password.

Add focused regression tests for preserving the current session, revoking
other sessions, persisting revocation, and avoiding revocation when the
current password is incorrect.
2026-06-02 05:59:22 +09:00
SurprisedDuck
7d10fb6260 Reserve internal sentinel usernames
`core.middleware.require_admin` grants admin to any request whose
`request.state.current_user == "internal-tool"` — the sentinel meant only
for the in-process tool-loopback path. But the normal cookie auth path
(app.py) sets `current_user` to the raw username, and neither `create_user`
nor the signup route reserved that name. As a result an account literally
named "internal-tool" was silently treated as admin by every
`require_admin`-gated route. With self-service signup enabled this is an
anonymous -> admin privilege escalation.

Reserve the full synthetic-owner set the codebase already special-cases —
"internal-tool", "api", "demo", "system" (see `_SYNTHETIC_OWNERS` in
routes/assistant_routes.py and the matching guards in src/task_scheduler.py
and routes/research_routes.py). "api" collides with the bearer-token owner
sentinel; "demo"/"system" would leave a real account denied an assistant
and inconsistently owner-scoped.

Refuse to create or rename into any reserved name (case/space-normalized),
and reject empty usernames while we're here. Adds a regression test.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-02 05:58:58 +09:00
Yatsuiii
63d93ff211 Normalize stored usernames on auth load
verify_password() and create_session() both call .strip().lower() on
the incoming username, but _load() stored keys verbatim from auth.json.
Any mixed-case key (e.g. written by manual edit or a future migration)
would never match, producing a permanent 'Invalid credentials' error.

Fix: lowercase all keys at load time so the in-memory dict always
matches what the login path expects.

Fixes #423
2026-06-02 05:50:36 +09:00
Collin
70a71f603c Scope email calendar extraction to account owner
The email auto-calendar pass (settings.email_auto_calendar / the
extract_email_events task) scans recently received mail and lets an LLM
create / update / cancel calendar events. Two problems made it a cross-tenant,
remotely triggerable hole:

1. No owner scoping. _auto_summarize_pass(account_id=None) fans out over EVERY
   enabled account of EVERY user. For each message it fetched an upcoming-events
   snapshot with NO owner filter (all tenants' events) and handed those uids +
   titles to the extraction LLM, then executed the model's ops via
   do_manage_calendar(...) with owner=None. do_manage_calendar only filters by
   owner when owner is not None, so create/update/delete ran across ALL users'
   calendars. Net: every user's event titles/times were disclosed to the model,
   and the model could cancel/move/duplicate any tenant's events by uid.

2. No prompt-injection wrapping. The raw email From/Subject/body were
   interpolated straight into an instruction-shaped extraction prompt (unlike
   the chat path, which wraps external text via src/prompt_security). Anyone
   who can email a user whose instance has auto-calendar enabled could inject
   operations: create attacker-controlled "meeting" events (the path even
   auto-harvests URLs from the body into the event location/description — a
   phishing primitive) or cancel/modify the victim's real events, with zero
   human in the loop.

Fix:
- Add core.database.get_upcoming_events(owner) and use it for the snapshot, so
  the LLM only ever sees the processed account owner's events.
- Look up the EmailAccount owner in _auto_summarize_pass_single and pass owner=
  to every do_manage_calendar call, so create/update/delete are scoped to that
  user (owner=None stays the single-user / legacy escape hatch).
- Tell the extraction model the email is untrusted data and not to follow
  instructions inside it (defense-in-depth against injection).

Add tests/test_calendar_owner_scope.py: get_upcoming_events returns only the
given owner's events (and everything when owner is None). Fails against the old
unscoped query.
2026-06-01 23:12:32 +09:00
roxsand12
766ddcaa99 fix: add _setup_lock to prevent race condition in first-run setup (#508) 2026-06-01 22:29:03 +09:00
Jamieson O'Reilly
171c29dcf3 Fix email-thread HTML injection, attachment path traversal, and missing authz (#475)
Hardens issues found in a security review of the current tree (separate from
the cookbook SSH PR):

- Email thread rendering (static/js/emailLibrary.js): the flat read path runs
  inbound HTML through the allowlist sanitizer, but the two threaded paths
  (_renderTurnsAsBubbles / _renderTurnsFromServer — the default view) injected
  server-parsed `body_html` raw into the DOM. A crafted inbound email could
  inject arbitrary markup (phishing/form/credential-capture/tracking; full XSS
  if a deployment relaxes the script CSP). Now sanitized on all paths.

- Attachment extraction (routes/email_routes.py, routes/email_helpers.py): the
  on-disk extraction dir was `ATTACHMENTS_DIR / f"{folder}_{uid}"` with
  user-controlled folder/uid and no containment, so a folder like `../../tmp`
  could escape ATTACHMENTS_DIR. New attachment_extract_dir() flattens both to a
  single safe segment and asserts containment.

- Diagnostics routes (routes/diagnostics_routes.py): /api/db/stats,
  /api/rag/stats, /api/test/youtube, /api/test-research relied only on the
  global session check (any logged-in user). Now require_admin-gated.

- Defense-in-depth HTML escaping: session HTML export escapes the session name
  (routes/session_routes.py); the MCP OAuth page escapes the reflected Host
  header / server_id (routes/mcp_routes.py).

- Internal-tool token now compared with secrets.compare_digest (constant time)
  in core/middleware.py and app.py.

Adds regression tests in tests/test_security_regressions.py.
2026-06-01 22:20:17 +09:00
pewdiepie-archdaemon
0888a3b3e6 Add native Windows compatibility layer 2026-06-01 15:09:47 +09:00
Collin
0a7de1fdf4 fix: stop leaking DB connections when persisting session mode (#64)
chat_routes.py persisted a session's "mode" in three best-effort spots —
reading the current mode, writing the effective mode, and setting
research_pending on the stream path. Each opened a session with SessionLocal()
and called .close() as the LAST statement inside a try/except, so if anything
before close() raised (e.g. a SQLite "database is locked" under concurrent chat
streams) the except only logged and the connection was never returned to the
pool.

DATABASE_URL defaults to file-backed SQLite, whose engine uses SQLAlchemy's
default QueuePool (5 connections + 10 overflow). Repeated leaks on these hot
paths exhaust the pool; later requests then block for pool_timeout and fail
with "QueuePool limit ... reached", taking the app down until restart.

Move the logic into two best-effort helpers in core.database, next to the
existing session helpers (update_session_last_accessed, get_session_by_id):

  - get_session_mode(session_id) -> Optional[str]
  - set_session_mode(session_id, mode) -> bool

Both route through the existing get_db_session() context manager, which commits
on success, rolls back on error, and always closes in a finally, so the
connection is returned to the pool on every path. chat_routes.py now calls
these instead of hand-rolling sessions, also removing three copies of the same
try/except.

Add tests/test_session_mode_helpers.py: the helpers commit+close on success
and, on a mid-operation DB error, swallow + roll back + close (no leak). The
error-path tests fail against the old close()-inside-try pattern.
2026-06-01 13:57:48 +09:00
pewdiepie-archdaemon
4e79ddcfb7 Add admin user rename 2026-06-01 12:52:58 +09:00
pewdiepie-archdaemon
577f2cfc18 Fix chat message history timestamps 2026-06-01 11:18:18 +09:00
pewdiepie-archdaemon
fc7f107b22 Improve Ollama setup and model endpoint handling 2026-06-01 10:00:15 +09:00
pewdiepie-archdaemon
e5c99a5eee Odysseus v1.0 2026-05-31 23:58:26 +09:00