🪢 fix: Action Domain Encoding Collision for HTTPS URLs#12271
Merged
Conversation
All https:// (and http://) domains produced the same 10-char base64 prefix due to ENCODED_DOMAIN_LENGTH truncation, causing tool name collisions for agents with multiple actions. Strip the protocol before encoding so the base64 key is derived from the hostname. Add `legacyDomainEncode` to preserve the old encoding logic for backward-compatible matching of existing stored actions.
Update `getActionToolDefinitions` to match stored tools against both new and legacy domain encodings. Update `loadActionToolsForExecution` to resolve model-called tool names via a `normalizedToDomain` map that includes both encoding variants, with legacy fallback for request builder lookup.
Save routes now remove old tools matching either new or legacy domain encoding, preventing stale entries when an action's encoding changes on update. Delete routes no longer re-encode the already-encoded domain extracted from the stored actions array, which was producing incorrect keys and leaving orphaned tools.
Rewrite ActionService tests to cover real matching patterns used by ToolService and action routes. Tests verify encode/decode round-trips, protocol stripping, backward-compatible tool name matching at both definition and execution phases, save-route cleanup of old/new encodings, delete-route domain extraction, and the collision fix for multi-action agents.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates how Action “domains” are encoded into tool names to prevent collisions (notably those caused by base64 prefixes when encoding full https://... URLs), while adding backward-compatibility so previously-saved agent/assistant tool names continue to match.
Changes:
- Update
domainParser()to encode based on protocol-stripped inputs and introducelegacyDomainEncode()for compatibility with previously stored tool names. - Extend tool-definition matching and execution-time tool resolution to recognize both new and legacy encoded domain variants.
- Update agent/assistant action routes to remove/replace tools using both new and legacy encodings during action updates.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| api/server/services/ToolService.js | Adds legacy-aware matching for action tool definitions and execution-time tool loading. |
| api/server/services/ActionService.js | Implements protocol-stripping encoding changes and adds legacyDomainEncode() / stripProtocol(). |
| api/server/services/ActionService.spec.js | Expands tests around new/legacy encoding behavior and matching logic. |
| api/server/routes/assistants/actions.js | Removes old tools using both new and legacy encoded domain names on action upsert. |
| api/server/routes/agents/actions.js | Same as assistants route, but for agent actions tool string lists. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
…inEncode sync CRITICAL: processRequiredActions (assistants path) was not updated with legacy domain matching — existing assistants with https:// domain actions would silently fail post-deployment because domainMap only had new encoding. MAJOR: loadAgentTools definitionsOnly=false path had the same issue. Both now use a normalizedToDomain map with legacy+new entries and extract function names via the matched key (not the canonical domain). Also: make legacyDomainEncode synchronous (no async operations), store legacyNormalized in processedActionSets to eliminate recomputation in the per-tool fallback, and hoist domainSeparatorRegex to module level.
…ction routes Rename shadowed 'domain' to 'encodedDomain' to separate raw URL from encoded key in both agent and assistant save routes. Rename shouldRemoveTool to shouldRemoveAgentTool / shouldRemoveAssistantTool to make the distinct data-shape guards explicit. Remove await on now-synchronous legacyDomainEncode.
- Add validateAndUpdateTool tests (protocol-stripping match logic) - Restore unicode domain encode/decode/round-trip tests - Add processRequiredActions matching pattern tests (assistants path) - Add legacy guard skip test for short bare hostnames - Add pre-normalized Set test for definition-phase optimization - Fix corrupt-cache test to assert typeof instead of toBeDefined - Verify legacyDomainEncode is synchronous (not a Promise) - Remove all await on legacyDomainEncode (now sync) 58 tests, up from 44.
A: Fix stale JSDoc @returns {Promise<string>} on now-synchronous legacyDomainEncode — changed to @returns {string}. B: Rename normalizedToDomain to domainLookupMap in processRequiredActions and loadAgentTools where keys are raw encoded domains (not normalized), avoiding confusion with loadActionToolsForExecution where keys ARE normalized. C: Pre-normalize actionToolNames into a Set<string> in getActionToolDefinitions, replacing O(signatures × tools) per-check .some() + .replace() with O(1) Set.has() lookups. D: Remove stripProtocol from ActionService exports — it is a one-line internal helper. Spec tests for it removed; behavior is fully covered by domainParser protocol-stripping tests. E: Fix pre-existing bug where processRequiredActions re-loaded action sets on every missing-tool iteration. The guard !actionSets.length always re-triggered because actionSets was reassigned to a plain object (whose .length is undefined). Replaced with a null-check on a dedicated actionSetsData variable.
152355b to
5025a4c
Compare
URLs like 'https://api.example.com/v1/endpoint?foo=bar' previously retained the path after protocol stripping, contaminating the encoded domain key. Now strips everything after the first '/' following the host, using string indexing instead of URL parsing to avoid punycode normalization of unicode hostnames. Closes Copilot review comments 1, 2, and 5.
d02a15e to
57c345d
Compare
jcbartle
pushed a commit
to jcbartle/LibreChat
that referenced
this pull request
May 11, 2026
…2271) * fix: strip protocol from domain before encoding in `domainParser` All https:// (and http://) domains produced the same 10-char base64 prefix due to ENCODED_DOMAIN_LENGTH truncation, causing tool name collisions for agents with multiple actions. Strip the protocol before encoding so the base64 key is derived from the hostname. Add `legacyDomainEncode` to preserve the old encoding logic for backward-compatible matching of existing stored actions. * fix: backward-compatible tool matching in ToolService Update `getActionToolDefinitions` to match stored tools against both new and legacy domain encodings. Update `loadActionToolsForExecution` to resolve model-called tool names via a `normalizedToDomain` map that includes both encoding variants, with legacy fallback for request builder lookup. * fix: action route save/delete domain encoding issues Save routes now remove old tools matching either new or legacy domain encoding, preventing stale entries when an action's encoding changes on update. Delete routes no longer re-encode the already-encoded domain extracted from the stored actions array, which was producing incorrect keys and leaving orphaned tools. * test: comprehensive coverage for action domain encoding Rewrite ActionService tests to cover real matching patterns used by ToolService and action routes. Tests verify encode/decode round-trips, protocol stripping, backward-compatible tool name matching at both definition and execution phases, save-route cleanup of old/new encodings, delete-route domain extraction, and the collision fix for multi-action agents. * fix: add legacy domain compat to all execution paths, make legacyDomainEncode sync CRITICAL: processRequiredActions (assistants path) was not updated with legacy domain matching — existing assistants with https:// domain actions would silently fail post-deployment because domainMap only had new encoding. MAJOR: loadAgentTools definitionsOnly=false path had the same issue. Both now use a normalizedToDomain map with legacy+new entries and extract function names via the matched key (not the canonical domain). Also: make legacyDomainEncode synchronous (no async operations), store legacyNormalized in processedActionSets to eliminate recomputation in the per-tool fallback, and hoist domainSeparatorRegex to module level. * refactor: clarify domain variable naming and tool-filter helpers in action routes Rename shadowed 'domain' to 'encodedDomain' to separate raw URL from encoded key in both agent and assistant save routes. Rename shouldRemoveTool to shouldRemoveAgentTool / shouldRemoveAssistantTool to make the distinct data-shape guards explicit. Remove await on now-synchronous legacyDomainEncode. * test: expand coverage for all review findings - Add validateAndUpdateTool tests (protocol-stripping match logic) - Restore unicode domain encode/decode/round-trip tests - Add processRequiredActions matching pattern tests (assistants path) - Add legacy guard skip test for short bare hostnames - Add pre-normalized Set test for definition-phase optimization - Fix corrupt-cache test to assert typeof instead of toBeDefined - Verify legacyDomainEncode is synchronous (not a Promise) - Remove all await on legacyDomainEncode (now sync) 58 tests, up from 44. * fix: address follow-up review findings A-E A: Fix stale JSDoc @returns {Promise<string>} on now-synchronous legacyDomainEncode — changed to @returns {string}. B: Rename normalizedToDomain to domainLookupMap in processRequiredActions and loadAgentTools where keys are raw encoded domains (not normalized), avoiding confusion with loadActionToolsForExecution where keys ARE normalized. C: Pre-normalize actionToolNames into a Set<string> in getActionToolDefinitions, replacing O(signatures × tools) per-check .some() + .replace() with O(1) Set.has() lookups. D: Remove stripProtocol from ActionService exports — it is a one-line internal helper. Spec tests for it removed; behavior is fully covered by domainParser protocol-stripping tests. E: Fix pre-existing bug where processRequiredActions re-loaded action sets on every missing-tool iteration. The guard !actionSets.length always re-triggered because actionSets was reassigned to a plain object (whose .length is undefined). Replaced with a null-check on a dedicated actionSetsData variable. * fix: strip path and query from domain URLs in stripProtocol URLs like 'https://api.example.com/v1/endpoint?foo=bar' previously retained the path after protocol stripping, contaminating the encoded domain key. Now strips everything after the first '/' following the host, using string indexing instead of URL parsing to avoid punycode normalization of unicode hostnames. Closes Copilot review comments 1, 2, and 5.
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.
Summary
Fixed a base64 key collision in
domainParserwhere allhttps://(andhttp://) domains produced the same 10-character truncated key (aHR0cHM6Ly) due to the protocol prefix dominating theENCODED_DOMAIN_LENGTH-truncated base64 output. This caused tool name collisions for agents and assistants with multiple actions pointing to different domains. Added full backward-compatibility support for actions stored before this fix.domainParserto strip the protocol prefix before base64-encoding, so the key is derived from the hostname rather than the full URL.legacyDomainEncodeto reproduce the old (broken) encoding, used exclusively for backward-compatible matching of actions stored before this fix.loadActionToolsForExecutionto register both new and legacy normalized domain keys innormalizedToDomain, withlegacyNormalizedstored inprocessedActionSetsto avoid redundant recomputation in the per-tool fallback path.getActionToolDefinitionsto match stored tool names against both new and legacy encodings; pre-normalizedactionToolNamesinto aSet<string>before the loop, replacing O(signatures × tools).some()+.replace()with O(1).has()lookups.processRequiredActions(assistants execution path) — it previously builtdomainMapwith new-encoding keys only, silently dropping all tool calls for assistants with legacy-encoded tools stored in OpenAI. Replaced withdomainLookupMapcarrying both encodings; function name is now extracted viamatchedKeyrather than the canonical domain.loadAgentToolsdefinitionsOnly=falsepath with the samedomainLookupMap+matchedKeypattern.processRequiredActionswhere the!actionSets.lengthguard re-triggered on every missing-tool iteration after the array was reassigned to a plain object. Replaced with anull-initializedactionSetsDatavariable and a null-check.domainParser, which was producing incorrect keys and leaving orphaned tool entries.legacyDomainEncodesynchronous — it contains no I/O or async operations.domainSeparatorRegexto module level inToolService.js.domainvariable toencodedDomainin both action route save handlers to separate the raw URL from the encoded key.shouldRemoveTooltoshouldRemoveAgentTool/shouldRemoveAssistantToolto make the distinct data-shape guards explicit.normalizedToDomaintodomainLookupMapin the two functions where map keys are raw encoded domains rather than normalized ones.stripProtocolfromActionServiceexports; it is an internal helper whose behavior is fully covered by thedomainParserprotocol-stripping tests.validateAndUpdateToolto match actions wheremetadata.domaincarries anhttps://prefix against tool names carrying only the bare hostname.ActionService.spec.jswith 50 tests covering:domainParserencode/decode round-trips, protocol stripping, unicode hostnames,legacyDomainEncodesynchronicity and collision behavior,validateAndUpdateToolmatching logic, backward-compatible tool name matching at both definition and execution phases, save-route cleanup of old and new encodings, delete-route domain extraction, and the multi-action collision scenario.Change Type
Testing
The collision fix and backward-compatibility layer are covered by the rewritten unit test suite in
api/server/services/ActionService.spec.js(50 tests). The following scenarios are explicitly exercised:https://actions on the same agent previously produced identical tool names; they now produce distinct keys.getPeople---aHR0cHM6Ly) resolves correctly to the right action and function name via thedomainLookupMap/matchedKeypattern.domainParserwithhttps://input and bare-hostname input produce identical output.Test Configuration: Run from the
apiworkspace:Checklist