Tool dynamic-select uses a credential_id-agnostic singleton cache that is never invalidated on credential update (stale credentials)
- Dominant language
- TypeScript
- Stars
- 156k
- Forks
- 24.6k
- Avg merge
- 22h 9m
- Merged PRs (30d)
- 610
Description
# Tool `dynamic-select` uses a credential_id-agnostic singleton cache that is never invalidated on credential update (stale credentials)
## Summary
When fetching options for a tool's `dynamic-select` parameter, Dify decrypts the provider
credentials through a **provider-level singleton cache** (`SingletonProviderCredentialsCache`)
that (a) ignores `credential_id` and (b) is **not invalidated** when a credential is created,
updated, or deleted. As a result:
- After editing a tool provider credential (e.g. changing a non-secret field such as
`base_url`), the `dynamic-select` dropdown keeps using the **old** value for up to the cache
TTL (24h), even though the DB row and the per-credential cache are already updated.
- With **multiple credentials** on the same provider (different `base_url` / API key), the
singleton cache **collides** — whichever credential was decrypted first "wins", so the
dropdown can silently use the wrong credential.
The regular tool invocation path is not affected because it uses the per-`credential_id`
cache. The inconsistency is specific to the `dynamic-select` (parameter options) path.
## Affected version
Dify 1.15.0 (code paths unchanged on `main` at time of writing). Plugin daemon 0.6.3.
## Steps to reproduce
1. Install a tool plugin with a `dynamic-select` parameter whose options are fetched from an
external API using a `base_url` credential (any tool plugin implementing
`_fetch_parameter_options`).
2. Configure a credential with `base_url = A`. Open the dropdown once (works; value `A` is now
cached in the singleton cache).
3. Edit the same credential, set `base_url = B`, and save.
4. Open the dropdown again → it still calls `A` (stale) until the 24h TTL expires.
- Variant: add a *second* credential with `base_url = B`; the dropdown may still use `A`
because the singleton cache key does not include `credential_id`.
## Root cause (code references)
`get_dynamic_select_options` (tool branch) decrypts via `create_tool_provider_encrypter`:
- `api/services/plugin/plugin_parameter_service.py` — `PluginParameterService.get_dynamic_select_options`
→ `encrypter.decrypt(db_record.credentials)`
- `api/core/tools/utils/encryption.py` — `create_tool_provider_encrypter()` builds a
`SingletonProviderCredentialsCache(tenant_id, provider_type, provider_identity)` — **no
`credential_id`**.
- `api/core/helper/provider_cache.py` — `SingletonProviderCredentialsCache._generate_cache_key`
→ `"{provider_type}_credentials:tenant_id:{tenant_id}:id:{provider_type}.{identity_name}"`
(one entry per provider, TTL 86400s).
Meanwhile the credential create/update/delete flow only invalidates the **per-credential**
cache:
- `api/services/tools/builtin_tools_manage_service.py` uses
`ToolProviderCredentialsCache(tenant_id, provider, credential_id=...)` and calls
`cache.delete()` on save/update/delete — this never touches the singleton cache above.
- Regular tool invocation also uses the per-credential cache
(`api/core/tools/tool_manager.py`, `ToolProviderCredentialsCache`), which is why only
`dynamic-select` is affected.
## Evidence
Confirmed against a running 1.15.0 instance: the Redis key
```
plugin_credentials:tenant_id::id:plugin.
```
held the **old** `base_url`, while the DB row and the per-credential
`tool_credentials:...:credential_id:` key held the **new** value. Deleting the
singleton key made the dropdown pick up the correct value immediately (no restart needed).
## Suggested fix (any of)
1. Invalidate the singleton cache in the credential create/update/delete flow
(`builtin_tools_manage_service`), in addition to the per-credential cache; **or**
2. Make the `dynamic-select` path use the per-`credential_id` `ToolProviderCredentialsCache`
(consistent with `tool_manager`); **or**
3. Have `create_tool_provider_encrypter` take a `credential_id` and key the cache by it.
## Related
- #37986 (parent #37983) — credential visibility / usage-boundary consistency (mentions
dynamic-option paths carrying `credential_id` inconsistently).
- #30160 / #30161 — frontend cache for *trigger* dynamic-select edit modal (different layer /
provider type; not the same as this backend tool-credential cache).
Contributor guide
Research direction
Trace PluginParameterService.get_dynamic_select_options through api/core/tools/utils/encryption.py and SingletonProviderCredentialsCache in api/core/helper/provider_cache.py. Compare its cache behavior with ToolProviderCredentialsCache in builtin_tools_manage_service.py and api/core/tools/tool_manager.py. Done means dynamic-select reflects credential updates and keeps multiple credential_ids from sharing stale provider-level values.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, redis
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 56/100