Repository navigation
chore: upgrade to Chatwoot v4.18.0 - #2
Merged
Merged
Conversation
…chatwoot#15337) When an agent starts a new conversation, the WhatsApp template picker in the composer only shows templates that were already loaded into the frontend store — it doesn't refresh when the composer opens. If a template sync completed after the store was last populated, the newly synced template shows up on the inbox's Settings > Templates page (which always refetches on load) but not in the New Conversation composer, since that view relied solely on the account-cache-invalidated websocket event, which doesn't always reach an already-open session in time. ## What changed - `ComposeConversation.vue` now dispatches a cache-aware `inboxes/get` refetch every time the composer popover opens, so the WhatsApp template list is current before an agent picks a template to message a customer. The refetch checks the account's cache key first and only re-pulls the full inbox list when it's actually stale, so it stays cheap in the common case. ## How to reproduce 1. Sync/update WhatsApp templates for an inbox (e.g. via Settings > Inboxes > [WhatsApp inbox] > Sync Templates). 2. Without reloading the page, open the New Conversation composer for that inbox and check the WhatsApp template picker — a newly synced template may be missing until this fix. --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Data imports created without a name now receive a readable default such as `Contacts - 2026-08-06`. This prevents legacy CSV contact imports from appearing as `Untitled import` under Settings → Data while preserving names supplied by users or other import providers. ### Closes - [CW-7877](https://linear.app/chatwoot/issue/CW-7877/add-default-names-for-unnamed-data-imports) ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? 1. Create a contact CSV import without supplying a name. 2. Open Settings → Data and confirm its name follows `Contacts - YYYY-MM-DD`. 3. Create an import with an explicit name and confirm that name remains unchanged. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
) ## Description **Problem.** The default Active Storage upload route, `POST /rails/active_storage/direct_uploads`, is mounted automatically by Rails and requires no authentication. Chatwoot doesn't rely on it, our dashboard and widget uploads all use scoped, authenticated endpoints, so the route just sits there letting anyone create blobs anonymously. **Fix.** Block the built-in route so it returns `403`. Chatwoot's own upload controllers inherit from the same Rails class but are left working, an `instance_of?` check makes the block apply only to the bare route, not to the subclasses that call `super`. Fixes https://linear.app/chatwoot/issue/INF-94 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? Added a request spec asserting the bare route returns `403` and creates no blob. Existing widget and conversation direct-upload specs still pass, confirming the scoped endpoints are unaffected. 13 examples, 0 failures across the three direct-upload specs; rubocop clean. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes --------- Co-authored-by: Sony Mathew <sony@chatwoot.com>
…woot#15326) When an agent has multiple tabs or windows open on the same account, an inbound WhatsApp call rings in all of them, as expected. But once the call is answered in one tab, the others never found out — they kept showing the incoming-call popup and playing the ringtone indefinitely, as if the call were still waiting to be picked up. ## How to reproduce 1. Log into the same agent account in two browser tabs. 2. Receive an inbound WhatsApp call (rings in both tabs). 3. Accept the call in one tab. 4. The other tab keeps ringing and shows the call as still incoming. ## What changed - The backend already broadcasts a `voice_call.accepted` event account-wide when a call is answered, but the dashboard never had a listener registered for it — the event was silently dropped. Added the missing handler in `actionCable.js` so every tab except the one that owns the now-active call clears its ringing state. - Added `root: true` to `.eslintrc.js` so ESLint config resolution stops at the project root instead of also picking up a parent directory's config (this repo's git-worktree layout nests worktrees under the main checkout, which was causing an ambiguous plugin-resolution error for anyone linting from a worktree). --------- Co-authored-by: Sony Mathew <sony@chatwoot.com>
…hatwoot#14103) ## Linear Ticket - https://linear.app/chatwoot/issue/CW-6884/nudge-users-with-a-dashboard-banner-when-2fa-backup-codes-run-low ## Description Shows a dashboard-wide banner when the signed-in user has 3 or fewer unused backup codes left (amber), turning to an alert style at 0 remaining. Clicking "Generate codes" takes the user to the MFA settings page so they can regenerate codes before they get locked out. Inspired by Google's post-backup-code-use nudges. ## How to test 1. Sign in as a user with MFA enabled. <img width="1512" height="824" alt="Screenshot 2026-08-05 at 4 52 15 PM" src="https://github.com/user-attachments/assets/08138f3e-cc15-451e-bbcc-7772dc2a875c" /> <img width="1507" height="701" alt="Screenshot 2026-08-05 at 4 54 02 PM" src="https://github.com/user-attachments/assets/580051fa-cbb5-47c1-81a3-258b7c5b5a03" /> 2. In a Rails console, simulate a low state by marking most backup codes as used: ```ruby u = User.find_by(email: '<your user>') codes = u.otp_backup_codes.dup (0...8).each { |i| codes[i] = 'XXXXXXXX' } u.otp_backup_codes = codes u.save! ``` 3. Reload any dashboard page — the amber banner should appear with a "Generate codes" CTA. 4. Click the CTA — it should route to **Profile → Two-Factor Authentication**, where you can regenerate codes. 5. Set the count to 0 (mark all 10 as `'XXXXXXXX'`) — banner should switch to the red/alert style. 6. Regenerate codes — banner should disappear on the next dashboard page load. ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules > Base is the [disable-with-backup-code PR branch](chatwoot#14102) so CTAs around recovery are consistent; rebase onto `develop` once that merges. --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Sony Mathew <sony@chatwoot.com>
Data Imports is now part of the shared paid-plan entitlement set. Startups, Business, and Enterprise accounts receive the feature through billing reconciliation, while Hacker/default accounts remain gated and the existing API/UI feature checks stay unchanged. ### Closes - [CW-7878](https://linear.app/chatwoot/issue/CW-7878/enable-data-imports-for-all-paid-cloud-plans) ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? 1. Reconcile a Hacker account and confirm Data Imports remains disabled. 2. Reconcile Startups, Business, and Enterprise accounts and confirm Data Imports is enabled for each paid tier. 3. Exercise the Stripe subscription update path and confirm the same plan hierarchy is applied. ## Rollout Existing paid accounts need a one-time reconciliation after deployment. Run the following in the Rails console: ```rb paid_plan_names = InstallationConfig.find_by!(name: 'CHATWOOT_CLOUD_PLANS').value.drop(1).pluck('name') paid_accounts = Account.where("custom_attributes ->> 'plan_name' IN (?)", paid_plan_names) puts "Reconciling #{paid_accounts.count} paid accounts" paid_accounts.find_each do |account| Enterprise::Billing::ReconcilePlanFeaturesService.new(account: account).perform end ``` Future plan changes and subscription renewals are handled by the normal Stripe reconciliation path. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
…chatwoot#15207) An agent who is removed from an inbox could still see that inbox's conversations under the **Participating** filter. Removing an agent from an inbox does not delete the conversation participant records they already had, and the participating filter was ignoring inbox access entirely — so those conversations stayed visible indefinitely. The filter now respects inbox access like every other conversation filter. ## Linear Ticket - https://linear.app/chatwoot/issue/CW-6923 ## How to reproduce 1. Add an agent to two inboxes, A and B. 2. As that agent, become a participant on a conversation in inbox B (open it, or get added as a participant). 3. Remove the agent from inbox B in Settings → Inboxes → Collaborators. 4. Log in as the agent and open Conversations → Participating. 5. The inbox B conversation is still listed, and is openable. ## What changed `ConversationFinder#filter_by_conversation_type` **replaced** `@conversations` with `current_user.participating_conversations` for the `participating` type, discarding the inbox/permission-filtered scope built up by `Conversations::PermissionFilterService` immediately before it. It now narrows the existing scope by participating ids instead, so permission filtering survives.
…twoot#14902) Captain assistants now support **audience** and **schedule** controls, so you can decide *who* an assistant replies to and *when* it's on duty. By default nothing changes, an assistant still responds to every conversation in its connected inboxes but you can now narrow that down. - **Audience**: build a condition tree (contact attributes, conversation attributes, and custom attributes) with and/or groups, mirroring the contact-segment filter semantics. Only conversations whose contact matches the audience get a Captain reply. - **Schedule**: choose when Captain replies — *Anytime*, *During business hours*, or *Outside business hours* (based on each inbox's configured working hours; inboxes without business hours are always covered). When an assistant opts out of a conversation (contact outside the audience, or off-schedule), the conversation is routed to the human queue instead of being parked pending on a silent bot — both on initial creation and on reopen. Fixes https://linear.app/chatwoot/issue/CW-7414/audience-and-availability-controls |Audience|Availability| |--|--| | <img width="1132" height="627" alt="Screenshot 2026-06-30 at 5 52 09 PM" src="https://github.com/user-attachments/assets/866910e0-e1d7-4248-8630-d91afc758688" /> | <img width="1131" height="539" alt="Screenshot 2026-06-30 at 5 52 13 PM" src="https://github.com/user-attachments/assets/aad0d6f7-ceb7-4546-a049-095c5b46b483" /> | ## How to test 1. Open **Captain → Assistants → (an assistant) → Settings**. 2. Under **Audience**, add a condition or condition group (e.g. `Contact language equal_to en`) and save. Start a conversation from a contact that does *not* match — Captain should stay silent and the conversation should land in the human (open) queue instead of pending. 3. With a matching contact, Captain should respond as before. 4. Under **Schedule**, pick **During business hours** (or **Outside business hours**) on an inbox that has working hours configured, and confirm Captain only engages within/outside that window. An empty/`Anytime` schedule always responds. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com> Co-authored-by: aakashb95 <aakashbakhle@gmail.com> Co-authored-by: iamsivin <iamsivin@gmail.com>
…hatwoot#15356) WhatsApp inbox responses now return an empty template list when the stored `message_templates` value is not an array. This prevents mobile clients from receiving the legacy/default object shape that can crash the WhatsApp reply box. Valid template arrays remain unchanged. Related: chatwoot/chatwoot-mobile-app#1127 ### Things to know - No database migration or backfill is included. - This does not change the web template-button behavior; that can be handled separately. ### How to reproduce 1. In a local environment, set a WhatsApp inbox's `message_templates` value to `{}`. 2. Load the inbox API response and open one of its conversations in the mobile app. 3. Confirm the response contains `message_templates: []` and the reply box does not crash. 4. Repeat with a valid template array and confirm the templates are preserved. Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Assigning a conversation to an Agent Bot now moves it to pending. Assigning a bot-owned pending conversation to a human opens it again, while other assignment changes preserve the existing status. This makes existing Agent Bot ownership behave like an AI handoff without depending on the assignment dropdown UI work. Closes: https://linear.app/chatwoot/issue/CW-7448/apply-agent-bot-assignment-behavior ## Why Agent Bot ownership should remove conversations from the main open queue while the bot is handling them. Explicit human takeover should bring a bot-owned pending conversation back to the open queue and clear the bot owner. ## What changed - Agent Bot assignment clears the human assignee and marks the conversation pending. - Human assignment clears the Agent Bot owner and opens the conversation only when it was bot-owned and pending. - Ordinary human assignment, non-pending bot takeover, and unassignment preserve the existing conversation status. - Manual human takeover uses the existing assignment and status events. Bot-initiated handoffs continue to use the existing bot-handoff event path. ## Validation - Assign an open conversation to an Agent Bot through the assignment API and verify it becomes pending. - Assign that bot-owned pending conversation to a human and verify it becomes open. - Verify ordinary human assignment, non-pending bot takeover, and unassignment do not force a status change.
AgentBot-owned conversations are now treated as assigned across conversation lists, counts, pagination, permissions, unread membership, human auto-assignment, and advanced assignee filters. ## Closes - https://linear.app/chatwoot/issue/CW-7689/align-agent-bot-ownership-with-unassigned-counts-and-pagination ## Follow-ups - https://linear.app/chatwoot/issue/CW-7870/refresh-agentbot-ownership-state-when-deleting-an-agent-bot tracks ownership refresh during AgentBot deletion. - https://linear.app/chatwoot/issue/CW-7899/refresh-saved-filter-totals-after-live-conversation-ownership-changes tracks the existing saved-filter header count refresh gap. ## Why The backend treated every conversation without a human assignee as unassigned, even when an AgentBot owned it. The frontend already hid AgentBot-owned conversations from the Unassigned list, so counts, pagination, filters, unread membership, direct-access permissions, and auto-assignment could disagree with the visible queue. ## What changed - Treat conversations with either a human assignee or AgentBot owner as assigned. - Keep conversation counts, pagination, unread memberships, advanced filters, and automation assignee conditions aligned with the shared ownership semantics. - Keep human assignee equality and not-equality filters human-only, even when a human and AgentBot have the same numeric ID. - Exclude AgentBot-owned conversations from both legacy and V2 human auto-assignment, and from unassigned-only Enterprise access. - Preserve AgentBot ownership when Twilio or WhatsApp call flows reuse or accept an assigned conversation. - Emit ownership-change updates when only the AgentBot owner changes, so connected clients refresh queue state. ## Validation - AgentBot assignment changed ownership to the bot, moved the conversation to pending, and removed it from the open queue. - Assignee "is present" returned human- and AgentBot-owned conversations; "is not present" returned only genuinely unassigned conversations. - Automation assignee presence conditions treated AgentBot ownership as present and did not execute the absent-owner path. - Live human-assignee equality and not-equality filters excluded AgentBot-owned conversations, including numeric ID collisions. - A 32-conversation pending queue loaded across pagination with matching totals and no missing or duplicate rows. - AgentBot ownership changes and human takeover updated filtered rows immediately without a reload. - Human takeover opened the conversation and restored the public reply composer; subsequent unassignment kept the conversation open. - An unassigned-only custom-role agent saw only genuinely unassigned conversations and could not see AgentBot-owned conversations. - AgentBot-owned conversations showed the handled-by-bot banner, Take over action, and disabled public reply composer. - New conversations in the connected inbox were assigned to the AgentBot and excluded from human auto-assignment. - Opening an AgentBot-owned conversation did not let the legacy assignment callback or its locked recheck replace the bot. - Moving an AgentBot-owned conversation to an auto-assigning team preserved the bot and did not create a second human owner. - Twilio conference pickup, Twilio outbound reuse, WhatsApp outbound reuse, and inbound WhatsApp acceptance preserved existing AgentBot owners. - Focused ownership, filters, pagination, permissions, unread-count, auto-assignment, frontend, and lint checks passed locally. - GitHub Actions, Docker builds, CircleCI, security checks, and the final Codex review are green on the final head.
## What changed This pull request adds three focused log emitters for Captain V2 response jobs: * `job_dequeued` when Sidekiq fetches the job from Redis * `job_skipped` when the conversation is not Pending at the first job guard * `response_discarded` when a newer customer message exists before model generation The dequeue middleware identifies V2 jobs by the triggering message ID in the third serialized Active Job argument. It does not log Captain V1 or other Sidekiq jobs. ## Why Recent production incidents have an enqueue record but no later job record. Active Job `Performing` and `Performed` logs are now available temporarily, but they do not show whether Sidekiq fetched a job before a worker disappeared. The dequeue log closes that gap. The two application logs explain the early exits that otherwise produce no model trace. The production root cause remains unresolved. This pull request adds evidence for the next occurrence and does not change Captain response behavior. ## Log volume A Captain V2 response job adds one dequeue line. The other two lines occur only on an early status skip or a pre-generation burst discard. Existing Active Job, Langfuse, completion, failure, handoff, and usage logs cover later stages. ## Validation * Ruby syntax checks passed for the four implementation files. * RuboCop found no offenses in the four implementation files. * No new specs were added because this is temporary diagnostic logging with no response behavior change.
This upgrades Chatwoot to Rails 7.2.3.1 while retaining the current Rails 7.0 framework defaults, so the runtime upgrade can be deployed and observed independently from default-behavior changes. ## What changed - Upgrade Rails and the compatible dependency set to Rails 7.2.3.1. - Keep `config.load_defaults 7.0` for a staged, lower-risk rollout. - Replace the unmaintained Azure Active Storage fork with the maintained `azure-blob` adapter while preserving the `microsoft` service name. - Pin Sidekiq 7.3.10 with `connection_pool` 2.x after validating scheduled-job execution against Redis. - Update Rails 7.2 compatibility surfaces in Active Record, strong parameters, migrations, storage, and tests. - Add read-only production preflight checks, an opt-in Active Storage smoke script, a deployment runbook, and the full Rails 7.2/8.0/8.1 assessment. ## How to test 1. Sign in and verify the dashboard and conversation UI load normally. 2. Open the agent-management modal and confirm agent data is rendered. 3. Create an API inbox and wait for the asynchronous deletion flow to complete. 4. Open Super Admin pages, including instance status and account-user management. 5. Upload and download an attachment using the configured Active Storage service. 6. Confirm recurring Sidekiq Cron jobs register and execute after startup. ## Rollout Follow `docs/rails_upgrades/7_2.md` for pre-deploy checks, deployment order, smoke tests, monitoring, and rollback. Run `bundle exec rails runner script/rails_upgrade/preflight.rb` against a production-equivalent environment before rollout. ## Tracking - [CW-5863 — Upgrade Rails to 8+](https://linear.app/chatwoot/issue/CW-5863/upgrade-rails-to-8) - [Rails 7.2 to 8.1 upgrade and production rollout plan](https://linear.app/chatwoot/document/chatwoot-rails-72-to-81-upgrade-and-production-rollout-plan-44e9f4964cb2) --------- Co-authored-by: Shivam Mishra <scm.mymail@gmail.com> Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> Co-authored-by: Sony Mathew <sony@chatwoot.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Improves the WhatsApp template listing so account admins can understand template state and refresh templates from one place. Templates that have not entered the WhatsApp approval flow now use clearer wording, the latest sync attempt appears once at page level, and templates can be filtered by content type. The page can now start a template sync across all active WhatsApp inboxes in the account and reports complete or partial failures. The New template action is removed for now; provider-side template management remains unchanged. ### Related Related: chatwoot#15312 ### How to test 1. Open **Settings → Templates** in an account with WhatsApp templates. 2. Confirm the latest sync attempt appears below the page description and is no longer repeated on every template. 3. Confirm an unsubmitted template displays **Not submitted for WhatsApp approval**. 4. Filter templates by content type and verify the list updates. 5. Click **Sync templates** and confirm sync starts for the account's active WhatsApp inboxes. 6. Confirm the **New template** button is not displayed. Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Captain V2 assistants can now persist a configurable inactivity timer and choose whether inactivity resolution sends the saved closing message or resolves silently. This PR contains only the API, persistence, runtime behavior, and backend specs. ## Closes [AI-163](https://linear.app/chatwoot/issue/AI-163) ## Depends on Stack 2 of 5. Based on the assistant-policy foundation in chatwoot#15299. The frontend follows in chatwoot#15308. ## What changed - Added per-assistant inactivity duration and resolution-message settings with safe defaults. - Restricted the Part 2 settings API to Captain V2 while keeping the Part 1 policy mode available without V2. - Updated inactivity handling to use the assistant timer and skip the public resolution message when disabled. - Serialized the effective timer and message settings for the frontend. - Added model, request, and job coverage, including the explicit Captain V2 boundary. ## How to test 1. Enable Captain V2 and update `auto_resolve_after` and `send_inactivity_resolution_message` through the assistant API. 2. Run the inactivity job and confirm it uses the assistant timer. 3. Disable the resolution message and confirm the conversation resolves silently. 4. Disable Captain V2 and confirm timer/message updates are ignored while `auto_resolve_mode` remains updateable. --------- Co-authored-by: Sony Mathew <sony@chatwoot.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com>
…twoot#15354) # Pull Request Template ## Description This PR reworks the canned response picker to make it easier to search, browse, and preview canned responses before inserting them. Typing `/` now opens a larger picker with its own search field and a preview pane. Previously, only a few responses were visible at a time, there was no way to preview the full content, and searching relied on typing into the composer, which stopped working for multi-word queries. Search is now handled entirely inside the picker, so the composer stays untouched while searching. Results match both the canned response shortcut and its content, and each result shows a snippet centered around the matched text instead of always displaying the beginning of the response. The preview renders the response exactly as it will be inserted, with variables resolved against the current conversation and formatting unsupported by the channel already stripped. The picker is positioned relative to the current typing line and teleported to `body`, so it is no longer clipped by the composer. It behaves consistently across the reply editor, the New Conversation composer, and narrower editors such as Contact Notes, where the preview moves below the list instead of disappearing. This also fixes a pre-existing bug where variables without a value were removed from the inserted text instead of being left for the backend to resolve. In the New Conversation composer, where no variables are available, all `{{ }}` placeholders were previously being silently removed. Fixes https://linear.app/chatwoot/issue/CW-7854/inconvenient-canned-response-picker-and-lack-of-personal-canned ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? ### Screenshots <img width="1135" height="576" alt="image" src="https://github.com/user-attachments/assets/b5f27b94-eeb5-4ac6-b6d9-da7dfd8c2306" /> <img width="393" height="490" alt="image" src="https://github.com/user-attachments/assets/779f88b1-4958-41f5-9f51-c2eb8db7e53c" /> ### Steps 1. Open a conversation and type `/` in the reply editor. 2. Search using a multi-word phrase that appears within a canned response. Verify the matching response appears with a snippet centered around the matched text. 3. Navigate the results with the arrow keys or Tab and verify the preview updates. 4. Press Enter or click a response to insert it, and press Escape to close the picker. 5. Repeat in a narrow editor such as Contact Notes and verify the preview pane moves below the list. 6. In the New Conversation composer, insert a canned response containing variables and verify the `{{ }}` placeholders are kept rather than removed. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
Improves SAML user handling for users associated with multiple accounts. Restricts cross-account invitations and skips provider updates for multi-account users. Aligns SAML authentication and provider reset behavior with these eligibility rules.
Co-authored-by: Sony Mathew <sony@chatwoot.com> Co-authored-by: Sojan Jose <sojan@pepalo.com>
Facebook inbox creation failures in Settings could leave users on the same screen without explaining what went wrong. This change preserves the backend error response and displays it through the existing toast mechanism, making account-limit and other creation failures visible and actionable. The shared action now passes the original API error through, so the newer onboarding flow also receives the specific backend message. Successful Facebook inbox creation remains unchanged. ### How to reproduce 1. Use an account that has reached its inbox limit. 2. Go to Settings → Inboxes → Add Inbox → Facebook. 3. Select a Facebook Page and create the inbox. 4. Previously, the loading state ended without any visible error. ### How to test 1. Attempt Facebook inbox creation while the backend returns an HTTP 402 account-limit error. 2. Confirm the returned error appears as a toast message. 3. Retry after increasing the account limit and confirm inbox creation continues normally. ### Things to know The toast falls back to a generic localized creation error when the API response does not contain a displayable message. Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
…ntact's email (chatwoot#15373) Automations that send an email transcript now let you insert the conversation contact's email directly into the recipient field, instead of only accepting a hardcoded address typed in by hand. ## Closes https://linear.app/chatwoot/issue/CW-7535/allow-automation-to-send-conversation-transcripts-to-user-emails ## How to test 1. Go to **Settings → Automation → Create Automation** (or edit an existing rule). 2. Add the **Send an Email Transcript** action. 3. Click **Use contact's email** next to the recipient field — it inserts `{{contact.email}}` into the field, appending to any address already typed rather than replacing it. 4. Save the rule and trigger it on a conversation with a contact that has an email address; confirm the transcript is delivered to that contact. ## What changed - `AutomationActionInput.vue`: added a "Use contact's email" quick-insert action next to the recipient field for the `send_email_transcript` action, and switched the input from `type="email"` to `type="text"` so the dynamic token isn't fought by native email-format validation. - No backend changes were needed — `send_email_transcript` already resolves `{{contact.email}}` via the existing Liquid templating support in `ActionService#send_email_transcript`; this change only exposes that capability in the UI. - This also benefits the Macros editor, which shares the same input component. --------- Co-authored-by: iamsivin <iamsivin@gmail.com>
## Description Captain Copilot's `get_conversation` tool returned any conversation in the account, without checking whether the agent asking for it could open that conversation from the inbox view. An agent who belongs to a single inbox, or who holds a narrow custom role, could therefore read the message history of conversations outside their access, including the private notes on them. The tool now runs the same permission filter that the conversation list endpoint and Copilot's own `search_conversation` tool already use, so it returns only the conversations the agent can already open. Administrators see no change, because the filter returns the whole account scope for them. The Copilot chat service had the same gap. When an agent opened Copilot while viewing a conversation, the service looked that conversation up by account alone and wrote its ID and contact ID into the system prompt. It now resolves the conversation through the same filter, and leaves the context out when the agent cannot access it. ## Closes https://linear.app/chatwoot/issue/CW-7768 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How to reproduce 1. Create two inboxes in one account, for example Inbox A and Inbox B. 2. Add an agent to Inbox A only, then start a conversation in Inbox B and leave a private note on it. 3. Sign in as that agent, open Copilot, and ask it for the Inbox B conversation by its ID. 4. Before the change Copilot returns the full message history including the private note. After the change it reports that the conversation was not found. Opening the same conversation from the inbox view as that agent is rejected both before and after the change, so the inbox view and Copilot now agree on what the agent can read. ## Checklist - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
…nts [CW-7792] (chatwoot#15316) Records Captain conversation outcomes at episode grain so reporting can distinguish initial demand from reopened conversations and measure replies, handoffs, resolutions, human follow-up, and CSAT. Eligibility creates the episode at demand time. Message-derived fields are snapshotted from persisted messages at handoff or resolution, keeping terminal analytics accurate without writing outcomes for every message. Outcome tracking remains reporting-only and fail-open. Builds on the episode-grain schema from chatwoot#15315. ## Closes - https://linear.app/chatwoot/issue/CW-7792 ## How to test 1. Enable `captain_integration_v2` and connect a Captain assistant to an inbox. 2. Send an inbound customer message and confirm an initial outcome episode is created at the message timestamp. 3. Let Captain reply and then resolve or hand off the conversation. Confirm the episode records Captain reply counts and timestamps, the outcome timestamp, and the handoff category where applicable. 4. Reply after resolution and confirm a `reopen` episode is created while preserving the previous episode. 5. Resolve the reopened conversation and submit CSAT. Confirm the response is attributed to the episode that issued the survey. ## What changed - Creates the initial episode from demand-level eligibility and appends a new episode when a resolved conversation reopens. - Snapshots Captain replies and the first qualifying human reply from persisted messages at handoff and resolution. - Attributes asynchronous resolution events using the episode active at the event timestamp. - Records later CSAT responses using the survey message timestamp. - Keeps boundary writes transactional and fail-open without retries, advisory locks, late-boundary repair, or handoff self-healing. - Adds schema-constrained handoff reason categories, including lifecycle coverage for incomplete V2 tool fallback handoffs. Open, non-terminal episodes may retain empty or stale message-derived fields until handoff or resolution.
MFA-authenticated users now remain signed in across browser restarts, matching the session lifetime of password-authenticated users. ## Closes - [CW-7898](https://linear.app/chatwoot/issue/CW-7898/mfa-users-are-logged-out-when-the-browser-session-ends) ## How to reproduce 1. Enable MFA for a user. 2. Sign in and complete OTP or backup-code verification. 3. End the browser session, then reopen the browser and return to Chatwoot. 4. Before this change, the user is sent back to the login page. ## Root cause and evidence Before this change, successful MFA verification wrote `cw_d_session_info` directly through `document.cookie` without an `Expires` or `Max-Age` attribute. This makes it a session cookie: - [MFA cookie creation on the base commit](https://github.com/chatwoot/chatwoot/blob/a4eae9710ab49e7dc98d64a7e713fd1f77c451f8/app/javascript/dashboard/components/auth/MfaVerification.vue#L70-L87) - [MDN session-cookie behavior](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie) - [Chromium cookie persistence documentation](https://chromium.googlesource.com/chromium/src/+/master/net/cookies/README.md) Password login already uses `setAuthCredentials`, which derives a persistent cookie expiry from the authentication response: - [Existing credential helper](https://github.com/chatwoot/chatwoot/blob/a4eae9710ab49e7dc98d64a7e713fd1f77c451f8/app/javascript/dashboard/store/utils/api.js#L28-L36) - [Configured two-month token lifetime](https://github.com/chatwoot/chatwoot/blob/a4eae9710ab49e7dc98d64a7e713fd1f77c451f8/config/initializers/devise_token_auth.rb#L8-L10) This change routes successful MFA verification through the same credential helper and existing verified event. The regression test asserts that MFA credentials use this shared persistence path. Session-cookie restoration is browser and profile dependent. Chromium may persist session cookies when restoring a previous browsing session, so the problem can be masked on some restarts. This PR does not rely on a desktop-versus-mobile distinction. ## How to verify 1. On `develop`, complete an MFA login. 2. Inspect `cw_d_session_info` in browser developer tools. Its expiry is shown as `Session`. 3. Repeat on this branch. 4. Confirm that `cw_d_session_info` has a concrete expiry derived from the authentication response instead of `Session`.
## Description This systemd unit is installed on every self-hosted Linux install (copied by `deployment/setup_18.04.sh` / `setup_20.04.sh`) as well as our own worker hosts, so the memory cap has to be correct across a wide range of box sizes. It is currently a fixed `MemoryMax=1.2G`, chosen when workers ran on 2GB hosts (chatwoot#12915). On larger hosts that fixed cap sits at a high fraction of the limit under normal load, so transient spikes hit the hard limit and the worker is OOM-restarted more often than necessary (each restart drops the jobs in-flight at that instant). Switching to `MemoryMax=60%` makes the cap scale with the host's physical memory instead of being fixed: - 2GB host: 60% = 1.2G, identical to today (no regression for small installs) - 4GB host: 60% = ~2.4G, room to absorb spikes without a premature restart - larger hosts scale proportionally `MemoryHigh=infinity` is unchanged, so the worker is still never throttled by cgroup reclaim (kept intentionally since chatwoot#12871 to avoid reclaim-induced worker stalls). Only the hard cap changes, and only its scaling. Fixes https://linear.app/chatwoot/issue/INF-100 ## Type of change - [x] Performance (non-breaking change which improves performance)
…t#15808) Fixes advanced-filter totals reverting to open-conversation counts when requests finish out of order. Count updates now follow request order within the active view, preserving newer event refreshes and allowing useful metadata updates when a list refresh fails. Adds regression coverage for request ordering, failures, pagination, and cached assignee-tab switches. --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
chatwoot#15826) Temporarily hide the WhatsApp Request Contact Info action in conversation responses to remove message-history lookups from routine inbox browsing. Conversation payloads keep the existing fields but return `available: false`, `reason: null`, and `delivery_mode: null`. This is an alternative to chatwoot#15825. Instead of displaying an action that can later fail because of a pending request in older history, this mitigation hides the dedicated action entirely while a cheaper pending-state approach is considered. ## What changed - Return disabled contact-info availability from the shared conversation partial without invoking eligibility checks. - Disconnect the batch lookup from list rendering while retaining the service code for re-enabling the feature. - Preserve validation for direct API requests and manually selected contact-info templates. This disables the dedicated UI action, not the backend capability. ## How to reproduce Open a WhatsApp Cloud conversation for a BSUID contact without a phone number. Before this change, conversation rendering checks message history to determine whether to show or disable Request Contact Info. After this change, the dedicated action is hidden and rendering no longer runs that lookup. Check conversation lists, filtered lists, contact conversation lists, and individual conversation responses. Each should return the disabled contact-info payload.
chatwoot#15827) The Contacts page could fail to load on larger accounts. When a user has never picked a sort, the list asked for contacts ordered by **oldest** activity first. No index can serve that order, so every page load read every contact in the account just to return 15. When those rows aren't cached, or the database disk is under load, that runs past the 14s statement timeout and the page never loads. The page now defaults to **most recent** activity first. The existing `(account_id, last_activity_at DESC NULLS LAST)` index serves that order, so a page load reads about 20 pages whatever the account's size. Users who picked a sort keep it. ## Closes - No existing issue. Found via recurring `PG::QueryCanceled` / `Rack::Timeout` errors on `contacts#index`. ## How to reproduce 1. On a large account, open **Contacts** as a user who has never changed the sort. 2. The page sends `GET /api/v1/accounts/:id/contacts?sort=last_activity_at`, which becomes `ORDER BY last_activity_at ASC NULLS LAST LIMIT 15`. 3. Postgres fetches and sorts every listed contact of the account. When those pages come from disk, the request hits the statement timeout. ## What changed - `ContactsIndex.vue`: when no `contacts_sort_by` UI setting is saved, the default sort is now `-last_activity_at` instead of `last_activity_at`. - Measured on production for a ~57k-contact account: - old default: 57,768 pages read per load (56 ms cached, about 24s from disk at the measured ~0.42 ms per page); - new default: an index scan that stops after 15 rows. - Contact search uses the same plan for either sort direction (trigram bitmap scan, then a sort of the matches), so it is unaffected. - Known trade-off: on accounts made up mostly of anonymous visitors, the new order walks more of the activity index before finding 15 listed contacts. A partial index restricted to listed contacts removes that cost if it shows up. Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
…ot#15752) # Pull Request Template ## Description Sign-in and sign-out write one audit row per account the user belongs to using `insert_all!`, which bypasses the `after_create_commit` that enqueues the IP geolocation lookup added in chatwoot#15455. Session audits were the only rows that never got a city or country. Restoring the callback would undo chatwoot#15572, which took a 500-account sign-in from 2016 queries to 519. Since every row from one sign-in shares a `remote_address` and `request_uuid`, this enqueues a single job per sign-in that resolves the address once and applies it to that request's rows in one `update_all`, scoped to accounts with `ip_lookup` enabled. Constant cost regardless of membership count. The second commit fixes a defect found while testing. `audit_event_rows` used `::Audited.store[:current_request_uuid] || SecureRandom.uuid`. Rails strips an inbound `X-Request-Id` to an empty string when it holds only punctuation, and `""` is truthy, so those rows were written with a blank uuid and the new job would skip the batch. Now uses `.presence`. Fixes https://linear.app/chatwoot/issue/CW-8135 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? New job spec: one lookup applied to every row of a request, isolation from other requests, a batch spanning eligible and ineligible accounts, no lookup for ineligible or empty scopes, error swallowing. Session controller spec: exactly one job enqueued for a user in four accounts, and rows still share a generated uuid when the request id sanitises to blank. Verified that spec fails without the `.presence` fix. 27 examples, 0 failures locally. Rubocop clean. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules
## Description Agents can now use the existing conversation sort menu while viewing conversations narrowed by advanced filters or saved folders. Changing the order refetches the filtered result set before pagination, so oldest/newest, activity, unread, priority, and waiting-time ordering remain consistent across loaded pages. ## Closes - [CW-7862](https://linear.app/chatwoot/issue/CW-7862/lack-of-sorting-options-when-using-filters-in-conversations-tab) ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [x] This change requires a documentation update ## What changed - Keep the existing order menu available for ad-hoc filtered and saved-folder conversation lists while hiding the conflicting status selector. - Share the server-side sort whitelist between standard and filtered conversation queries, preserving existing defaults and legacy aliases. - Send the selected sort through filtered fetches, pagination, and reconnect refreshes, with client-side ordering aligned to the server. - Document the optional `sort_by` parameter for the conversation filter API. ## How to test 1. Open Conversations and apply an advanced filter. 2. Open the sort menu and confirm the existing order options are available while the status selector is hidden. 3. Change between oldest/newest, unread, priority, and waiting-time options and confirm the filtered list refetches in the selected order. 4. Open a saved conversation folder and confirm the same sorting behavior, including after loading another page. ## Checklist - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Incoming WhatsApp replies through Twilio lose their quoted-message context, leaving agents unable to tell which earlier message the customer means. This change displays the referenced message when it exists in the same conversation. The Twilio callback now accepts `OriginalRepliedMessageSid` and passes it to the existing message reply resolver as `in_reply_to_external_id`. ### Things to know - Applies to incoming quoted replies. Sending quoted replies through Twilio remains unchanged. - Uses the existing same-conversation lookup; references to unavailable messages retain the existing unresolved-reply behavior. - Previously received messages are not backfilled. ### How to reproduce 1. In a Twilio WhatsApp conversation, reply to an earlier message using WhatsApp's reply action. 2. Before this fix, Chatwoot displays the reply without the quoted message even when Twilio supplies its SID. ### How to test 1. Reply to a recent message in an existing Twilio WhatsApp conversation. 2. Confirm the incoming reply displays the original message in its quote preview. 3. Click the quote preview to navigate to the original message. Locally verified by posting a mock Twilio webhook to the callback endpoint: the incoming message persisted both reply identifiers and displayed the original message in the conversation UI. Regression coverage checks that the controller forwards the quoted SID and the incoming-message service persists both reply identifiers. The focused Twilio suite passed with 52 examples and no failures; RuboCop and diff checks also passed. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
… is not in the store (chatwoot#15795) Answering an incoming WhatsApp call from the call bubble in the conversation could show "Internal Server Error", and the caller then saw the call as missed. This happened when the call wasn't in the agent's calls store: it rang while the agent wasn't online, or it was dismissed earlier. In that case the dashboard fell back to the Twilio flow and requested a Twilio token for the WhatsApp inbox. ## Closes - https://linear.app/chatwoot/issue/CW-8174 ## How to reproduce 1. Set your availability to Busy or Offline and receive a WhatsApp call on an inbox with calling enabled. 2. Open the conversation and click Answer on the ringing call bubble. 3. Before: "Internal Server Error" and the call ends as missed. After: the call connects. ## What changed - The call bubble adds the call to the calls store from its own message data before joining, so the WhatsApp answer path is used. - `ConferenceController` only resolves Twilio inboxes, so the Twilio token and conference endpoints return 404 for other inboxes instead of 500.
# Pull Request Template ## Description This PR fixes sidebar navigation items not behaving like real links. Middle-click, Cmd/Ctrl-click, and the browser’s “Open link in new tab” action now work as expected. Fixes https://linear.app/chatwoot/issue/CW-7868/impossibility-to-open-navigation-menu-items-in-a-new-window-or-new-tab ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ### Screenshots **Before** <img width="369" height="387" alt="image" src="https://github.com/user-attachments/assets/280b540a-1c4c-4f7c-b459-17ea8823fd67" /> **After** <img width="369" height="387" alt="image" src="https://github.com/user-attachments/assets/87a02578-7064-41ae-abd9-2f6bd4cb5a5e" /> ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
## Description Restore the dependency-audit CI check by adding a documented exception for RubyLLM's CVE-2026-67991 on Chatwoot's supported runtime. The change adds only this advisory to `.bundler-audit.yml`, with the applicability rationale and removal condition. RubyLLM remains at 1.15.0 and every other advisory remains subject to the existing audit configuration. ### Advisory verification and applicability [CVE-2026-67991 / GHSA-42r3-x6vx-x49x](https://github.com/rubysec/ruby-advisory-db/blob/44784c295391577f25d198a9205eae4ba73ec4da/gems/ruby_llm/CVE-2026-67991.yml) describes polynomial-time name normalization on Ruby 3.1.x when a very long crafted class, agent, or tool name reaches the affected operation. The released 1.15.0 gem contains the regex inline in `RubyLLM::Tool#name`; the advisory's later `Utils.underscore` reference does not mean released gems were unaffected. The exception is based on the application's runtime and actual input paths: - **Runtime:** the Gemfile, `.ruby-version`, CircleCI and Docker images specify Ruby 3.4.4. A bounded local probe of the installed `Tool#name` method reproduced quadratic behavior on Ruby 3.1.0, but linear behavior on Ruby 3.4.4. The latter also reports this regex as linear-time, consistent with [Ruby's regex memoization introduced in 3.2](https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/). - **Built-in tools:** names come from fixed application classes, rather than user-generated class names. - **Custom tools:** [slugs are limited to 64 characters](https://github.com/chatwoot/chatwoot/blob/e16d8f2fa4bd00c65747de0b8363f0e05092e36c/enterprise/app/models/captain/custom_tool.rb#L38-L65). [Toolable overrides the name method](https://github.com/chatwoot/chatwoot/blob/e16d8f2fa4bd00c65747de0b8363f0e05092e36c/enterprise/app/models/concerns/toolable.rb#L8-L25) to return that slug directly, bypassing RubyLLM's normalizer. - **Agent handoffs:** Chatwoot uses `Agents::Agent` from ai-agents; its handoff tools override naming with a separate lowercase/filter conversion. Scenario handoff names also have a length budget. Ordinary prompt/message text does not automatically enter the affected normalization operation. Local timings for synthetic all-capital class names: | Characters | Ruby 3.1.0 | Ruby 3.4.4 | | --- | ---: | ---: | | 5,000 | 104 ms | 0.85 ms | | 10,000 | 430 ms | 1.72 ms | | 20,000 | 1,676 ms | 3.31 ms | These measurements characterize the reported regex behavior, not production capacity. Ruby 3.1.0 was used only for the isolated method probe; Chatwoot itself was not run on that unsupported version. Live deployment runtimes were not remotely inspected. ### Why this PR does not upgrade RubyLLM As of 16 September 2026, stable 1.16.0 still contains the pattern and still matches the advisory. The [upstream fix](crmne/ruby_llm@9d75b03) is released starting with 2.0.0.rc1; the latest release is 2.0.0.rc4, also a prerelease. `ai-agents 0.12.0` requires `ruby_llm ~> 1.14`. An isolated Bundler resolution rejects 2.0.0.rc4. Bypassing the constraint for a compatibility probe makes `require 'agents'` fail on the removed `param` method. Chatwoot [requires agents unconditionally during initialization](https://github.com/chatwoot/chatwoot/blob/e16d8f2fa4bd00c65747de0b8363f0e05092e36c/config/initializers/ai_agents.rb#L1-L3), so simply widening that constraint can prevent application startup, including when Captain is unused. The [2.0 migration](https://github.com/crmne/ruby_llm/blob/v2.0.0.rc4/docs/_reference/upgrading.md) also affects existing Chatwoot behavior: | Surface | Required migration | | --- | --- | | Captain V2 / scenario handoffs | Replace removed `Tool::Halt`, update instruction/tool replacement APIs, and preserve runner state and handoff control. | | Built-in/custom tools and Copilot | Update `param`/`params`, parameter-schema readers, tool registration, provider options and callbacks. | | Rewrites, replies, summaries, labels and other Captain tasks | Replace direct token readers; current response conversion can otherwise fail after a successful provider response. | | Images and conversation replay | Replace `RubyLLM::Content` with the new attachment/message API. | | Structured generation | Read parsed output through `response.parsed`; current Hash consumers can otherwise produce empty overview points, blank article fields or raw JSON taglines. | | OpenAI-compatible endpoints | Make protocol selection explicit: 2.0 defaults to `/responses` instead of `/chat/completions`. Existing `response_format` options and custom endpoints need adaptation or an explicit Chat Completions protocol. | | Request behavior / observability | Validate temperature settings, token/cost tracing and the renamed model-registry refresh method. | The isolated request probe confirmed the protocol change and that a GPT-5.2 request configured with temperature 0.5 sends 1.0 under 1.15 and 0.5 under rc4. No live provider requests were made. There is no `acts_as_chat`/`acts_as_message` persistence integration in Chatwoot, so the upstream Rails data migration does not automatically apply. Embedding/moderation readers used here still exist, and legacy speech/PDF operations use the separate OpenAI client. RubyLLM remains MIT licensed. A supported upgrade should be coordinated with ai-agents instead of being folded into this audit exception. ### Removal condition Remove the exception when Chatwoot adopts a supported patched RubyLLM/ai-agents combination, including a recognized compatible 1.x backport if one becomes available. Reassess it if the supported Ruby runtime or tool-name construction changes. This records advisory applicability; it does not claim that RubyLLM 1.15.0 includes the upstream fix. The original audit failure was reproduced against ruby-advisory-db `44784c295391577f25d198a9205eae4ba73ec4da`. The actual changed configuration now passes the refreshed audit, YAML formatting and whitespace checks. The prior investigation passed 528 existing focused specs on this application revision; after adding the exception, all 131 selected Captain base-task, custom-tool and scenario specs passed again. Existing Rails enum deprecation warnings remain unchanged. No new specs were added for the configuration-only change. ## Closes No linked ticket. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How to reproduce 1. On the parent commit, run the CircleCI dependency-audit check with an updated ruby-advisory-db. It reports RubyLLM 1.15.0 / CVE-2026-67991 and exits unsuccessfully. 2. Run the same check on this branch. It succeeds with the documented exception; the gem versions and application behavior remain unchanged. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation (inline exception rationale and removal condition) - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works (not applicable: audit configuration only) - [x] New and existing unit tests pass locally with my changes (131 focused existing examples) - [ ] Any dependent changes have been merged and published in downstream modules (not applicable: no dependent changes)
Add reconnect attempts configuration for Redis. ## Description This PR fixes an issue where Chatwoot stops delivering websocket updates after Redis is restarted. For the past few months, `needrestart` has been silently restarting services that depend on upgraded libraries during system updates, including Redis. This is the case for me, and maybe it will be for other people as well. Issue is When Redis restarts, Action Cable loses its Redis subscription connection. The root cause is that the Redis client used by Action Cable only performs a limited reconnect attempt. If Redis is unavailable during that window, the subscription remains disconnected permanently. As a result, websocket connections stay open and continue sending pings, but no real-time updates are delivered to clients. This change configures Redis subscription reconnection attempts with exponential backoff, allowing Action Cable to recover automatically from temporary Redis outages and service restarts. Closes chatwoot#14705 ## Type of change * [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ### Reproduction Steps 1. Deploy Chatwoot v4.8.0. 2. Open the Chatwoot admin panel. 3. Verify that real-time updates are working. 4. Restart Redis: ```sh sudo systemctl restart redis-server ``` 5. Send a new message or trigger a real-time event. ### Before this change * Websocket connections remain open. * Only ping frames are exchanged. * No real-time updates are delivered. * Chatwoot requires a restart to restore functionality. ### After this change * Action Cable automatically reconnects to Redis. * Real-time updates continue working after Redis becomes available. * No Chatwoot restart is required. ## Checklist: * [x] My code follows the style guidelines of this project * [x] I have performed a self-review of my code * [x] I have commented on my code, particularly in hard-to-understand areas * [x] I have made corresponding changes to the documentation * [x] My changes generate no new warnings * [x] I have added tests that prove my fix is effective or that my feature works * [x] New and existing unit tests pass locally with my changes * [x] Any dependent changes have been merged and published in downstream modules ## Environment | Component | Version | |---|---| | **OS** | Ubuntu (Noble/24.04) | | **Ruby** | 3.4.4 | | **Rails** | 7.1.5.2 | | **ActionCable** | 7.1.5.2 | | **redis** (gem) | 5.0.6 | | **redis-client** (gem) | 0.22.2 | | **Redis Server** | 8.0.5 | --------- Co-authored-by: tejush <mac@MacBook-Air.local>
…a_id (chatwoot#15702) WhatsApp Cloud attachments are now uploaded to Meta's media endpoint and sent by `media_id` instead of a public `link`. Meta no longer downloads the file from the Chatwoot server, so outbound media stops failing intermittently with `131053: Media upload error` on hosts that share an ASN with other Chatwoot/Evolution API instances — Meta's `fwdproxy` rate limits per destination ASN, not per WABA account. It also removes the secondary failure where `fwdproxy` does not follow ActiveStorage's `302` redirect and reports `Unsupported mime type text/html`. Operators who need the previous behaviour can set `WHATSAPP_MEDIA_UPLOAD_STRATEGY=link`. No migrations, no config required on upgrade. Supersedes chatwoot#15069 by @AdarshJ173, which established this approach and the `media_id` direction. The review points raised by @nestordavalos on that PR are addressed here. ## Closes - Closes chatwoot#13540 - Supersedes chatwoot#15069 ## How to reproduce On a WhatsApp Cloud inbox, send an image, document or voice note from an instance whose hosting provider shares an ASN with other Chatwoot deployments (e.g. Hostinger, ASN 47583). Meta's webhook returns: ``` Downloading media from weblink failed with http code 429 ratelimit reason: Request ratelimit by fwdproxy [GlobalCountingRatelimiter] Request hit ratelimit policy — destination matcher asn_list: 47583 ``` After this change the file is uploaded first and the message references the returned `media_id`, so Meta never fetches it from us. ## What changed - **New `Whatsapp::MediaUploadService`** — streams the blob to `POST /{version}/{phone_number_id}/media` as multipart form data and returns `{ 'id' => media_id }`, or `nil` when the upload is disabled or fails. - **`WhatsappCloudService#build_attachment_content`** sends the returned media object, falling back to `{ 'link' => download_url }` so a media-endpoint failure never blocks a message. - Uses `Faraday::Multipart::FilePart`, matching `Telegram::SendAttachmentsService`, the codebase's existing multipart upload. The file part carries an explicit filename and content type. - Reads the blob through `blob.open` and streams it via Faraday's `CompositeReadIO`, so large attachments are never buffered in memory and any ActiveStorage backend works. - Follows `WHATSAPP_API_VERSION` via `GlobalConfigService`, matching `calls_phone_id_path` in the enterprise provider, rather than pinning a version in source. - Falls back to the link on `Faraday::Error` or a non-success response only; other errors surface instead of silently degrading. - Logs one warning naming the fallback and Meta's own error, including for non-JSON error pages: `[WHATSAPP] Media upload failed, falling back to link for account … attachment …: HTTP 429 Request ratelimit by fwdproxy`. The `voice`/`caption`/`filename` fields and the existing `audio/opus` → `audio/ogg` normalisation are unchanged; the normalisation now also governs the uploaded part's content type. --------- Co-authored-by: A.Adarsh Jagannath <adarshjagannath777@gmail.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
## Problem Widget file uploads via ActiveStorage DirectUpload fail on production with: ```text ActionController::InvalidAuthenticityToken (Can't verify CSRF token authenticity.) ``` This occurs because `Api::V1::Widget::DirectUploadsController` inherits from `ActiveStorage::DirectUploadsController` → `ActiveStorage::BaseController`, which has protect_from_forgery with: :exception enabled. While CSRF works on same-origin, it fails in production because the widget is embedded in an iframe on third-party customer sites (cross-origin) ## Solution Skip CSRF verification for the widget direct uploads endpoint - this is safe because: - Widget endpoints authenticate via website_token + X-Auth-Token (token-based auth) - CSRF protection is designed for session-based auth, not token-based auth --------- Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
…hatwoot#14050) Summary This PR fixes XSS vulnerabilities by sanitizing unsafe HTML rendering paths: Replaced unsafe [v-html](vscode-file://vscode-app/c:/Users/gurji/AppData/Local/Programs/Microsoft%20VS%20Code/e7fb5e96c0/resources/app/out/vs/code/electron-browser/workbench/workbench.html)/[innerHTML](vscode-file://vscode-app/c:/Users/gurji/AppData/Local/Programs/Microsoft%20VS%20Code/e7fb5e96c0/resources/app/out/vs/code/electron-browser/workbench/workbench.html) rendering with [v-dompurify-html](vscode-file://vscode-app/c:/Users/gurji/AppData/Local/Programs/Microsoft%20VS%20Code/e7fb5e96c0/resources/app/out/vs/code/electron-browser/workbench/workbench.html) in captain assistant UI components Sanitized signup terms HTML before rendering in the signup form Added server-side markdown sanitization in [chatwoot_markdown_renderer.rb](vscode-file://vscode-app/c:/Users/gurji/AppData/Local/Programs/Microsoft%20VS%20Code/e7fb5e96c0/resources/app/out/vs/code/electron-browser/workbench/workbench.html) before marking HTML safe Added regression coverage for markdown XSS sanitization. Changed files [MessageList.vue](vscode-file://vscode-app/c:/Users/gurji/AppData/Local/Programs/Microsoft%20VS%20Code/e7fb5e96c0/resources/app/out/vs/code/electron-browser/workbench/workbench.html) [ScenariosCard.vue](vscode-file://vscode-app/c:/Users/gurji/AppData/Local/Programs/Microsoft%20VS%20Code/e7fb5e96c0/resources/app/out/vs/code/electron-browser/workbench/workbench.html) [Index.vue](vscode-file://vscode-app/c:/Users/gurji/AppData/Local/Programs/Microsoft%20VS%20Code/e7fb5e96c0/resources/app/out/vs/code/electron-browser/workbench/workbench.html) [Form.vue](vscode-file://vscode-app/c:/Users/gurji/AppData/Local/Programs/Microsoft%20VS%20Code/e7fb5e96c0/resources/app/out/vs/code/electron-browser/workbench/workbench.html) [chatwoot_markdown_renderer.rb](vscode-file://vscode-app/c:/Users/gurji/AppData/Local/Programs/Microsoft%20VS%20Code/e7fb5e96c0/resources/app/out/vs/code/electron-browser/workbench/workbench.html) [chatwoot_markdown_renderer_spec.rb](vscode-file://vscode-app/c:/Users/gurji/AppData/Local/Programs/Microsoft%20VS%20Code/e7fb5e96c0/resources/app/out/vs/code/electron-browser/workbench/workbench.html) Testing bundle exec rspec spec/lib/chatwoot_markdown_renderer_spec.rb Optional frontend validation by building the dashboard and confirming the updated components render safely without unsafe HTML injection paths. --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Sony Mathew <sony@chatwoot.com> Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
…15062) Ruby 3.4 with YJIT enabled inlines constant values at compile time, so `stub_const` in tests can't replace a class-level constant that YJIT has already cached — the spec calls still see the original value and the session-limit tests return 200 instead of 409. ## What changed - Removed the `MAX_SESSIONS` class constant from `DeviseOverrides::SessionsController` - `sessions_limit_reached?` now reads `ENV.fetch('MAX_USER_SESSIONS', 25).to_i` directly on each call (with a guard for `<= 0`) - Updated the spec to use `with_modified_env` instead of `stub_const` so the limit is configurable at test time without hitting the YJIT cache ## How to test Run the session limit enforcement specs: ``` bundle exec rspec spec/controllers/devise_overrides/sessions_controller_spec.rb ``` All tests should pass, including on Ruby 3.4 with YJIT enabled. --------- Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
Fixes chatwoot#15797 - credit to @Uankur for the precise report and diagnosis (exact citations, repro curls, and the proposed direction); this PR implements the fix they sketched, verified independently against current `develop`. @Uankur - would love your eyes on this. ## Problem `GET /api/v1/accounts/:account_id/portals/:slug` returns HTTP 500 when the slug does not exist in the account. `fetch_portal` uses `find_by(slug:)`, which returns nil instead of raising, and `show` then calls `@portal.articles` on nil. The blast radius is wider than `show`: `fetch_portal` runs as a `before_action` for every portal action except `index`/`create`, so `update`, `destroy`, `archive`, `logo` and `send_instructions` hit the same nil crash (EE `ssl_status` too). ## Fix One-token change: `find_by` -> `find_by!`. `ActiveRecord::RecordNotFound` then flows through the existing `handle_with_exception` rescue, which renders the standard 404 `"Resource could not be found"` - the same pattern sibling controllers already use (`labels_controller` uses `find`, and the public portals controllers use `find_by!(slug:)`). The lookup stays scoped to `Current.account.portals`, so a slug belonging to another account also returns 404, matching the reporter's expectation. Pundit authorization is class-level here and unchanged. ## Tests Two request specs added to `portals_controller_spec.rb`: unknown slug -> 404 with the standard body, and cross-account slug -> 404. **Test status (honest):** the specs are written but NOT executed - no Ruby toolchain was available in our environment. Verification was static: the nil path, the `handle_with_exception` RecordNotFound -> 404 rescue, and the sibling/public-controller `find_by!` precedent were all confirmed against current `develop` (`2f1ed80f89`). The one-token change mirrors the repo's own usage elsewhere. > Built by breken, your AI support engineer - breken.ai - this one's on us.
## Description This fixes Facebook Messenger postback events not being received by Chatwoot inboxes. Previously, Chatwoot subscribed Facebook pages without the `messaging_postbacks` webhook field, so actions like `Get Started` and button postbacks were not delivered. This change adds the missing subscription field, registers a Messenger `:postback` handler, and maps postback payloads into the existing Facebook inbound message flow. Fixes chatwoot#8821 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? Manual reproduction steps: 1. Create or connect a Facebook Page inbox in Chatwoot. 2. Configure a Messenger `Get Started` button or a button that sends a postback. 3. Trigger the postback from Messenger. 4. Confirm the event is received and created through the existing Facebook inbound message pipeline. Automated coverage added: - Added a model spec to verify `messaging_postbacks` is included in Facebook page webhook subscriptions. - Added a builder spec to verify a Messenger postback creates an incoming message using the postback title. Note: - I could not run the full local Ruby test suite in the current environment because Ruby/Bundler was not available in the active shell. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Include ET translation files Co-authored-by: Sojan Jose <sojan@pepalo.com>
* fix: authorize conversations before macro execution * fix: log skipped macro executions and cover authorization
* fix: run pre-authentication checks on header-supplied sign-in credentials * fix: normalize email in SAML password-auth guard
## Description Prevent non-Cloud login and application pages, Super Admin, and installation setup pages from being indexed. Cloud application pages remain eligible for indexing. Widget pages always include `noindex`, and their existing crawl exclusion is preserved to avoid creating contacts from crawler requests. Public Help Center homepages and articles retain their existing indexing behavior, including when a widget is embedded. A public Rails route replaces the static robots.txt file and retains `Disallow: /widget`. The application layout uses the existing Cloud detection helper to apply `noindex` conditionally; widget, Super Admin, and setup pages include it on every installation. Public Help Center layouts remain unchanged. Because widget crawling remains blocked, crawlers cannot reliably read its `noindex` tag to remove already-indexed widget URLs. Allowing widget crawling safely requires a separate change to make widget GET requests create no records. ## Closes Closes https://linear.app/chatwoot/issue/CW-8224/prevent-login-page-indexing-on-non-cloud-installations ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? 1. On a self-hosted installation, open the page source for `/app/login`, `/app/login/sso`, `/app`, `/`, and `/widget?website_token=<valid-token>`. The HTML head includes `<meta name="robots" content="noindex">`, including when the manifest is disabled. 2. Set the installation's `DEPLOYMENT_ENV` to `cloud` with Enterprise enabled and reload. The application pages no longer include the directive, while the widget still includes it. Community installations retain the directive on application and widget pages. 3. Open `/super_admin/sign_in`, sign in, and inspect `/super_admin` and `/super_admin/accounts`. All include `noindex` on Cloud, self-hosted Enterprise, and Community installations. 4. On a fresh installation, open `/` and follow the redirect to `/installation/onboarding`. The setup page includes `noindex`. 5. Open a public Help Center homepage, article, and custom-domain homepage using either portal layout, including with a widget attached. None receives the directive. Plain article previews remain unaffected. 6. Open `/robots.txt` without signing in. It returns plain text with `User-agent: *` and `Disallow: /widget`, including in API-only deployments. Other paths remain crawlable. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
|
🚧 Skipped: PR exceeds review size limit. Please split into smaller PRs and re-run. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
🚧 Skipped: PR exceeds review size limit. Please split into smaller PRs and re-run. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings our fork up from Chatwoot v4.13.0 to upstream v4.18.0 (793 upstream commits).
sdk.js.pre-4.18-backup(1a21383bef) for rollback.Deploy notes
db:migrate.assets:precompile).🤖 Generated with Claude Code