Skip to content

🪢 fix: Action Domain Encoding Collision for HTTPS URLs#12271

Merged
danny-avila merged 9 commits into
devfrom
fix/action-domain-encoding
Mar 17, 2026
Merged

🪢 fix: Action Domain Encoding Collision for HTTPS URLs#12271
danny-avila merged 9 commits into
devfrom
fix/action-domain-encoding

Conversation

@danny-avila

@danny-avila danny-avila commented Mar 16, 2026

Copy link
Copy Markdown
Owner

Summary

Fixed a base64 key collision in domainParser where all https:// (and http://) domains produced the same 10-character truncated key (aHR0cHM6Ly) due to the protocol prefix dominating the ENCODED_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.

  • Fixed domainParser to strip the protocol prefix before base64-encoding, so the key is derived from the hostname rather than the full URL.
  • Added legacyDomainEncode to reproduce the old (broken) encoding, used exclusively for backward-compatible matching of actions stored before this fix.
  • Updated loadActionToolsForExecution to register both new and legacy normalized domain keys in normalizedToDomain, with legacyNormalized stored in processedActionSets to avoid redundant recomputation in the per-tool fallback path.
  • Updated getActionToolDefinitions to match stored tool names against both new and legacy encodings; pre-normalized actionToolNames into a Set<string> before the loop, replacing O(signatures × tools) .some() + .replace() with O(1) .has() lookups.
  • Fixed processRequiredActions (assistants execution path) — it previously built domainMap with new-encoding keys only, silently dropping all tool calls for assistants with legacy-encoded tools stored in OpenAI. Replaced with domainLookupMap carrying both encodings; function name is now extracted via matchedKey rather than the canonical domain.
  • Fixed loadAgentTools definitionsOnly=false path with the same domainLookupMap + matchedKey pattern.
  • Fixed pre-existing bug in processRequiredActions where the !actionSets.length guard re-triggered on every missing-tool iteration after the array was reassigned to a plain object. Replaced with a null-initialized actionSetsData variable and a null-check.
  • Fixed agent and assistant save routes to filter tools matching both new and legacy domain encodings, preventing stale tool entries when an action's encoding changes on update.
  • Fixed agent and assistant delete routes to use the domain stored verbatim in the actions array rather than re-encoding it through domainParser, which was producing incorrect keys and leaving orphaned tool entries.
  • Made legacyDomainEncode synchronous — it contains no I/O or async operations.
  • Hoisted domainSeparatorRegex to module level in ToolService.js.
  • Renamed the domain variable to encodedDomain in both action route save handlers to separate the raw URL from the encoded key.
  • Renamed shouldRemoveTool to shouldRemoveAgentTool / shouldRemoveAssistantTool to make the distinct data-shape guards explicit.
  • Renamed normalizedToDomain to domainLookupMap in the two functions where map keys are raw encoded domains rather than normalized ones.
  • Removed stripProtocol from ActionService exports; it is an internal helper whose behavior is fully covered by the domainParser protocol-stripping tests.
  • Updated validateAndUpdateTool to match actions where metadata.domain carries an https:// prefix against tool names carrying only the bare hostname.
  • Rewrote ActionService.spec.js with 50 tests covering: domainParser encode/decode round-trips, protocol stripping, unicode hostnames, legacyDomainEncode synchronicity and collision behavior, validateAndUpdateTool matching 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

  • Bug fix (non-breaking change which fixes an issue)

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:

  • Two https:// actions on the same agent previously produced identical tool names; they now produce distinct keys.
  • A legacy-encoded tool name (e.g., getPeople---aHR0cHM6Ly) resolves correctly to the right action and function name via the domainLookupMap/matchedKey pattern.
  • A new-encoded tool name resolves correctly through the same path.
  • Save routes remove tools matching both the new and legacy encoding of the updated domain.
  • Delete routes extract the domain verbatim from the stored actions array without re-encoding it.
  • domainParser with https:// input and bare-hostname input produce identical output.
  • Unicode hostnames encode, decode, and round-trip correctly.

Test Configuration: Run from the api workspace:

cd api && npx jest ActionService.spec

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes

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.
Copilot AI review requested due to automatic review settings March 16, 2026 21:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 introduce legacyDomainEncode() 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.

Comment thread api/server/services/ActionService.js
Comment thread api/server/services/ActionService.js
Comment thread api/server/services/ActionService.js
Comment thread api/server/services/ActionService.spec.js
Comment thread api/server/services/ActionService.js Outdated
…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.
@danny-avila danny-avila force-pushed the fix/action-domain-encoding branch from 152355b to 5025a4c Compare March 16, 2026 21:57
@danny-avila danny-avila changed the title fix/action domain encoding 🪢 fix: Action Domain Encoding Collision for HTTPS URLs Mar 17, 2026
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.
@danny-avila danny-avila force-pushed the fix/action-domain-encoding branch from d02a15e to 57c345d Compare March 17, 2026 05:34
@danny-avila danny-avila merged commit 9a64791 into dev Mar 17, 2026
9 checks passed
@danny-avila danny-avila deleted the fix/action-domain-encoding branch March 17, 2026 05:38
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants