Commit Graph

207 Commits

Author SHA1 Message Date
pewdiepie-archdaemon
a19b6d2d4d Cookbook scheduler: calendar events drive model serve windows (experimental, feature-flagged)
Add a calendar-driven scheduler so a user can pick a model in Cookbook, click "Schedule…" instead of "Launch", choose time windows + days of the week + (optional) end date, and have Odysseus auto-launch the serve when the window starts and hard-kill it when the window ends. The calendar IS the source of truth — events on a designated calendar are interpreted as serve schedules, so editing the event in the calendar UI immediately changes the schedule.

Whole feature is gated by setting `cookbook_scheduler_enabled` (default False). Disabling the setting silences the reconciler and the API refuses requests; setting + three new files = entire surface, easy to revert.

New files:
  - src/cookbook_scheduler.py — background reconciler: ticks every 60s, reads next ±90s of calendar events on the designated calendar, launches/kills serves to match. Honors "refuse if GPUs busy" (skips with reason, no retry). Adopts pre-existing manual serves matching the event's model so window-end cleanup still applies. Tags scheduler-owned tasks with `_scheduledBy: <event_uid>` so it never kills serves it doesn't own.
  - routes/cookbook_schedule_routes.py — POST /api/cookbook/schedule/from-cookbook builds RRULE+ICS events from the modal's input (model, slots[], days[], until). GET /upcoming returns the next 24h with per-event status (scheduled / running / adopted / skipped / failed / ended) for the UI. POST /reconcile-now manually kicks the reconciler.
  - static/js/cookbookSchedule.js — Schedule button click handler + modal. Daily/hourly time slot picker, multi-slot ("+ add another time slot"), weekday chips with Weekdays/Weekend/Every-day quicksets, optional Until date. Calls /from-cookbook on save. Whole module is a single IIFE; deleting the file plus its <script> tag removes the UI surface.

Existing files touched (minimal):
  - app.py: register the new router + add the reconcile loop as a startup task (~10 lines, all in one block). Reconcile loop checks the feature flag on every tick, so leaving it running with the flag off costs ~one settings lookup per minute.
  - static/index.html: one new <script> tag for cookbookSchedule.js.
  - static/js/cookbookServe.js: add a "Schedule…" button next to the existing Launch button. Hidden by default; cookbookSchedule.js reveals it after confirming the feature flag is on.
  - static/style.css: ~80 lines for the modal styles (mobile-aware via @media).

User choices baked in:
  - Calendar events are the source of truth.
  - Refuse to launch if GPUs busy (skip + log reason in scheduler.events[uid].reason).
  - Hard kill at event end.
  - No retry on a skipped event within the window.
  - Multi-slot per day supported (one calendar event per slot, shared RRULE).
  - Pre-existing manual serves get adopted at window start so they're killed at end.

Known follow-ups (not in this commit):
  - Settings UI to pick the schedule calendar + toggle the feature flag.
  - Calendar event color/badge for status (running/skipped/failed).
  - "Lazy launch on first request" — currently launches at event start. Replacing _launch_serve with a proxy that defers vllm until the first chat request is a contained future change.
2026-06-05 02:35:23 +09:00
pewdiepie-archdaemon
9112861d8e cookbook agent debug loop: persistent log files, auto-adopt orphan tmux, Codex/Claude skill parity
Three converging fixes so the chat agent + external Codex/Claude skills can actually debug a crashed serve instead of staring at a post-crash neofetch banner:

* Serves now `tee` to /tmp/odysseus-tmux/SESSION.log on the host running them. Runner saves fds 3/4 before the tee and restores them right before `exec ${SHELL}`, so the post-crash interactive zsh banner does NOT pollute the log file.
* `tail_serve_output` (chat agent) and `/api/codex/cookbook/output/{sid}` (Codex+Claude skills) both prefer the persistent log file over the tmux pane. Pane is fallback for sessions predating the tee runner. Default tail bumped 150 -> 400.
* `list_served_models` "recent log" snippet seeks to the Traceback line instead of showing the last 6 lines (which was always the bash prompt).

Cookbook auto-adoption sweep on `/api/cookbook/tasks/status`: every 20s (rate-limited) the cookbook SSHes each configured server, finds `serve-*` / `cookbook-*` tmux sessions running an actual model process (vllm/python/llama-server/etc., filtered via `pane_current_command`), and writes them into state.tasks. So when the agent falls back to raw ssh+tmux, the session appears in the Cookbook UI on the next poll.

`serve_model` error path now reads `data["detail"]` in addition to `data["error"]` so the FastAPI HTTPException message ("Invalid characters in cmd") actually reaches the agent instead of being swallowed as a generic "Serve failed". Tool description updated to warn against `cd …`/`source …`/`&&` prefixes.

Intent-without-action supervisor in agent_loop: when the model writes "Let me tail the output" / "I'll check the logs" / "Let me investigate" and ends the turn without emitting a tool call, the loop injects a sharp system nudge ("You said you would X — DO IT NOW") and continues. Capped at 2 nudges per chat so a model that genuinely cannot use the tool does not pin the loop.

Codex/Claude skill parity: adds `/cookbook/cached`, `/cookbook/presets`, `/cookbook/preset/{name}`, `/cookbook/adopt` so external agents have the same surface as the chat agent. SKILL.md docs + odysseus_api.py wrapper updated for both bundles.

`adopt_served_model` promoted to the always-on tool set so the agent has a documented fallback when serve_model rejects a cmd.

Also various cookbook UI tweaks accumulated alongside the above (cookbook.js, cookbookRunning.js, cookbookServe.js, cookbook-diagnosis.js, settings.js, style.css).
2026-06-04 23:27:18 +09:00
Giuseppe
f6a5f6592f fix: log warnings on silently swallowed agent and endpoint failures (#2367)
get_builtin_overrides() was swallowing all exceptions with a bare
`except Exception: pass`, so misconfigured tool-description overrides
would silently produce wrong agent behaviour with no log trace.

The background endpoint refresh loop had the same pattern: any probe
failure was silently ignored, giving operators no signal that the
refresh was broken.

Also removes a circular self-import (`from src.agent_loop import
_build_base_prompt`) inside _build_system_prompt; the function is
already in scope and the import created a latent circular reference risk.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 12:29:31 +01:00
Kenny Van de Maele
d25052e43f chore: remove unused imports in calendar_routes (#2221)
routes/calendar_routes.py imports several names it never uses (pyflakes):
typing.Tuple, dateutil.rrule.{rruleset,DAILY,WEEKLY,MONTHLY,YEARLY}, and
auth_helpers.get_current_user. Drop them (the whole DAILY/WEEKLY/MONTHLY/
YEARLY line goes; rrulestr and require_user are kept). No behaviour change.
2026-06-04 12:13:18 +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
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
Paulo Victor Cordeiro
bd4067cf83 fix: guard remaining uid.decode() calls in auto-classify spam path (#1860)
Two more bare uid.decode() calls at lines 889 and 897 crash with
AttributeError when uid is already a string. Applies the same
isinstance guard used everywhere else in this function.
2026-06-04 04:06:10 +01:00
.bulat
e340674c12 Persist user prefs atomically (#1840) 2026-06-04 03:55:22 +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
pewdiepie-archdaemon
48f5182286 Merge branch 'codex-on-main' 2026-06-04 08:27:41 +09:00
pewdiepie-archdaemon
089246614d feat: Claude Agent integration + cookbook reconnect + UI polish
- Claude Agent integration: AGENT_CONFIGS.claude, INTG_TYPES.claude,
  setup_claude_routes + integrations/claude/ skill bundle. Wired in
  app.py alongside the existing Codex integration; same scope-gated
  /api/codex/* backend; agent form has new description so users know
  it's setup for an external CLI, not an agent streamed inside Odysseus.
- Remove mark_email_boundaries action: not good enough yet. Stripped
  from task UI, scheduler defaults, registry, tool schema, clear-cache
  route. Added to RETIRED_HOUSEKEEPING_ACTIONS so existing rows + their
  task_runs auto-purge on startup.
- Cookbook download reliability: "Reconnect" fix button in the crash
  diagnosis runs _reconnectTask after probing has-session. 30s confirm
  window before marking a download "done" — kills the Finished/Downloading
  flicker when tmux briefly drops between captures.
- Mobile UX: tap anywhere on a note card body opens the editor;
  Update button morphs to Archive when no text was edited; bell icon
  accent-colored; chip-trashing notif pills fade so only the icon
  rotates into the trash zone.
- Settings integrations: SVG-per-provider in email + API preset
  dropdowns, custom drop-up-aware menus, accent sub-header icons
  (IMAP/SMTP), consistent card styling between list + edit, contacts
  Edit/Delete icons, agent form description copy.
2026-06-04 08:27:26 +09: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
Sushanth Reddy
c58cb067f2 fix(calendar): avoid double-encrypting CalDAV password
cfg is loaded from prefs and already holds the existing, already-encrypted
password. When the edit form was re-submitted without re-typing the
password, the elif branch called encrypt() on that stored ciphertext,
compounding the encryption on every save and eventually breaking sync with
a decrypt error.

Drop the elif branch: the stored value is preserved as-is, and we only
encrypt when a new password is actually supplied.

Fixes #1915

Co-authored-by: EkaTantra Dev <dev@ekatantra.com>
2026-06-03 22:59:40 +01:00
pewdiepie-archdaemon
5939aec69f Codex Agent integration: HTTP surface + plugin bundle + Settings UI
This persists work that had been living only in the cookbook docker
container's writable layer — never committed to the host source. Brought
back to git intact, app.py registration re-applied surgically on top of
current main (not the older container copy, which would have regressed
the Windows MIME fix, asynccontextmanager lifespan, and webhook auth
exempts).

routes/codex_routes.py (new):
- GET  /api/codex/capabilities  — what this Odysseus exposes.
- GET  /api/codex/plugin.zip    — downloads integrations/codex as a zip.
- GET  /api/codex/todos         — scope-gated todos:read|write.
- POST /api/codex/todos         — scope-gated todos:write.
- GET  /api/codex/emails        — scope-gated email:read|draft|send.
- GET  /api/codex/emails/{uid}  — single-message fetch.
- _scope_owner() enforces api_token scopes before touching user data.

routes/api_token_routes.py (+103 lines):
- Adds Codex-token-specific issuance + revocation paths.

integrations/codex/ (new bundle, shipped via /api/codex/plugin.zip):
- README.md                       — install instructions.
- .codex-plugin/plugin.json       — Codex plugin manifest.
- scripts/odysseus_api.py         — Python client used by the skill.
- skills/odysseus/SKILL.md        — Codex skill definition.

static/js/settings.js (+253 lines):
- New "Codex Agent" option in the Integrations dropdown.
- Add / edit panel with plugin-bundle download link + curl-with-token
  install instructions per agent.

app.py:
- 7-line surgical change: capture email_router = setup_email_routes()
  and register setup_codex_routes(email_router=email_router) after the
  email module so the Codex routes can borrow its helpers.
2026-06-03 22:49:09 +09:00
pewdiepie-archdaemon
1f6c5ac66b Revert "Codex Agent integration: HTTP surface + plugin bundle + Settings UI"
This reverts commit 8c2705b42a.
2026-06-03 22:47:00 +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
pewdiepie-archdaemon
8161c1253d Merge branch 'main' of github.com:pewdiepie-archdaemon/odysseus 2026-06-03 22:39:33 +09:00
pewdiepie-archdaemon
8c2705b42a Codex Agent integration: HTTP surface + plugin bundle + Settings UI
This persists work that had been living only in the cookbook docker
container's writable layer — never committed to the host source. Brought
back to git intact, app.py registration re-applied surgically on top of
current main (not the older container copy, which would have regressed
the Windows MIME fix, asynccontextmanager lifespan, and webhook auth
exempts).

routes/codex_routes.py (new):
- GET  /api/codex/capabilities  — what this Odysseus exposes.
- GET  /api/codex/plugin.zip    — downloads integrations/codex as a zip.
- GET  /api/codex/todos         — scope-gated todos:read|write.
- POST /api/codex/todos         — scope-gated todos:write.
- GET  /api/codex/emails        — scope-gated email:read|draft|send.
- GET  /api/codex/emails/{uid}  — single-message fetch.
- _scope_owner() enforces api_token scopes before touching user data.

routes/api_token_routes.py (+103 lines):
- Adds Codex-token-specific issuance + revocation paths.

integrations/codex/ (new bundle, shipped via /api/codex/plugin.zip):
- README.md                       — install instructions.
- .codex-plugin/plugin.json       — Codex plugin manifest.
- scripts/odysseus_api.py         — Python client used by the skill.
- skills/odysseus/SKILL.md        — Codex skill definition.

static/js/settings.js (+253 lines):
- New "Codex Agent" option in the Integrations dropdown.
- Add / edit panel with plugin-bundle download link + curl-with-token
  install instructions per agent.

app.py:
- 7-line surgical change: capture email_router = setup_email_routes()
  and register setup_codex_routes(email_router=email_router) after the
  email module so the Codex routes can borrow its helpers.
2026-06-03 22:38:05 +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
pewdiepie-archdaemon
3706d756f3 Merge remote-tracking branch 'origin/main' into visual-pr-playground
# Conflicts:
#	routes/cookbook_routes.py
#	routes/hwfit_routes.py
#	services/hwfit/fit.py
#	services/hwfit/models.py
#	static/js/cookbook-diagnosis.js
#	static/js/cookbook-hwfit.js
#	static/js/cookbook.js
#	static/js/cookbookRunning.js
2026-06-03 16:49:10 +09:00
pewdiepie-archdaemon
eb79b76432 Cookbook: scoring fixes, UI polish, false-finished + stale-state bug fixes
Backend (services/hwfit + routes):
- rank_models picks visible set by REQUESTED column, not always score —
  sorting by Param now shows highest-param models PERIOD (incl. too_tight).
- New fit_only param. Multi-GPU rigs filter GGUF Q*/IQ quants (vLLM/SGLang
  cannot serve them); default non-prequantized to BF16 on 2+ GPUs.
- AWQ / GPTQ-8bit get a -1.0 quality penalty (was 0.0, tied with FP8), so
  FP8 wins when both fit.
- Version-aware tiebreaker (parse Mn.n / Vn) — MiniMax-M2.7 ranks above
  M2.5 on equal composite score; >=100B integers not misread as versions.
- /api/cookbook/hf-latest no longer drops models without an "NB" pattern in
  the repo id (MiniMax-M2.7, DeepSeek-V4-Pro etc. were silently filtered).
- Cached-model scan: atexit flushes models JSON even if the script is
  killed mid-walk; each scan_dir wrapped in try/except; timeout 60s -> 180s.
- KB granularity for sub-MB sizes (was "0 MB" for 12 KB shells). New
  "stalled" status for shells <1 MB with no .incomplete files.
- /api/cookbook/state POST guard: rejects "done" download tasks lacking
  DOWNLOAD_OK / DOWNLOAD_FAILED / /snapshots/ when the last-mentioned
  shard is N<total — stops stale tabs from poisoning persisted state.
- hf_models.json: add zai-org/GLM-5.1; flip zai-org/GLM-5 quantization
  Q4_K_M -> BF16 (it is the native base, not a quant).

Frontend (static/js):
- Scan/Download toolbar: quant defaults to All; ctx slider (8k/16k/32k/
  50k/128k/Max) ported from origin/main with sort=fit on drag, sort=score
  on Max. GPU toggle commits _activeCount to maxGpu on initial render. Fit
  column header tagged with active budget (RAM / GPU / N GPU).
- Foldable Download admin-card: the Download h2 is the chevron trigger;
  state persists in localStorage.
- Download card surfaces destination dir (Dir: <path>). Same dir on running
  task row, font/color matched to uptime (9px Fira Code muted, opacity .4).
- Serve panel ctx text input always resets to model max on open. Sub-MB
  cached models show with red "download stalled" badge.
- Bulk-select Cancel + Delete reset the Select button label on exit.
- Cookbook running: false-finished bug fixed — DOWNLOAD_OK or /snapshots/
  required; bare "Download complete" no longer marks the task done after
  the first config file. Clear button now sends tmux kill-session too.
  True overall % for multi-shard downloads: ((N-1)+frac)/total instead of
  hf_transfer per-shard aggregate.
- Diagnosis card simplified: removed fold toggle, copy button, dismiss X.
  Suggestion font matches message body (12px).
- HF token field flashes green check + "Saved" on save.
- Cached scan no longer counts stalled rows as downloaded in Scan/Download.

CSS:
- dep Install button width pinned to 76px to match Installed split.
- task-sub row +1px; task-status badge gets margin-right 8px.
- Ctx slider styled like gallery editor sliders (thin pill rail, red thumb).
- Bulk-select cancel button top -3px -> -5px.
2026-06-03 16:32:20 +09: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
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
Ruben G.
87fc675ccb fix(cookbook): auto-register a local endpoint when serving an LLM (#1380)
Serving a diffusion model auto-registered an image endpoint so it appeared in the model picker, but serving an LLM (llama.cpp/vLLM/SGLang/Ollama) did not — a downloaded-and-served model never showed up until the user manually ran /setup. Add _auto_register_llm_endpoint (text sibling of _auto_register_image_endpoint): parse the serve port (explicit --port, else Ollama 11434, else llama.cpp 8080), point an endpoint at http://host:port/v1, dedupe by base_url, and set supports_tools from --enable-auto-tool-choice. Wire it into /api/model/serve for any non-pip, non-diffusion serve.
2026-06-03 14:24:17 +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
Shaw
43ed3f7148 fix(contacts): parse Apple/iCloud item-grouped vCard EMAIL/TEL properties (#1438)
_parse_vcards matched property names with a bare line.startswith("EMAIL") /
"TEL" / "FN:" / "UID:". RFC 6350 property groups — emitted by default by Apple
Contacts / iCloud and many CardDAV servers — prefix the name with a group token,
e.g. item1.EMAIL;type=pref:jane@example.com. Those lines never matched, so emails
and phone numbers from any Apple-synced or Apple-exported address book were
silently dropped (breaking contact search by email, composer autocomplete, and
vCard/CSV export round-trips).

Strip an optional leading group token before matching and value extraction;
no-op for non-grouped lines.

Adds tests/test_contacts_vcard_parse.py (grouped + plain) — the grouped case
fails before this change and passes after.

Co-authored-by: NubsCarson <nubs@nubs.site>
2026-06-03 14:24:04 +09:00
Afonso Coutinho
f19265742c fix: SMTP envelope recipients split on commas inside display names (#1464) 2026-06-03 14:23:58 +09:00
Alexandre Teixeira
1c2ec288dd Check cudart before llama.cpp CUDA build (#1466) 2026-06-03 14:23:55 +09:00
lekt8
ffb8fd16bc Disable pip cache for Cookbook dependency installs (off the home disk) (#1477)
Cookbook dependency installs (vLLM and friends) build large wheels; pip's
default cache lives under $HOME/.cache/pip, so on a small home filesystem the
build dies mid-way with "[Errno 28] No space left on device" (issue #1219) and
the dependency ends up "installed" but unusable (issue #1459).

Add `--no-cache-dir` to the dependency pip-install command (the maintainer's
suggested PIP_CACHE_DIR= workaround, made the default) via a small
_pip_install_no_cache() helper applied at the install chokepoint. Consistent
with the existing --no-cache-dir on the llama-cpp-python build. Idempotent;
non-pip-install serve commands are untouched.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:23:49 +09:00
Afonso Coutinho
6e1df4ddc6 fix: POST /api/contacts/add crashes on JSON null name/email (None.strip()) (#1544) 2026-06-03 14:23:34 +09:00
Afonso Coutinho
26d040d116 fix: gallery records raw instead of display dimensions for EXIF-rotated photos (#1667) 2026-06-03 14:23:04 +09:00
Afonso Coutinho
fbb52a73a0 fix: re-importing an ICS file duplicates every tz-aware timed event (#1683) 2026-06-03 14:22:49 +09:00
Afonso Coutinho
96d59d2ff9 fix: _parse_dt does not understand 'tonight' so event start/end breaks (#1488) 2026-06-03 14:14:41 +09:00
Denis Kutuzov (Rybak27)
ec3b8b42ae fix: auto-naming for 24h time format (#1374)
* fix: auto-naming for 24h time format

needs_auto_name() required AM/PM suffix for default
frontend-generated names like 'deepseek-v4-flash 17:46:02'.
Frontend uses toLocaleTimeString() which outputs 24h
format in most locales — so the regex never matched and
auto-naming silently skipped.

Made AM/PM optional and added re.IGNORECASE for 'am'/'pm'.

* test: add regression tests for needs_auto_name (24h + 12h + custom)

---------

Co-authored-by: Calculator Dev <dev@calculator.local>
2026-06-03 14:14:34 +09:00
red person
ba6da17a92 Ignore non-object prefs JSON (#1257) 2026-06-03 14:12:45 +09:00
red person
84d54d9853 Ignore non-object embedding endpoint config (#1260) 2026-06-03 14:12:41 +09:00
Paulo Victor Cordeiro
844dbf6a22 fix: use safe .get for id lookup in uploads.json to prevent KeyError (#1465)
When uploads.json contains a malformed entry without an 'id' key,
the file-serve and lookup helpers crash with KeyError instead of
gracefully skipping the entry.
2026-06-03 14:12:00 +09:00
red person
5fba1735c2 Ignore invalid editor draft payloads (#1533) 2026-06-03 14:07:03 +09:00
Afonso Coutinho
10e797a1aa Normalize scheduled email offsets before storage
Normalize scheduled email send_at values with timezone offsets or Z suffixes to naive UTC before storing, matching the poller's lexicographic comparison format and preventing early/late sends.
2026-06-03 13:44:18 +09:00
Afonso Coutinho
076607c9b9 fix: archive browser model filter is suffix-only and drops matching models (#1709) 2026-06-03 13:34:54 +09:00
Afonso Coutinho
29e19f326a fix: _resolve_user_upload_path crashes on a non-dict resolve_upload result (#1715) 2026-06-03 13:34:33 +09:00
Mubashir R
319ba50a44 fix: validate client-supplied image _endpoint to prevent SSRF (gallery proxies) (#1718)
POST /api/image/harmonize and POST /api/image/inpaint read an `_endpoint` from
the request body and issue server-side httpx POSTs to it with no validation. A
caller can set `_endpoint` to http://169.254.169.254/ (cloud instance metadata)
or any internal/loopback address the server can reach, turning these routes into
an SSRF primitive.

routes/embedding_routes.py already runs its user-supplied endpoint through
src.url_safety.check_outbound_url; these two routes were missing the same guard.
Validate `_endpoint` the same way before any outbound request: non-HTTP(S)
schemes and the link-local metadata range are always rejected, and
IMAGE_BLOCK_PRIVATE_IPS=true blocks private/loopback for full lockdown (the
local-first default still allows LAN diffusion servers).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:34:17 +09:00
Afonso Coutinho
290d398900 fix: rewriting a message is lost on reload due to a non-existent DB column (#1729) 2026-06-03 13:31:19 +09:00