* fix(editor): stabilize image workflows and editing controls
* docs(editor): add desktop and mobile visual validation evidence
* fix(editor): synchronize quick edit model events and register imagePreferences in test coordinator
* fix(settings): enforce authoritative global image-gen policy and non-admin preference persistence
- Enforce authoritative administrator global image_gen_enabled disable in get_user_setting, do_generate_image, and image_editing_provider_available, preventing non-admin user preference overrides while preserving user-disabled opt-outs and can_generate_images privilege checks across direct, gallery, and editor paths.
- Filter non-admin preference persistence in saveImagePreferences to user preference keys only (image_model, image_edit_model, image_quality), preventing global policy toggles from leaking into user preferences.
- Return accurate 200 OK Response from saveImagePreferences after fallback persistence succeeds so settings UI displays 'Saved' instead of false 403 error, while retaining visibility of real errors.
- Load per-user preferences via readImagePreferences in initImageSettings so non-admin selections persist and display correctly after page reload.
- Clear per-user model preferences on successful admin save (POST /api/auth/settings) via PUT /api/prefs/{key} with value: null to prevent stale per-user preferences from shadowing updated global settings.
- Support clearing keys in PUT /api/prefs/{key} when value is null.
- Add comprehensive Python regression tests for global-disabled/user-enabled, global-enabled/user-disabled, and authorized generation, plus Node and browser regression tests for non-admin persistence, toggle isolation, reload, and admin clearing.
* fix(settings): invalidate shared appConfig cache near auth settings write
* fix(test): isolate test_tool_policy unit suite from external model endpoint state
* fix(test): isolate web_fetch failure regression from contextual follow-up heuristics
* fix(test): isolate test_tool_policy from cross-test approval gate state
Ensure _patch_loop_basics disables TOOL_APPROVAL_GATE_ENABLED so routing tests in test_tool_policy run in the intended opt-out posture even when prior tests populate DATA_DIR with skills or untrusted context. Remove unnecessary forced_tools and contextual web mock workarounds.
* fix(settings): isolate non-admin image toggle and surface preference cleanup errors
- Restrict image generation availability toggle to admin users in imageModels.js, disabling the switch for non-admins and reverting unauthorized changes with an explicit message.
- Separate non-admin image model preference saves from global toggle updates.
- Track preference cleanup results on admin save in imagePreferences.js, propagating cleanup failures instead of ignoring them.
- Require valid preference keys on 403 fallback so toggle-only attempts reject cleanly.
- Add /api/prefs route mock to test_image_model_settings_sync_both_directions_and_survive_reopening.
- Add regression coverage for admin cleanup failures and non-admin toggle rejection.
* fix(settings): reconcile admin toggle on partial cleanup failure and report errors
- In imagePreferences.js, distinguish pre-persistence failures from post-save cleanup failures using err.globalSaved and err.phase.
- In imagePreferences.js, notify listeners of global policy updates on partial success before propagating cleanup failure.
- In imageModels.js, retain the persisted target state on admin toggle when global persistence succeeds but cleanup fails.
- In imageModels.js, reconcile admin toggle with authoritative server state via readImagePreferences() after ambiguous or partial failures.
- In imageModels.js, report partial success accurately without suppressing preference cleanup error messages.
- Add regression coverage in test_settings_save_response_ok_js.py for cleanup HTTP 500, cleanup network rejection, successful cleanup, and failed global POST.
---------
Co-authored-by: pewdiepie-archdaemon <pewdiepie-archdaemon@users.noreply.github.com>
Co-authored-by: Alexandre Teixeira <general@alexandreteixeira.dev>
* fix(chat): warn before "regenerate" permanently deletes later messages
Both "regenerate" entry points — the per-message footer's ↻ "Regenerate
from here" button, and the vision editor's "Regenerate message" button —
call POST /api/session/{id}/truncate, which does a real, permanent SQL
delete of every message after the point clicked, then generates a new
reply. Clicking either on anything but the very last exchange did this with
zero warning: the button's own label doesn't say it also erases the rest
of the conversation, and neither button visually signals it's destructive
differently from any other message-footer action.
Caught live: a mis-click destroyed a real conversation with no way to
recover it (no backup covered the affected window). This adds a
confirmation prompt — via the existing uiModule.styledConfirm — whenever
the action would delete more than just the message(s) being regenerated,
stating the exact count of messages that would be lost. Regenerating the
most recent exchange (the everyday case, nothing exists after it) is
unaffected and still proceeds with zero friction.
New test: tests/test_regenerate_truncate_warning_js.py — drives the real
regenerateFrom/resendUserMessage functions under Node with a fake message
list DOM, confirming: the last-exchange case never prompts and truncates
directly; an earlier case prompts with the correct count; cancelling
aborts before any network call; confirming proceeds with the correct
keep_count. Also fixed a pre-existing, unrelated gap this surfaced while
verifying the full suite: test_pr6020_browser_review_regressions.py's
Node harness never stubbed `navigator` at all (sessions.js reads
navigator.platform at module load) — a gap on origin/dev itself, not
caused by this branch's diff, using the same Object.defineProperty fix
already applied on #6019/#6148 for the sibling issue there (Node 21+'s own
getter-only navigator global, not relevant to this specific failure but
the same defensive pattern).
Verified: full pytest suite in-container, 5798 passed / 11 pre-existing
unrelated failures (README/docs/agent-loop baseline) / 0 new.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* test(chat): cover plain resend after dev's replace-by-default change
dev made plain resend (no options) replace from the clicked turn by
default, with append-only resend opt-in via { append: true } (see
test_resend_message_nondestructive.py). The footer "Resend message"
button calls resendUserMessage(el) with no options, so it now truncates
everything after the clicked turn and goes through this PR's warning.
Replace the old "plain resend never prompts" test, which assumed the
previous append-by-default behavior, with one asserting that plain
resend on an earlier pair prompts with the right count, and one
asserting that explicit append-only resend never truncates or prompts.
Also resolve ui.js/sessions.js through the same cache-busting query
string chat.js imports them with. ES modules cache by full URL, so the
harness's unversioned import of ui.js was a second module instance and
its uiMod.styledConfirm stub never reached the instance chat.js calls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(chat): warn before an edit deletes later messages; offer to fork instead
Sending an edited user message truncates the chat at that message, the
same unrecoverable server-side delete as regenerate and resend, but did
it with no prompt. It now goes through the same confirmation, worded for
the action ("Sending this edit will also permanently delete the N
messages after it"). Cancelling leaves the editor open with the draft.
The confirmation for all three actions (regenerate, resend, edit) also
offers "Fork from here": it forks the chat just before the user turn,
switches to the new chat, and replays the action there (the same message,
or the edited text), leaving the original chat untouched. forkFrom now
shares the same fork helper.
Tests drive the real editUserMessage Send handler under Node and cover
the edit prompt (count, cancel, last-message no-prompt) and the fork
choice for each action (fork keep_count, no truncate, replayed text).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(chat): cut and fork a chat by message id, not loaded-page position
Regenerate and edit sent keep_count, the clicked message's position among
the messages loaded in the page. A long chat loads only its newest messages
until the user scrolls up, so that position is smaller than the real one and
the truncate deleted real messages above the clicked one (regenerating the
latest reply in a ~50-message chat removed about a dozen earlier messages).
Resend already sent before_msg_id; regenerate and edit now do the same.
The fork options this PR adds had the same flaw. Fork now accepts
before_msg_id (fork before that message) and through_msg_id (include it),
resolved against the stored messages; keep_count stays as the fallback for a
message that has no id yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(chat): keep a regenerated turn's message id current; stop on a refused cut
Regenerate keeps the original user bubble on screen and resubmits its text
without drawing a new bubble. The resubmitted message is a new DB row, but
the bubble kept the deleted row's id, so the next regenerate's cut was
refused (404) and the page sent the turn anyway, stacking a second reply.
The stream now reports the saved user message's id (user_message_saved),
which the page attaches to the user bubble, or to the bubble a regenerate
kept. Regenerate and edit stop with an error when the server refuses the
cut instead of sending.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(chat): name the buttons in words instead of symbols
CONTRIBUTING asks for no Unicode emoji in UI or code; the test comments
used the regenerate and pencil glyphs to name buttons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(chat): share the fork-and-send step across edit, resend and regenerate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(chat): pass the regenerate fork its attachment ids explicitly
Resend already hands its collected file ids to _forkAndSend; regenerate
relied on the earlier _pendingRegenAttachments assignment. Pass _regenIds
too so both fork paths read the same, and check the hand-off in the
regenerate tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(chat): assert fork choices keep the user message's attachments
Run regenerate and resend's "Fork from here" against a user message with
an uploaded file and check that the send fired in the fork carries its
file id, replacing the source-text check. chat.js exposes a read-only
_pendingRegenAttachmentIds() for this, like cookbook-hwfit.js's test hooks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(chat): preserve attachments across edit and regeneration
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Alexandre Teixeira <general@alexandreteixeira.dev>
- Add a diagnosis for "NVIDIA driver on your system is too old" and
"CUDA driver version is insufficient for CUDA runtime version", the
errors PyPI's CUDA 13 torch/vLLM builds hit on drivers older than 580.
- Remove the nested _diagnose_serve_output copy in cookbook_routes.py.
It shadowed the helper and had drifted, so /api/cookbook/tasks/status
never used the helper's newer diagnoses.
- Capture the tmux pane with -J in the status poll so lines wrapped at
the pane width do not split error text mid-word.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Gallery search built a single `%{search}%` LIKE, so multi-word queries only matched the exact adjacent phrase ("red car" missed "a car that is red") and literal % / _ acted as SQL wildcards. Split on whitespace and require each token to match (prompt OR tags OR ai_tags), escaping LIKE wildcards per token; same escaping for the tag filter. Promote _escape_like to public escape_like_pattern() shared helper.
Part of #2123.
Tests: tests/test_gallery_search_multiword.py (5 tests); 4 fail pre-fix, 5 pass post-fix. Neighbors (session_search, gallery null-user, shims): 25 passed.
update_skill only checked whether the target directory existed. A skill
with that name in another category lives elsewhere on disk, so the
rename went through and left two skills with one name. Update and
delete look skills up by name, so the next delete removed whichever
file os.walk reached first.
Check the owner's skills in every category before renaming, and answer
409 from PUT /api/skills/{id} instead of the misleading 404.
In routes/cookbook_routes.py, the exception handler for GET /api/cookbook/hf-gguf-files referenced repo instead of the parameter repo_id, causing an unhandled NameError and 500 status when upstream requests to Hugging Face failed.
Fixes#6341.
Ensure the native candidate that passed the health check is prioritized and executed when an earlier broken candidate exists on PATH.
Reconcile Odysseus-owned shims safely, invalidate command hash cache, and verify launchability rather than command existence before serve dispatch.
Fixes#6079
- routes/gallery/gallery_routes.py: use stable diagnostic log message without
interpolating raw exception, resolver details, tokens, or destination strings.
- tests/e2e/teardown.js: remove environment-variable path overrides from production
code, strictly confine test options and temp directories, and prevent arbitrary
directory cleanup.
- Add regression tests covering non-leakage of synthetic tokens/queries/exceptions
into logs and responses, and path confinement during teardown.
- Reconcile conflict resolution in routes/editor_agent_routes.py and src/editor_agent_model.py
- Enforce strict server-authorized endpoint resolution and reject client URL overrides
- Enforce bearer-token privilege separation: delegated API tokens are capped at non-admin scope per src/tool_security.py and cannot inspect private base URLs or probe metadata
- Preserve chat-scope authorization, missing-session 401 rejection, and owner-scoped browser bridge/SSE queues
- Preserve administrator-disabled-tool denylist enforcement introduced by PR #6357
- Fix mobile toolbar flyout hit-testing and stacking context trapping on viewports <= 700px via scoped elevation and mask suppression
- Stabilize project recovery test fixture by awaiting document draft quiescence prior to baseline capture
* fix(security): stop the agent from re-enabling tools an admin disabled
settings.json:disabled_tools is the only global tool-permission boundary
the Settings > Agent Tools panel offers, yet manage_settings exposed an
`enable_tool` action that removed entries from it. Any agent turn could
undo the admin's restriction, and because the agent reads untrusted web
pages, emails and documents, a planted instruction could re-enable bash,
email or any other tool that had been switched off.
The denylist can now only be narrowed from a turn: `disable_tool` and
`list_tools` keep working, `enable_tool` is refused with a message that
points the user to Settings > Agent Tools, and the action is dropped from
the function schema, the prompt rule and the tool index so models are not
steered into calling it. The free-form set/delete/reset actions were
already unable to reach `disabled_tools`; a test now pins that.
Fixes#5523
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UAB5sz5srMKR7rbeDFtdMH
* fix(settings): close internal API tool-policy bypass
Refuse global tool-policy replacement from internal-token requests even when an admin owner is impersonated, and block the same write in app_api. Route explicit image re-enable requests to list_tools with a Settings pointer. Keep disabled_tools outside general settings actions even if a default is added later. Remove the retired action from capability metadata. Validation: 573 focused tests passed; the running app rejected handler, generic API and encoded-path internal HTTP attempts while preserving human administrator controls.
* fix(mcp): block agent writes to per-server MCP tool policy
PATCH /api/mcp/servers/{id}/tools only checked require_admin, which accepts
the internal tool token, and app_api still forwarded it, so an agent turn
could re-enable MCP tools an admin had switched off. Refuse the internal
token on that route (same check as POST /api/tools) and add PATCH
/api/mcp/servers to the app_api blocklist, with regression tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCbhLNC7mJsunxqfwwshqn
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Afonso Junior <chatgpt@yack.com.br>
* fix(email): guard user-supplied IMAP/SMTP hosts against SSRF
POST /api/email/accounts/test opened raw imaplib/smtplib connections to
whatever host:port the request body named, and POST/PUT /api/email/accounts
and PUT /api/email/config persisted arbitrary hosts for the background
pollers to contact. All of these sit behind require_user/require_owner with
no admin gate, so any authenticated user could make the server connect to
loopback, LAN and cloud-metadata targets (169.254.169.254) and read the
first 200 characters of the remote banner back out of the friendly error.
THREAT_MODEL.md lists email as admin-only; the tool layer honours that but
the HTTP routes did not.
Add check_outbound_host() to src/url_safety.py, a bare-host companion to
check_outbound_url() with the same classification (link-local, multicast,
reserved and unspecified always rejected; private/loopback only with
block_private) plus localhost-name handling and an explicit policy for names
that do not resolve. Wire it into the four email routes via
_check_mail_host(): non-admin owners (and everyone when
EMAIL_BLOCK_PRIVATE_IPS=true) also lose private/loopback targets, admins
keep the local-first LAN/Dovecot setups. Unresolvable names pass through
because the connect that follows fails with the same DNS error.
Regression tests cover the helper's policy table and each route: the test
route rejects before any socket opens, save routes reject before touching
the DB, admins keep loopback unless locked down, and an update that fails
validation leaves the stored row untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TwcwfY2uQMxNRsK3y6xBAp
* fix(email): harden IMAP/SMTP SSRF protection against rebinding, TOCTOU, and unvetted hosts
- P0: Implement safe_create_connection() in src/url_safety.py to resolve hostnames once, validate every returned address against SSRF policy, and connect directly to the pinned IP. This eliminates DNS rebinding and TOCTOU while preserving TLS SNI and certificate hostname verification across IMAP, IMAPS, STARTTLS, SMTP, and SMTP_SSL.
- P1: Ensure unresolvable or deferred hostnames cannot become an unrestricted future connection capability by enforcing runtime SSRF validation fail-closed on every connection, while maintaining compatibility with offline configurations.
- P2: Extend SSRF enforcement to the core connection layer (_open_imap_connection, _send_smtp_message, _imap_connect) using ContextVar owner scoping, protecting pre-existing DB rows, restored configs, background pollers, and scheduled operations.
- P3: Allow admin and single-user access to IPv6 loopback (::1, [::1]) for local mail servers (e.g. Dovecot) by checking loopback before is_reserved, while keeping link-local, cloud metadata (169.254.169.254, 100.100.100.200), and non-admin restrictions strictly enforced.
- Add comprehensive regression and adversarial test coverage in tests/test_email_account_host_ssrf.py.
* fix(email): reject unresolved saved hosts without stdlib monkeypatches
Use the existing explicit policy IMAP/SMTP classes and canonical connect_outbound_tcp helper. Remove process-wide stdlib assignments, context-variable plumbing, redundant aliases and resolver-signature retries. Reject unsafe or unresolved hosts before account/config persistence, keep DNS work off the event loop, return fixed client errors, identify Alibaba metadata accurately and reject invalid TCP ports. Validation: 654 relevant email, URL-safety and CodeQL regression tests passed, 3 skipped. Three unrelated browser checks were excluded after environment failures (missing Playwright browser and Chromium timeout). Running-app checks verified rejected create/update/config requests leave stored data unchanged, administrator LAN configuration still works, and metadata/non-admin loopback are blocked.
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: PR #6029 Reviewer <reviewer@odysseus-review.local>
Co-authored-by: Afonso Junior <chatgpt@yack.com.br>
* fix: block SSRF via user-supplied model endpoint URLs
* fix(test): stub DNS in loopback SSRF tests for deterministic results
Replace environment-dependent test_loopback_always_rejected with two
deterministic tests using a stub resolver:
- test_loopback_accepted_by_default (local-first, block_private=False)
- test_loopback_rejected_in_strict_mode (block_private=True)
The old test only passed when localhost resolved to ::1 (hit is_reserved),
not because loopback was actually always rejected.
Co-Authored-By: Santoshkumarpuppala
* fix: guard PATCH base_url against SSRF and add ipv6 loopback coverage
---------
Co-authored-by: isharak7m <192635824+isharak7m@users.noreply.github.com>
- Propagate preflight HTTPException(status_code=502) regardless of whether other mailbox groups exported messages into the temporary ZIP
- Guarantee temporary file cleanup via SpooledTemporaryFile.close() on preflight failure
- Preserve partial success (HTTP 200) for genuinely missing mail
- Add executable tests for multi-group preflight failure orderings, missing mail, and zero body fetches
- Require valid RFC822.SIZE preflight metadata for every message before body FETCH
- Fail safely with HTTP 502 without body fetching on NO, malformed, or inconsistent sizes
- Distinguish missing mail (HTTP 404) from bad size preflight (HTTP 502)
- Discard duplicate IMAP response records for already-processed UIDs in batch loop
- Correct print notification toast to "Prepared N emails for printing; M unavailable"
- Add dedicated Playwright browser regression test script to repository test suite
- Add request-level tests for preflight NO, empty, partial, malformed, and duplicates