fix(tools): fix stale secret-input decrypt cache reused across loop iterations - #40926
Open
chelsealong wants to merge 2 commits into
Open
fix(tools): fix stale secret-input decrypt cache reused across loop iterations#40926chelsealong wants to merge 2 commits into
chelsealong wants to merge 2 commits into
Conversation
…terations ToolParameterConfigurationManager.decrypt_tool_parameters cached decrypted secret-input values keyed only by tenant/provider/tool/node identity, with no dependency on the actual input values. When a secret-input parameter is dynamically bound inside a loop/iteration node, every iteration after the first silently reused the first iteration's decrypted secret instead of its own, because a cache hit returned the whole cached dict unconditionally. Fingerprint the secret-form inputs and only serve a cache entry when it was built from the same fingerprint, so static secrets keep the caching benefit and dynamically bound secrets resolve per call. Legacy entries without a fingerprint are treated as stale and recomputed. Fixes langgenius#40920
decrypt_tool_parameters previously returned the entire cached parameter dict wholesale on a fingerprint-matched cache hit, including any non-secret FORM parameter values captured at cache-write time. Since _convert_tool_parameters_type can resolve any FORM parameter (not just SECRET_INPUT) from a per-iteration variable selector, a non-secret field bound inside the same loop/iteration node was just as able to go stale as a secret one, while a constant secret input kept the fingerprint matching every iteration. Restrict the cache to storing/serving only the decrypted secret-input values, merged onto the current call's own freshly resolved parameters, so non-secret FORM values always come from the live call and can never be replayed from an earlier iteration. Fixes langgenius#40920
Contributor
Pyrefly Type Coverage
|
Contributor
Author
|
Checked the failing "Web Full-Stack E2E" check. The only failure is scenario "Run a minimal workflow app" ( That scenario runs a minimal workflow with no tool node, so it never touches |
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
Fixes #40920
ToolParameterConfigurationManager.decrypt_tool_parameters(api/core/tools/utils/configuration.py) caches decrypted secret-input tool parameters in Redis, keyed only bytenant_id:provider:tool_name:identity_id(ToolParameterCache, 24h TTL). The cache key does not depend on the actual input values, so on a cache hit the entire cached parameter dict was returned unconditionally.When a
secret-inputtool parameter is dynamically bound inside a loop/iteration node (e.g. a per-iteration webhook key resolved from the loop item), every iteration after the first silently reused the first iteration's decrypted secret instead of resolving its own — becauseidentity_idis scoped to the node, not the call, and the cache had no way to tell that the inputs changed between iterations.Fix
Fingerprint the secret-form input values (SHA-256 of the canonical JSON of just the secret-input parameters), and restrict the cache so a hit only ever supplies the decrypted secret field(s), merged onto the current call's own freshly resolved parameters:
_convert_tool_parameters_typecan resolve anyform=FORMparameter — not justSECRET_INPUTones — from a per-iteration variable selector, so a non-secret field (e.g. a loop-bound recipient/channel/URL) is just as able to vary per iteration as a secret one. Fingerprinting only the secret inputs while still returning the whole cached dict on a hit would leave that non-secret value servable stale whenever the co-located secret happens to stay constant across iterations. Restricting the cache to secret fields only closes that gap: non-secret values always come from the live call.Changes
api/core/tools/utils/configuration.py: add_secret_input_fingerprint;decrypt_tool_parametersnow caches/serves only the decrypted secret-input values (never the full parameter dict), gated on the fingerprint.api/tests/unit_tests/core/tools/utils/test_configuration.py: update the existing cache hit/miss test for the new cache entry shape, addtest_decrypt_tool_parameters_ignores_stale_cache_for_different_secret_input(a stale cache entry from a prior call with a different secret input must not be served for a new call), and addtest_decrypt_tool_parameters_cache_hit_never_stales_non_secret_form_param(a non-secret FORM parameter that varies per call, alongside a secret that stays constant, must always reflect the current call, never a cached value from an earlier call).Test plan
Confirmed the new/updated tests fail without the fix and pass with it:
Full related suite after the fix:
Lint / format / type-check on the changed files:
Disclosure
This PR was prepared with AI assistance (Claude).