Move the webhook SSRF guard into a shared internal/ssrf package and wire it
into every place the server fetches an admin-set URL: webhooks, OIDC discovery,
the AI provider base URL, and custom AI tool calls. Add a global [ssrf] config
block, off by default with an allowed_cidrs bypass, so single-tenant self-hosters
keep reaching internal hosts while multi-tenant or hosted deploys can turn it on.
The old [webhook] allowed_hosts key is still read for backward compat and folds
into the guard.
Fix a reindex race: snippet embedding runs outside the lock, so a slower
job from an older edit could commit stale vectors after a newer edit.
Split Reindex into embed (lock-free) and commit (locked), and gate the
commit with a per-snippet generation counter so only the latest edit
wins. Delete drops the counter so an in-flight job can't re-insert
vectors for a deleted snippet.
Clear the DB avatar reference before deleting the avatar media file. The
old order deleted the file first, so a failed DB update left the DB
pointing at a missing file and a broken image.
Also handle unchecked errors flagged by errcheck (tx.Rollback,
html.Render, fmt.Fprintf), guard capToTokens against a negative limit
that would panic, and stop logging the full import URL since it can
carry credentials.
Snippet import: new "Import from URL" flow fetches a page and stores its
readable content as a snippet. Extraction uses the mackee/go-readability
library (Mozilla Readability port) and outputs Markdown, so nav/footer
boilerplate is dropped. The snippet list shows the source, and the edit
dialog is now wider with a taller content box.
Summarize: new "Summarize with AI" action on a conversation calls the AI and
adds the result as a private note. It shows an info toast right away so the
user knows it started, since the call can take a few seconds. This adds an
"info" toast variant that any feature can use.
Assistant languages: assistants can be given a list of allowed reply
languages. The assistant replies in the customer's language when it is one of
them, otherwise it falls back to the first. The preview also lists the
knowledge sources it used.
Cleanup: replace hardcoded gray/zinc/white colors with theme tokens
(text-muted-foreground, text-foreground, bg-accent) across several components.
Thread context through provider calls so cancelled requests stop retrying, make snippet delete and FAQ review transitions atomic, cap provider response reads, guard stale AI replies and copilot responses from overwriting newer conversation state, and stop logging raw search queries and chunk content.
Reasoning models like gpt-5 reject max_tokens and non-default temperature.
The client now reads the structured 400, renames max_tokens to
max_completion_tokens or drops the bad tuning param, and retries. This
removes the old rule that only sent max_completion_tokens when reasoning
effort was set.
Provider form gets a "Test connection" button that makes one live call
with the form values and shows the provider's real error. Embedding test
also checks the returned vector length against the Dimensions field.
Add an Anthropic preset (their OpenAI-compatible endpoint) and stop
pre-filling temperature since blank is safe on every model.
Fix the shared Button loader so the spinner is visible on non-filled
variants, and polish the copilot panel: bot avatars on messages, dot
loader while thinking, cleaner input box, and tab slide-in animation.
Custom tools are HTTP calls with no read-only guarantee, so a mutating tool
could fire from copilot chat or while drafting a reply. Copilot and
generate-reply now get only the built-in knowledge search, and custom tools
stay exclusive to assistants where admins pick them explicitly. This matches
Intercom, Chatwoot, and Freshdesk. The contact identity headers now flow only
on the assistant path.
Also:
- new "Offer handoff to a human" switch on assistants (default on). When off,
the hand_off_to_human tool is not registered and the prompt tells the
assistant to say it cannot help instead of offering a human. Safety exits
(error, max turns, other participant) still unassign as before.
- workspace admin instructions from the AI config no longer leak into the
customer-facing assistant prompt.
- copilot and reply-draft prompts now treat conversation text and tool
outputs as untrusted data.
Replies from the AI agent are now converted from markdown to HTML with
goldmark before queueing, so bold, links, and lists render properly in the
widget, agent app, and email. The prompt now allows simple markdown. Raw
HTML in model output is escaped by goldmark, and both frontends sanitize
on render anyway.
Other fixes bundled in:
- validate avatar type and size before creating or updating an assistant,
and roll back the assistant if the avatar upload fails after create
- return 404 from agent update and API key endpoints for AI assistant
identity users, and hide assistants from mention and SLA user pickers
- reserve the autonomous assistant's built-in tool names so custom tools
cannot shadow them
- apply resolve after the reply is posted so the CSAT survey follows the
answer instead of preceding it
- unassign the assistant on handoff even when the fallback team is the
same team
- count reopens by status category instead of status name, and exclude
CSAT messages from the turn cap
- split oversized wrapper divs into child blocks when chunking KB HTML
instead of truncating them
- return 404 when soft-deleting an already-deleted agent, and keep AI
assistants (which have no email) visible in the compact users list
Reply drafts are now written in first person as the agent and never offer to escalate, since a human is already handling the conversation. Add quick preset buttons to the copilot panel and reuse a shared transcript helper.
CSAT was only sent when a human resolved from the UI. AI and automation resolves go straight through UpdateConversationStatus and skipped it. Moved the send there so all paths trigger it. It is idempotent and no-ops when the contact has no email.
Medium (title + short body):
add WIP autonomous AI agent
New internal/aiagent package runs AI assistants that reply to customers
on conversations assigned to them, grounded on a knowledge base. Also mines
resolved conversations for FAQ suggestions. Adds admin UI and the v2.7.0
schema. Still work in progress.
Adds two agent-facing AI features: a copilot chat panel in the conversation
sidebar and a generate-reply button in the reply box. Both run an agentic
tool-calling loop whose first tool searches the knowledge base.
Snippets are chunked and embedded on save, then searched in memory with
brute-force cosine similarity (no pgvector). Providers are split into completion
and embedding types. Both are OpenAI-compatible and the API key is encrypted at
rest. Admins can also register custom HTTP tools the model can call.
A new admin AI settings page covers provider config, snippets, and tools. The
v2.6.0 migration and schema add the ai_knowledge_base, embeddings, and ai_tools
tables plus the ai_providers type column.
CreateContact treated any SetExternalUserID failure as "ext_id taken" and
fell through to the upsert, so a transient DB error created a second
contact with the same email. Now only a unique violation or a contact
deleted mid-flight falls through, other errors are returned.
SetExternalUserID also reports whether a row was actually updated, and
dbutil error checks use errors.As so wrapped errors match. Widget JWTs
with no email now store NULL instead of an empty string.
Livechat:
- Add a live widget preview that renders in the inbox settings help rail.
- Warn when the primary or header colors are too close to the background.
- Require gradient/image background values and notice banner text when enabled.
- Show a default launcher logo in the widget when none is set, matching the preview.
- Drop the Beta badge from the livechat channel.
General settings:
- Add "set agents away on login". Agents log in as away and must go online manually.
- Add "show subject in conversation list" toggle.
Other:
- Add a keyboard shortcuts dialog from the user menu.
- Show a "snoozed until" badge on snoozed conversations.
- Contact search now matches emails partially instead of exact.
- Give outgoing message bubbles a secondary background.
Address CodeRabbit review: the query deleted by message uuid alone, so an agent
with access to one conversation could delete a private note from another by
pairing an accessible cuuid with a foreign message uuid. The delete is now
scoped via conversation_id = (SELECT id FROM conversations WHERE uuid = $2), and
DeletePrivateMessage / the handler pass the conversation uuid alongside the
message uuid.
[Used Claude Code 🤖]
Adds DELETE /api/v1/conversations/{cuuid}/messages/{uuid} for removing private
notes via the API. Uses a dedicated `delete-private-message` query/stmt
(separate from the generic `delete-message` used internally by continuity) so
only messages with private=true can be deleted — sent/incoming messages are
protected. Returns 404 if the message doesn't exist or isn't private.
Useful for API consumers that create private notes programmatically and need to
clean them up (e.g. replacing an outdated note before adding a new one).
[Used Claude Code 🤖]
- sanitize Content-Disposition filename via stringutil.SanitizeFilename to neutralize header injection
- use i18n.Tc for count-aware singular/plural attachment label
- append download anchor to DOM and defer URL.revokeObjectURL for cross-browser reliability
Adds a from_name_template field to email inboxes. When set and the sender is an agent, the template renders with agent and inbox variables and becomes the From display name; the inbox email address is unchanged. Any render failure, or a system-user sender, falls back to the plain inbox From so a misconfigured template never blocks delivery.
Variables: .Agent.FirstName, .Agent.LastName, .Agent.FullName, .Inbox.Name (the inbox name). Based on #249.
Co-authored-by: josephsellers <6892567+josephsellers@users.noreply.github.com>
Agents can download a full conversation as a .txt file from the
header menu. The conversation sidebar now shows the CSAT rating and
feedback once the customer responds.
Reject disabled agents in FilterAuthorizedListUUIDs, kick role members on
permission removal or role delete, and re-sub the list and open conv on
ws reconnect using local state. Also plug the app logger into the ws
package so kicks no longer log when no connections exist.
Convs and messages now push the full list-row payload to perm-authorized agents on create/assign/message, so livechat returning customers bubble up instantly without waiting for the timer refresh. Background list refresh bumped 30s -> 60s as a drift safety net.
Targets broadcasts via per-tab list and open subs (validated at subscribe time)
instead of broadcasting message uuid to all agents. Adds frontend list typing, throttled sound,
and many perf improvements to reduce cpu memory & database queries.
Other fixes: Enforce message-to-conversation binding in handleGetMessage