Commit Graph

474 Commits

Author SHA1 Message Date
Joeseph Grey
fa1fe7f866 security: sanitize rendered research-report HTML (#364)
The visual research report is assembled from LLM output over crawled web
pages (untrusted content) and served under a relaxed `script-src
'unsafe-inline'` CSP. Two values reached that HTML without sanitization:

- `_md_to_html` rendered the report markdown via python-markdown, which
  passes raw HTML through verbatim, so `<script>` / `<img onerror>` /
  `<svg onload>` / `javascript:` links carried in crawled content ran in
  the app origin.
- `category` (from the /api/research/start request body, no enum check) was
  interpolated raw into `<body class="category-{category}">`.

Allowlist-sanitize the rendered markdown with nh3, keeping the formatting
the report emits (tables, code, details/summary, toc anchors, codehilite
classes, external-link target/rel) while dropping active content, and
html.escape the category. Adds regression tests.
2026-06-04 13:42:49 +01:00
Alexander Kenley
7b45a94b6d Fix calendar routing and user-local time context (#408)
* fix(chat): add user-local time context

* fix(chat): route calendar follow-up phrasing

* refactor(chat): log tool intent routing reasons

* test(chat): align user time prompt shim

---------

Co-authored-by: Alex Kenley <Alex.Kenley@threatvectorsecurity.com>
2026-06-04 13:20:04 +01:00
tanmayraut45
f59edee611 Support extra CA bundle for private-CA LLM providers (#769)
Adding GigaChat (Sber) or an on-premise enterprise LLM gateway as a
model endpoint fails on first probe with

    CERTIFICATE_VERIFY_FAILED: self-signed certificate in certificate
    chain (_ssl.c:1000)

because their TLS chain is signed by a private root CA (Russian Trusted
Root CA for GigaChat; corporate CA for on-prem) that isn't part of the
default system / certifi trust store. The endpoint shows offline in
the picker even though the URL and API key are correct (issue #722).

The right fix is to extend the trust store, not to weaken verification.
This change:

- src/tls_overrides.py: new module that resolves an opt-in env var
  LLM_CA_BUNDLE at import time, builds a shared SSLContext via
  ssl.create_default_context() (so the system / certifi bundle is
  loaded first) and layers the operator's PEM on top with
  load_verify_locations(). Exposes llm_verify() returning a value
  suitable for httpx `verify=`. Defaults to True (httpx built-in
  trust) when the env var is unset, when the file is missing, or
  when the PEM fails to load — verification is never silently
  disabled, the warning is logged and we fall back to the safe path.

- src/llm_core.py: thread llm_verify() into the shared AsyncClient
  used by stream_llm / streaming completions.

- routes/model_routes.py: thread llm_verify() into the five httpx.get
  call sites in _probe_endpoint / _ping_endpoint so adding a
  private-CA endpoint goes green on the very first probe and the
  picker stops showing it offline.

- .env.example: document LLM_CA_BUNDLE with the GigaChat case as the
  concrete example.

Deliberately NOT included: a verify=False knob (global or per-host).
Disabling verification exposes the affected endpoint to MITM, and the
operator-supplied bundle is the correct fix for legitimate private-CA
providers — so the only switch in this PR is the safe one.

Closes #722.
2026-06-04 13:18:50 +01:00
SHORYA BAJ
f876fc7704 fix(cookbook): don't mark successful dependency installs as crashed (#1315)
Pip dependency installs are tracked as download tasks but finish with the
runner's "=== Process exited with code 0 ===" sentinel and pip's
"Successfully installed" line — never the HuggingFace download markers
(DONE / 100% / /snapshots/ / DOWNLOAD_OK) the download heuristics look for.

Once the tmux pane is gone, the backend's only completion check is the HF
cache lookup, which a pip package (e.g. llama-cpp-python[server], no "/")
never matches, so it reports "stopped" — and the frontend maps a stopped
download to "crashed". The reconnect loop's session-gone heuristic had the
same gap. Result: a clean install (exit 0) showed "crashed" in the Running
tab while the Dependencies tab correctly showed it installed.

Add a shared _depInstallSucceeded() helper that keys off the exit-0
sentinel (falling back to pip's success line, rejecting ERROR/Traceback)
and wire it into both the session-gone heuristic and the background status
reconciler, gated on payload._dep so real model downloads are unaffected.

Also fixes the pre-existing test_background_status_poll_reconciles_into_local_tasks
assertion that no longer matched the evolved reconciler, and adds regression
coverage for both paths.
2026-06-04 12:55:06 +01:00
ghreprimand
28c43121d7 Fix session export 500 on multimodal/None message content (#1984)
txt/html/md export joined and string-munged message.content directly, so a
multimodal turn (content is a list of blocks) crashed export with a TypeError
on join (txt) / AttributeError on .replace (html), and None content (tool-only
assistant turns) rendered as the literal 'None'. Add a _content_to_text helper
that flattens string/list/None to plain text and apply it at the three export
sites. JSON export is unchanged (it serializes structured content correctly).
Plain-string content is returned unchanged, so existing exports are identical.

Co-authored-by: ghreprimand <203024559+ghreprimand@users.noreply.github.com>
2026-06-04 12:53:44 +01:00
Alexandre Teixeira
f2b11ba94e tools: add read-only PR blocker audit helper
Adds a standalone read-only PR blocker audit helper with Markdown, terminal, and JSON output plus focused tests and documentation.
2026-06-04 12:51:48 +01:00
Giuseppe
68cb715914 fix(endpoint): import ModelEndpoint from core database
ModelEndpoint is defined in core.database, not src.database. The wrong
import silently prevented the module from loading in deployment
configurations that do not have a src/database.py shim, resulting in an
ImportError at startup.

Also adds a warning log when resolve_endpoint finds no usable model
(all models hidden or the list is empty), making the otherwise-silent
failure visible in operator logs.

The test_auth_regressions stub for src.endpoint_resolver was missing the
build_models_url attribute, which caused test collection errors.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 11:51:47 +01:00
Marius Popa
dc365a1b27 Fix Ollama agent single-token responses (#1591)
Agent mode treated local /v1 endpoints, including Ollama on :11434, as native-tool-capable by host/model heuristics. On Ollama's OpenAI-compatible surface some models that advertise tool support stop after a single token when schemas are sent (issue #1567). Default local Ollama /v1 back to fenced tool blocks unless the endpoint explicitly has supports_tools=True.

Also compare both the runtime chat URL and the normalized endpoint base when reading ModelEndpoint.supports_tools. That keeps a saved base URL such as http://localhost:11434/v1 effective when the active session URL is /v1/chat/completions.

Tests: .venv/bin/python -m pytest tests/test_tool_support_heuristic.py
2026-06-04 11:45:10 +01:00
Wes Huber
965185c6f9 fix(tests): pre-import real sqlalchemy/database in conftest to prevent stub contamination (#2398)
Some test files (e.g. test_llm_core_sanitize_tool_calls) stub
sqlalchemy and core.database at module level with
`if mod not in sys.modules`. During pytest collection these stubs
fire before the real modules are imported, contaminating every
subsequent test that needs real ORM objects (IntegrityError, missing
columns, etc.).

Pre-import the real modules in conftest.py so the module-level
guards find them already loaded and skip the stubs. Fixes ~10+
cascading test failures that only appear in the full suite.

Fixes #2395

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 11:39:49 +01:00
ooovenenoso
e163384015 fix: treat Nix files as readable uploads (#2249) 2026-06-04 12:06:24 +02:00
Nicholai
4dc11cfe6b refactor(memory): canonicalize memory imports (#50) 2026-06-04 05:31:15 +01:00
Yuri
a2e691da2b fix(models): stabilize proxy endpoint refresh behavior
* fix: support large proxy model endpoint refresh

Large OpenAI-compatible proxy endpoints can expose hundreds of models and make /v1/models slow. Treating those endpoints like local model servers caused model picker opens and background probes to repeatedly hit /models, producing timeouts and making otherwise usable endpoints appear offline.

Make model endpoint discovery cached-first for normal UI usage, add explicit proxy/API classification and refresh policy fields, exclude proxy/API endpoints from aggressive local probing, and preserve cached models when refresh fails.

Manual Test/Add/Refresh actions still fetch the full model list with longer timeouts so users can intentionally import large proxy model lists without blocking normal model picker usage.

* fix: preserve endpoint ping status semantics
2026-06-04 04:56:11 +01:00
Afonso Coutinho
09fe308720 fix(auth): revoke API tokens when deleting users
* fix: revoke API bearer tokens when their owner is deleted

* Re-run CI

* Invalidate bearer-token cache on user delete so warmed cached tokens stop working
2026-06-04 04:44:34 +01:00
Marius Popa
666babfd58 fix(documents): refresh library counters after removal (#1924) 2026-06-04 04:42:23 +01:00
Rudy Wolf
1c43daa564 fix(compare): stop blind mode leaking model identities via session names (#1318)
Blind Compare anonymized the pane headers, but each pane still created a helper chat session named "[CMP] <real-model>" and GET /api/sessions returned the session's model field. So the sidebar and the session-list API let a user map "Model A" back to its real model before voting, defeating the blind test.

- Frontend (static/js/compare/index.js, panes.js): in blind mode, name helper sessions by their neutral slot ("[CMP] Model A") instead of the model, matching the existing blind pane labels.
- Backend GET /api/sessions (routes/session_routes.py): blank the model field for [CMP]-prefixed helper sessions via a new _public_model helper.
- Backend /api/compare/start (routes/compare_routes.py): name blind sessions by slot and withhold model_left/model_right/mapping from the blind response (revealed at /vote).
- Tests: tests/test_blind_compare_redaction.py.

Fixes #1285.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 04:39:01 +01:00
hawktuahs
3d8c364689 [Bash] Fix Windows cookbook background tasks (#676)
* Fix Windows cookbook background tasks

* Add Windows Cookbook reliability follow-ups
2026-06-04 04:30:01 +01:00
Afonso Coutinho
49c14af5c7 fix(calendar): scope CalDAV event lookup by calendar
* fix: CalDAV sync hijacks another user's event sharing a VEVENT uid

* Seed schema-valid dtstart/dtend in caldav uid-scope test fixture
2026-06-04 04:01:21 +01:00
.bulat
e340674c12 Persist user prefs atomically (#1840) 2026-06-04 03:55:22 +01:00
lekt8
ceb62385f1 Fetch full messages with BODY.PEEK[] so read_email works on iCloud IMAP (#1961) (#1963)
read_email, reply_to_email and download_attachment fetched the full message with
the legacy bare RFC822 item (UID FETCH <uid> (RFC822)). iCloud's IMAP server
silently ignores it — the fetch returns status OK but only (UID <uid>) with no
body tuple, so the parse reports 'Email not found with UID' even though the
message exists and list_emails (which uses RFC822.HEADER) shows it. Gmail honours
(RFC822), which is why it only reproduced on iCloud.

Switch the three full-message fetches to (BODY.PEEK[]), which iCloud and Gmail
both honour and which doesn't set \Seen. Response shape is unchanged (raw bytes
still at msg_data[0][1]), so parsing is unaffected; the RFC822.HEADER (listing)
and (UID) probe fetches are left as-is.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 03:53:14 +01:00
Ocean Bennett
a38df08a31 fix(tests): use current python for rag id stability (#1817) 2026-06-04 03:49:59 +01:00
nubs
37e1d401cf fix(tests): clean agent loop import stubs 2026-06-04 03:44:49 +01:00
Afonso Coutinho
03dbf976a5 fix: image model ranking crashes on a non-string search filter (#1898) 2026-06-04 03:26:35 +01:00
Afonso Coutinho
5043b2924c fix: image model ranking crashes when system is not a dict (#1900) 2026-06-04 03:23:59 +01:00
Alexandre Teixeira
be8f1fac85 fix(tests): add endpoint URLs to remaining session fixtures 2026-06-04 03:14:43 +01:00
Afonso Coutinho
eac354629a fix: model cost/info matches first substring key (gpt-4o-mini billed as gpt-4o) (#1439)
* fix: match model name to the longest known key, not the first substring

* test: model key matching prefers the longest specific key
2026-06-04 03:05:37 +01:00
raf
2efebcc278 fix(tests): allow multiple logout calls when IMAP fallback reconnects (#1976)
_latest_inbox_fallback_uids logs out the broken connection before
reconnecting. The outer finally then logs out the new
connection. Both logouts are correct, the test assertion of == 1
was written before the reconnect logic existed. Changed to >= 1.
2026-06-04 02:56:05 +01:00
ghreprimand
82fcec6bb6 Replace core database utcnow defaults (#1457)
Co-authored-by: ghreprimand <203024559+ghreprimand@users.noreply.github.com>
2026-06-04 02:50:19 +01:00
Wes Huber
6e66e69451 fix(tests): add endpoint URL to archived session seeds
The sessions table now enforces NOT NULL on endpoint_url, but the
test fixture omitted it when seeding archived sessions, causing
IntegrityError on all three test cases.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 02:32:54 +01:00
Vykos
5f58f9a45f fix(ai): scope tool model resolution by owner
* Stabilize full test collection

* Scope AI tool model resolution by owner
2026-06-04 00:37:28 +01:00
Vykos
aaef6b1c49 fix(search): align content URL guards
* Stabilize full test collection

* Align search content URL guards
2026-06-04 00:34:06 +01:00
Vykos
193dc2f085 fix(uploads): bound direct upload reads
* Stabilize full test collection

* Add bounded reads for direct uploads
2026-06-04 00:32:50 +01:00
Vykos
5869106089 test: stabilize full test collection 2026-06-04 00:27:29 +01:00
Mahdi Salmanzade
271489a10c fix(research): owner-scope endpoint resolution
POST /api/research/start (require_privilege "can_use_research" — a normal
user, not admin) resolves an endpoint two ways and feeds the row's *decrypted*
api_key + base_url into research_handler.start_research(llm_endpoint=,
llm_headers=):

  1. body.endpoint_id  -> query(ModelEndpoint).filter(id == endpoint_id,
                          is_enabled == True).first()
  2. no endpoint + nothing configured -> query(ModelEndpoint).filter(
                          is_enabled == True).first()

Neither was owner-scoped. ModelEndpoint is a per-user resource (core/database.py:
non-null owner = private, "the model picker only shows the endpoint to that
user"). So a research-privileged user (or a chat-scoped token) could pass another
user's PRIVATE endpoint_id — or fall through to their first-enabled row — and run
research against that owner's endpoint: spending their API key / quota and
reaching whatever internal base_url they configured (SSRF).

This is the same multi-tenant owner-scoping class already fixed for
companion/models, the /api/v1/chat session gate (#870), and the /api/v1/chat
first-enabled fallback (#1045, _first_enabled_endpoint). These two sinks on the
research path were missed.

Extract `_owned_enabled_endpoint(db, owner, endpoint_id=None)` which scopes via
the shared owner_filter helper (own rows + legacy null-owner shared rows),
matching webhook_routes._first_enabled_endpoint and session_routes._owned_endpoint.
Used for both sinks. A scoped miss on the explicit-id path returns the existing
404 ("Endpoint not found or disabled"), so endpoint existence isn't revealed. A
null/empty owner stays a no-op (single-user / legacy mode).

Add regression tests pinning both lookups (cross-owner rejected, own-row
allowed, legacy shared-row allowed, disabled-skipped, fallback never borrows,
null-owner no-op).
2026-06-03 23:19:28 +01:00
Mahdi Salmanzade
729a30a10e fix(compare): owner-scope endpoint key lookup
POST /api/compare/start (a normal-user route — no admin gate) creates two
caller-owned [CMP] sessions from caller-supplied endpoint URLs (endpoint_a /
endpoint_b), then copies a ModelEndpoint's *decrypted* api_key into each
session's headers by matching on URL:

    ep = db.query(ModelEndpoint).filter(ModelEndpoint.base_url == base).first()

The match was not owner-scoped. ModelEndpoint is per-user (core/database.py:
non-null owner = private, "the model picker only shows the endpoint to that
user"). So a user could pass another user's endpoint base_url, have that owner's
api_key copied into a [CMP] session they own, then drive /api/chat_stream on that
session — spending the victim's API key / quota and reaching whatever base_url
they configured. Same multi-tenant owner-scoping class already fixed for
companion/models, /api/v1/chat (#870, #1045), session create/switch-model
(#1093), and /api/research/start (#1099).

Extract `_owned_endpoint_by_url(db, base_url, owner)` which scopes the match via
the shared owner_filter helper (own rows + legacy null-owner shared rows),
mirroring session_routes._owned_endpoint. A scoped miss copies no key (the
comparison session simply carries no borrowed credential). A null/empty owner
stays a no-op (single-user / legacy mode).

Add regression tests pinning the scoped match (cross-owner rejected, own-row
allowed, legacy shared-row allowed, no-match None, null-owner no-op).
2026-06-03 23:17:12 +01:00
Afonso Coutinho
b6607d219d fix(memory): owner-scope memory route session access 2026-06-03 23:13:56 +01:00
pewdiepie-archdaemon
67b63e9844 Revert "fix(ui): allow manual prompt bar resize (#1201)"
This reverts commit 258e6fc0d4.
2026-06-03 23:04:28 +09:00
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
b1a4ed13b0 Harden API-token chat endpoint selection
Validate only token-supplied direct base_url values for API-token chat requests, while keeping admin-configured endpoints available for local/LAN providers.

Scope configured endpoint fallback selection to the API token owner, fail closed for unknown token owners, and preserve strict session ownership checks when resuming sessions from chat-scoped API tokens.

Add focused regression coverage for direct base_url SSRF rejection, configured endpoint fallback behavior, token-owner scoping, URL validation, and null-owner session/endpoint handling.
2026-06-03 13:05:13 +01:00
Alexandre Teixeira
145f4fd2b4 feat(models): support pinned endpoint model IDs 2026-06-03 13:00:07 +01:00
Alexandre Teixeira
1284b14a13 feat(docker): add standalone GPU compose files for stack UIs 2026-06-03 12:54:35 +01:00
Alexandre Teixeira
a75dd4a231 fix(search): apply recency UTC fix to live ranking module 2026-06-03 12:49:32 +01:00
Alexandre Teixeira
0deeba58ba tests(llm): cover Anthropic temperature clamping 2026-06-03 12:28:53 +01:00
red person
93249a14b0 Keep compact font family names together (#1263) 2026-06-03 14:24:30 +09:00
Shaw
b10e6bc870 fix(cookbook): install llama-cpp-python[server] so llama.cpp serving works (#730) (#1338)
The llama.cpp serve auto-install built a bare `llama-cpp-python` in the Linux
source-build fallback and the Termux path, but the serve command runs
`python3 -m llama_cpp.server`, which needs the `[server]` extra. Because the
"already installed?" guard only checks `import llama_cpp` (a bare install
satisfies it), the missing extra was never added, so serving crashed with
`ModuleNotFoundError: No module named 'starlette_context'` (issue #730).

- Request the `[server]` extra in both the Termux direct install and the Linux
  Python-bindings fallback (the Windows path already used `[server]`).
- Shell-quote the package spec in `_pip_install_fallback_chain` via `shlex.quote`
  so the `[server]` brackets aren't treated as a bash glob; plain names unaffected.

Tests: tests/test_cookbook_helpers.py gains extras-quoting coverage and a
serve-runner regression guard.
2026-06-03 14:24:26 +09:00
Shaw
552bc15067 fix(search): degrade to empty results on non-JSON provider responses (#1129) (#1352)
tavily_search, serper_search and google_pse_search parsed response.json()
inside the network try block, which only caught httpx.RequestError and
RateLimitError. When a provider returned a non-JSON body (an HTML error page, a
truncated/empty body, a gateway 5xx), response.json() raised an UNCAUGHT
json.JSONDecodeError that aborted the search in the background — exactly the
'search engines other than SearXNG fail in the background' symptom.

brave_search already handles this correctly: it parses JSON in its own try
block and returns [] on json.JSONDecodeError. Mirror that in the other three
providers so a malformed provider response degrades to no-results instead of
propagating an exception.

Adds tests/test_search_provider_json.py: a non-JSON 200 body now yields [] for
tavily, serper, google_pse, and brave (the last guards the reference behaviour).

Co-authored-by: NubsCarson <nubs@nubs.site>
2026-06-03 14:24:23 +09:00
Shaw
e678ff753f fix(email): guard _decode_header against unknown MIME charset (#1354)
A header that declares an unknown or invalid MIME charset (e.g. a malformed
or spam Subject like =?x-unknown-charset?B?...?=) raised an uncaught
LookupError. bytes.decode(..., errors="replace") only handles byte-decode
errors, not codec *lookup* failures, so the "replace" safety net did not
apply.

_decode_header decodes Subject/From/To/Cc for the inbox list, single-message
fetch, and the background mail pollers (routes/email_routes.py,
routes/email_pollers.py, src/builtin_actions.py), so a single bad message
could crash the whole inbox render or the poller loop.

Wrap the per-part decode in try/except (LookupError, ValueError) and fall
back to utf-8/replace. Valid charsets (utf-8, iso-8859-1, ...) are unchanged.

Adds tests/test_email_decode_header.py — the unknown-charset case fails
before this change and passes after.
2026-06-03 14:24:20 +09:00
Shaw
bfbbc9b479 fix(calendar): keep recurring events with a UTC UNTIL from collapsing to one (#1383)
Events are stored with a naive (UTC) dtstart, but standard .ics exporters
(Google, Apple, Outlook, Fastmail) write the recurrence bound as an absolute
UTC value, e.g. FREQ=DAILY;UNTIL=20240105T090000Z. dateutil refuses to mix a
tz-aware UNTIL with a naive DTSTART ("RRULE UNTIL values must be specified in
UTC when DTSTART is timezone-aware"), so _expand_rrule's except branch swallowed
the ValueError and silently downgraded the event to non-recurring — every
occurrence after the first vanished from the calendar.

When dtstart is naive, strip the trailing Z from UNTIL so it matches the naive
DTSTART before parsing. No effect on tz-aware dtstarts or naive-UNTIL rules.

Adds tests/test_calendar_rrule_until_utc.py — a daily series bounded by a UTC
UNTIL expands to all 5 occurrences (fails before: returns 1, non-recurring).

Co-authored-by: NubsCarson <nubs@nubs.site>
2026-06-03 14:24:14 +09:00
Afonso Coutinho
fb8a744cae fix: skill retrieval boosts on tag substrings (e.g. 'ai' tag for any 'email' query) (#1406)
* fix: match skill tags as whole tokens, not substrings, in retrieval

* test: skill tag matching uses whole tokens, not substrings

* test: give skill fixtures status=published so they reach the scoring path
2026-06-03 14:24:11 +09:00
Shaw
49bf73b228 fix(forms): keep PDF-form export from dropping values when the label has '*' (#1407)
parse_markdown_to_values — the read-back path for export-pdf, the export
preview, and prepare-signed-reply — matched the bold field label with [^*]+, so
it could not match a label containing '*' (the near-universal required-field
marker: "Email *", "State *", "Signature *"). The value then stayed empty, so
the exported PDF and the signed-reply attachment came out blank for that field
with no error — a whole form of required fields could export completely empty.

Match the label non-greedily (.+?) so '*' in labels is tolerated while still
splitting at the first ':**' / '**[', which also preserves a value that itself
contains ':**'.

Adds tests/test_form_markdown_roundtrip.py (render -> parse roundtrip): asterisk
text/choice/signature labels survive (fail before, pass after); plain labels and
colon-bearing values are unaffected.

Co-authored-by: NubsCarson <nubs@nubs.site>
2026-06-03 14:24:07 +09:00