langgenius / langgenius/dify

Tool dynamic-select uses a credential_id-agnostic singleton cache that is never invalidated on credential update (stale credentials)

Open
#38,633 0 comments 1 reaction 0 assignees View on GitHub
1.15.0 project#dify
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.