feat(librechat): full API integration — server-side conversations, search, tags, sharing, memories, presets#28
Conversation
- PHP proxy now calls LibreChat's OpenAI-compatible API at /api/agents/v1/chat/completions - Model changed from 'hermes-agent' to 'agent_jada_nextcloud' (LibreChat agent with 118 MCP tools) - Default URL changed to http://LibreChat:3080 (Docker DNS on nextcloud-aio network) - Vue SSE parser handles standard OpenAI delta.tool_calls format (LibreChat) - Tool names stripped of _mcp_serverName suffix for display (e.g. nc_webdav_list_directory_mcp_nextcloud → nc_webdav_list_directory) - Backward compat: legacy hermes.tool.progress events still parsed - Version bumped to 0.4.0 - Updated info.xml description to reflect LibreChat architecture
Fixes Devin Review finding: /_mcp_[^_]+$/ fails for server names containing underscores (e.g. my_server). Changed to /_mcp_.+$/ which greedily matches everything after the _mcp_ delimiter.
- getSkills(): return empty list (LibreChat has no skills endpoint) - getModels(): proxy to LibreChat's /api/agents/v1/models - getConfig(): return stub with engine info (config managed via LibreChat admin UI) - getSessions(): return empty list (LibreChat manages sessions internally) Fixes Devin Review finding: these 4 methods still used Hermes-specific /api/v1/* paths that don't exist on LibreChat.
- AdminSettings.php: default URL http://localhost:18789 → http://LibreChat:3080 - SettingsController.php: same default URL update - templates/admin.php: placeholder URL + heading updated to LibreChat Fixes Devin Review finding: admin UI showed stale Hermes defaults.
…ations, search, tags, sharing, memories, presets Tier 1: Server-side persistence - Replace localStorage with LibreChat /api/convos + /api/messages - JWT auth with 14min TTL caching and auto-refresh on 401 - Service account (jada@nextcloud.local) for internal API access - Full-text search via /api/messages?search= (MeiliSearch) - Conversation list grouped by Today/Yesterday/This Week/Older Tier 2: New capabilities (backend wired) - File attachments via /api/files - Conversation sharing via /api/share - Agent memory via /api/memories - Tags via /api/tags Tier 3: Polish (backend wired) - Presets via /api/presets - Archive conversations via /api/convos/archive - Auto-generated titles via /api/convos/gen_title Changes: - OpenClawService.php: dual auth (API key + JWT), token caching, 401 retry - AgentController.php: 15+ new proxy methods for all LibreChat APIs - routes.php: 30+ new routes organized by domain - api.js: complete frontend API client for all endpoints - store.js: server-side state management replacing localStorage - ChatView.vue: loads messages from server, no localStorage - SearchView.vue: debounced server-side + instant local search - App.vue: grouped conversation sidebar with relative timestamps - Version bump to 0.5.0
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| $result = $this->openClaw->jwtGet('/api/convos?' . $params); | ||
| return new JSONResponse($result); |
There was a problem hiding this comment.
🔴 All Nextcloud users share a single LibreChat service account — no per-user data isolation for JWT-authed endpoints
All 21 JWT-authed endpoints (conversations, messages, search, tags, sharing, memories, presets) authenticate to LibreChat using a single shared service account JWT (jada@nextcloud.local). Unlike the API-key-authed endpoints (health, chat, models) which pass X-Nextcloud-User headers via getUserHeaders(), none of the jwtGet/jwtPost/jwtDelete calls include any user identity. This means LibreChat treats all requests as the same user, so all Nextcloud users see each other's conversations, messages, memories, tags, and presets. This directly contradicts the stated feature of "Per-User Isolation".
Example: getConversations passes no user identity
In lib/Controller/AgentController.php:200, jwtGet is called without user headers:
$result = $this->openClaw->jwtGet('/api/convos?' . $params);Compare with the API-key-authed health() at line 53 which does include user headers:
$result = $this->openClaw->get($apiPath . '/models', $this->getUserHeaders());Prompt for agents
The fundamental issue is that all JWT-authenticated LibreChat API calls go through a single shared service account with no per-user scoping. Every call to jwtGet/jwtPost/jwtDelete in AgentController.php needs to include user identity so LibreChat can scope data per user. There are two main approaches to fix this:
1. Per-user JWT approach: Instead of a single shared JWT, obtain and cache a separate JWT for each Nextcloud user. This requires creating individual LibreChat accounts per Nextcloud user (or mapping them). The JWT cache in app config would need to be keyed per user.
2. Shared JWT + user header approach: If LibreChat supports user scoping via request headers (similar to how X-Nextcloud-User works for the Agents API), pass getUserHeaders() as extra headers in all jwtGet/jwtPost/jwtDelete calls. This would require the controller methods to call e.g. $this->openClaw->jwtGet('/api/convos?' . $params, $this->getUserHeaders()) instead of $this->openClaw->jwtGet('/api/convos?' . $params).
All 21 JWT-authed endpoints in AgentController.php are affected: getConversations, getConversation, deleteConversation, updateConversation, archiveConversation, genTitle, getMessages, getConversationMessages, searchMessages, getTags, addTag, removeTag, getSharedLinks, createShareLink, deleteShareLink, getMemories, deleteMemory, getPresets, createPreset, deletePreset, getConversationToolCalls.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Valid architectural concern. This is a known limitation of the current "shared service account" approach — all Nextcloud users share one LibreChat identity (jada@nextcloud.local).
Why it's this way for now: LibreChat's internal APIs (/api/convos, /api/messages, etc.) scope data by the authenticated user's JWT. There's no header-based user impersonation like the Agents API supports. To get true per-user isolation, we need one of:
- Per-user LibreChat accounts — auto-provision a LibreChat user per Nextcloud user on first login, cache per-user JWTs keyed by NC uid. Most correct but requires user provisioning logic.
- OIDC SSO — connect Nextcloud as an OIDC provider to LibreChat so each NC user gets their own LibreChat session natively. This is the planned long-term fix.
- MongoDB query scoping — add a custom header that LibreChat reads for data scoping (would require LibreChat core changes, not viable).
For single-admin setups (which is the current deployment), this is a non-issue. For multi-user, option 1 is the pragmatic next step and option 2 is the proper solution. I'll note this in the PR description as a known limitation.
…ation, array_search bug
1. Move hardcoded service account credentials to Nextcloud app config
- Removed SERVICE_EMAIL and SERVICE_PASSWORD constants
- Credentials now read from librechat_service_email and librechat_service_password app config keys
- Exposed in AdminSettings.php and SettingsController.php for admin UI configuration
- Changing password auto-clears cached JWT to force re-auth
2. Fix array_search returning false on 401 retry
- When original token was empty, array_search returned false (coerced to 0)
- This overwrote Content-Type header instead of adding Authorization
- Now uses safe $authIndex !== false check with fallback to array append
3. Per-user isolation noted as architectural limitation
- Single service account JWT means all Nextcloud users share one LibreChat user
- Documented as known limitation; proper fix requires per-user LibreChat accounts
or OIDC SSO integration (planned for future)
Under strict_types=1, curl_exec returning false would throw TypeError when passed to json_decode(). Added the same false guard that exists in the main request flow.
…sthrough 1. Admin template + JS: Added form fields for librechat_service_email and librechat_service_password so JWT auth is configurable without DB access. Updated labels from OpenClaw to LibreChat. 2. chatSSE: Pass conversationId from request body to LibreChat payload so messages append to existing conversations instead of creating new ones.
ChatView watcher on activeConversationId already calls loadServerMessages(), so calling loadConversationMessages() in selectConversation caused two identical network requests per conversation selection.
1. ICredentialsManager for service password — stored encrypted at rest instead of plaintext in appconfig. Auto-migrates legacy plaintext on first read. AdminSettings + SettingsController updated to match. 2. skipNextWatcherLoad flag — prevents ChatView watcher from re-fetching messages when SSE streaming just created a new conversation. The watcher would race against server indexing and overwrite visible messages with empty/incomplete data.
| $agentName = $models[0]['name'] ?? 'Jada'; | ||
| $provider = $models[0]['provider'] ?? 'Gemini'; |
There was a problem hiding this comment.
🟡 Accessing $models[0] on empty array generates PHP 8 warnings in health endpoint
When the LibreChat /models endpoint returns no models (e.g., backend is down, misconfigured, or API key is missing), $models is []. Lines 57-58 access $models[0]['name'] and $models[0]['provider'] directly, which generates two PHP 8 warnings: "Undefined array key 0" followed by "Trying to access array offset on null". The ?? fallback values work correctly so the response is fine, but the warnings pollute logs and will fire on every health check (called every 30 seconds by the frontend).
Note that $agentName is assigned but never used — only $provider feeds into the response at line 68.
| $agentName = $models[0]['name'] ?? 'Jada'; | |
| $provider = $models[0]['provider'] ?? 'Gemini'; | |
| $firstModel = $models[0] ?? []; | |
| $agentName = $firstModel['name'] ?? 'Jada'; | |
| $provider = $firstModel['provider'] ?? 'Gemini'; |
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Replaces localStorage-based conversation persistence with full LibreChat server-side API integration across all 3 tiers. This is the single biggest quality improvement — conversations now sync across devices, survive cache clears, and include full tool call history in MongoDB.
Tier 1: Server-side persistence (core)
store.jsloads from/api/convosinstead of localStorage.ChatView.vueloads message history from/api/messages/{id}on conversation switch.SearchView.vueuses debounced server-side search via/api/messages?search=(MeiliSearch full-text) plus instant local search for conversations/tools/workspaces.OpenClawService.phpimplements dual auth — API key for Agents API, JWT for internal APIs. Tokens cached with 14min TTL, auto-refresh on 401.Tier 2: New capabilities (backend fully wired)
/api/files/api/share/api/memories/api/tags(workspace mapping)Tier 3: Polish (backend fully wired)
/api/presets/api/convos/archive/api/convos/gen_titleInfrastructure
routes.phpAgentController.phpapi.jsarray_searchreturningfalse)Devin Review fixes (0743d62)
SERVICE_EMAIL/SERVICE_PASSWORDconstants replaced withlibrechat_service_email/librechat_service_passwordapp config keys, configurable via Admin → Jada Agent settingsarray_searchfalse bug fixed — 401 retry path now safely checks$authIndex !== falsebefore array assignment, with fallback to appendKnown Limitations
jada@nextcloud.local). This means conversation data is not isolated per-user. For single-admin setups this is fine. Multi-user isolation requires either per-user LibreChat account provisioning or OIDC SSO integration (planned).Review & Testing Checklist for Human
Notes
jada@nextcloud.local) must exist in LibreChat's MongoDB withrole: ADMINand email verifiedLink to Devin session: https://app.devin.ai/sessions/f4e16f12bfa34fc6bdc3d73a833a5d91
Requested by: @itsablabla