Secret placeholders not substituted inside parallel tool's nested tool_calls
Description
Secret alias placeholders (e.g. §§secret(KEY)) are correctly substituted when used in top-level tool arguments of a direct tool call. However, when the same placeholders appear inside the nested tool_calls array of the parallel tool, they are not substituted — the literal placeholder string is passed through to the inner tool, causing authentication failures and other errors.
Root Cause
The substitution is handled by the tool_execute_before extension:
extensions/python/tool_execute_before/_10_unmask_secrets.py
class UnmaskToolSecrets(Extension):
async def execute(self, **kwargs):
if not self.agent:
return
tool_args = kwargs.get("tool_args")
if not tool_args:
return
secrets_mgr = get_secrets_manager(self.agent.context)
for k, v in tool_args.items():
if isinstance(v, str): # <-- only top-level strings
tool_args[k] = secrets_mgr.replace_placeholders(v)
The loop only processes top-level string values (isinstance(v, str)). When the parallel tool is called, its tool_calls argument is a list of dicts — not a string — so it is skipped entirely. The nested tool_args inside each tool_calls entry are never traversed, and any §§secret(KEY) placeholders within them reach the inner tool as literal text.
The inner tool's execute_tool_call() in helpers/parallel_tools.py does call tool_execute_before again, but by that point the tool_args have already been passed through unchanged (the substitution only modifies top-level keys, and the worker reuses the same unmodified args).
Contrast with §§include()
The §§include() placeholder does work inside parallel because its substitution extension (extensions/python/response_stream/_15_replace_include_alias.py) uses a recursive replacement function:
def replace_placeholders(value: Any) -> Any:
if isinstance(value, str):
new_val = replace_file_includes(new_val, r"§§include\\(([^)]+)\\)")
return new_val
if isinstance(value, dict):
return {k: replace_placeholders(v) for k, v in value.items()}
if isinstance(value, list):
return [replace_placeholders(v) for v in value]
...
This inconsistency means §§include() works recursively but §§secret() does not.
Steps to Reproduce
- Store a secret, e.g.
§§secret(SOME_SECRET_TOKEN) in secrets.env.
- Call
code_execution_tool directly with the placeholder in the code arg — works (placeholder is substituted).
- Call
parallel with the same code_execution_tool nested inside tool_calls — fails (literal placeholder text is sent to the tool).
Failing example
{
"tool_name": "parallel",
"tool_args": {
"tool_calls": [
{
"tool_name": "code_execution_tool",
"tool_args": {
"code": "curl -H 'Authorization: Bearer §§secret(SOME_SECRET_TOKEN)' https://example.com/api"
}
}
]
}
}
The curl command receives the literal string §§secret(SOME_SECRET_TOKEN) instead of the actual token value.
Expected Behavior
§§secret() placeholders should be substituted recursively in all tool arguments, including nested structures inside parallel.tool_calls[], just as §§include() already does.
Suggested Fix
Make _10_unmask_secrets.py recursive, mirroring the approach in _15_replace_include_alias.py:
from helpers.secrets import get_secrets_manager
from typing import Any
def _replace_placeholders(value: Any, secrets_mgr) -> Any:
if isinstance(value, str):
return secrets_mgr.replace_placeholders(value)
if isinstance(value, dict):
return {k: _replace_placeholders(v, secrets_mgr) for k, v in value.items()}
if isinstance(value, list):
return [_replace_placeholders(v, secrets_mgr) for v in value]
if isinstance(value, tuple):
return tuple(_replace_placeholders(v, secrets_mgr) for v in value)
return value
class UnmaskToolSecrets(Extension):
async def execute(self, **kwargs):
if not self.agent:
return
tool_args = kwargs.get("tool_args")
if not tool_args:
return
secrets_mgr = get_secrets_manager(self.agent.context)
for k, v in tool_args.items():
tool_args[k] = _replace_placeholders(v, secrets_mgr)
Impact
Any workflow that uses parallel to batch independent API calls with secret-based authentication will fail. The workaround is to run such calls sequentially instead of in parallel, which defeats the purpose of the parallel tool.
Secret placeholders not substituted inside
paralleltool's nestedtool_callsDescription
Secret alias placeholders (e.g.
§§secret(KEY)) are correctly substituted when used in top-level tool arguments of a direct tool call. However, when the same placeholders appear inside the nestedtool_callsarray of theparalleltool, they are not substituted — the literal placeholder string is passed through to the inner tool, causing authentication failures and other errors.Root Cause
The substitution is handled by the
tool_execute_beforeextension:extensions/python/tool_execute_before/_10_unmask_secrets.pyThe loop only processes top-level string values (
isinstance(v, str)). When theparalleltool is called, itstool_callsargument is a list of dicts — not a string — so it is skipped entirely. The nestedtool_argsinside eachtool_callsentry are never traversed, and any§§secret(KEY)placeholders within them reach the inner tool as literal text.The inner tool's
execute_tool_call()inhelpers/parallel_tools.pydoes calltool_execute_beforeagain, but by that point thetool_argshave already been passed through unchanged (the substitution only modifies top-level keys, and the worker reuses the same unmodified args).Contrast with
§§include()The
§§include()placeholder does work insideparallelbecause its substitution extension (extensions/python/response_stream/_15_replace_include_alias.py) uses a recursive replacement function:This inconsistency means
§§include()works recursively but§§secret()does not.Steps to Reproduce
§§secret(SOME_SECRET_TOKEN)insecrets.env.code_execution_tooldirectly with the placeholder in thecodearg — works (placeholder is substituted).parallelwith the samecode_execution_toolnested insidetool_calls— fails (literal placeholder text is sent to the tool).Failing example
{ "tool_name": "parallel", "tool_args": { "tool_calls": [ { "tool_name": "code_execution_tool", "tool_args": { "code": "curl -H 'Authorization: Bearer §§secret(SOME_SECRET_TOKEN)' https://example.com/api" } } ] } }The curl command receives the literal string
§§secret(SOME_SECRET_TOKEN)instead of the actual token value.Expected Behavior
§§secret()placeholders should be substituted recursively in all tool arguments, including nested structures insideparallel.tool_calls[], just as§§include()already does.Suggested Fix
Make
_10_unmask_secrets.pyrecursive, mirroring the approach in_15_replace_include_alias.py:Impact
Any workflow that uses
parallelto batch independent API calls with secret-based authentication will fail. The workaround is to run such calls sequentially instead of in parallel, which defeats the purpose of theparalleltool.