feat(agent-sandbox): new tool resolver and bash execution implementation
Some checks are pending
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Waiting to run
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Blocked by required conditions
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Blocked by required conditions

This commit is contained in:
Harry 2026-01-13 18:16:33 +08:00
parent c6ba51127f
commit f28ded8455
11 changed files with 86 additions and 8 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -36,6 +36,9 @@ class InvokeFrom(StrEnum):
# this is used for plugin trigger and webhook trigger.
TRIGGER = "trigger"
# AGENT indicates that this invocation is from an agent.
AGENT = "agent"
# EXPLORE indicates that this invocation is from
# the workflow (or chatflow) explore page.
EXPLORE = "explore"

View File

@ -1,7 +1,7 @@
import shlex
from collections.abc import Generator
from typing import Any
from core.sandbox.debug import sandbox_debug
from core.tools.__base.tool import Tool
from core.tools.__base.tool_runtime import ToolRuntime
from core.tools.entities.common_entities import I18nObject
@ -68,7 +68,9 @@ class SandboxBashTool(Tool):
connection_handle = self._sandbox.establish_connection()
try:
cmd_list = shlex.split(command)
cmd_list = ["bash", "-c", command]
sandbox_debug("bash_tool", "cmd_list", cmd_list)
future = self._sandbox.run_command(connection_handle, cmd_list)
timeout = COMMAND_TIMEOUT_SECONDS if COMMAND_TIMEOUT_SECONDS > 0 else None
result = future.result(timeout=timeout)

18
api/core/sandbox/debug.py Normal file
View File

@ -0,0 +1,18 @@
"""Sandbox debug utilities. TODO: Remove this module when sandbox debugging is complete."""
from typing import Any
from core.callback_handler.agent_tool_callback_handler import print_text
SANDBOX_DEBUG_ENABLED = True
def sandbox_debug(tag: str, message: str, data: Any = None) -> None:
if not SANDBOX_DEBUG_ENABLED:
return
print_text(f"\n[{tag}]\n", color="blue")
if data is not None:
print_text(f"{message}: {data}\n", color="blue")
else:
print_text(f"{message}\n", color="blue")

View File

@ -5,9 +5,10 @@ from typing import TYPE_CHECKING, Any
from pydantic import BaseModel, Field
from core.model_runtime.utils.encoders import jsonable_encoder
from core.sandbox.constants import DIFY_CLI_PATH_PATTERN
from core.session.cli_api import CliApiSession
from core.tools.entities.tool_entities import ToolProviderType
from core.tools.entities.tool_entities import ToolParameter, ToolProviderType
from core.virtual_environment.__base.entities import Arch, OperatingSystem
if TYPE_CHECKING:
@ -77,11 +78,26 @@ class DifyCliToolConfig(BaseModel):
def create_from_tool(cls, tool: Tool) -> DifyCliToolConfig:
return cls(
provider_type=cls.transform_provider_type(tool.tool_provider_type()),
identity=tool.entity.identity.model_dump(),
description=tool.entity.description.model_dump() if tool.entity.description else {},
parameters=[param.model_dump() for param in tool.entity.parameters],
identity=to_json(tool.entity.identity),
description=to_json(tool.entity.description),
parameters=[cls.transform_parameter(parameter) for parameter in tool.entity.parameters],
)
@classmethod
def transform_parameter(cls, parameter: ToolParameter) -> dict[str, Any]:
transformed_parameter = to_json(parameter)
transformed_parameter.pop("input_schema", None)
transformed_parameter.pop("form", None)
match parameter.type:
case (
ToolParameter.ToolParameterType.SYSTEM_FILES
| ToolParameter.ToolParameterType.FILE
| ToolParameter.ToolParameterType.FILES
):
return transformed_parameter
case _:
return transformed_parameter
class DifyCliConfig(BaseModel):
env: DifyCliEnvConfig
@ -104,6 +120,10 @@ class DifyCliConfig(BaseModel):
)
def to_json(obj: Any) -> dict[str, Any]:
return jsonable_encoder(obj, exclude_unset=True, exclude_defaults=True, exclude_none=True)
__all__ = [
"DifyCliBinary",
"DifyCliConfig",

View File

@ -7,6 +7,7 @@ from types import TracebackType
from core.sandbox.bash_tool import SandboxBashTool
from core.sandbox.constants import DIFY_CLI_CONFIG_PATH, DIFY_CLI_PATH
from core.sandbox.debug import sandbox_debug
from core.sandbox.dify_cli import DifyCliConfig
from core.sandbox.manager import SandboxManager
from core.session.cli_api import CliApiSessionManager
@ -46,6 +47,7 @@ class SandboxSession:
config = DifyCliConfig.create(session, self._tools)
config_json = json.dumps(config.model_dump(mode="json"), ensure_ascii=False)
sandbox_debug("sandbox", "config_json", config_json)
sandbox.upload_file(DIFY_CLI_CONFIG_PATH, BytesIO(config_json.encode("utf-8")))
connection_handle = sandbox.establish_connection()

View File

@ -4,6 +4,7 @@ import shlex
from collections.abc import Mapping, Sequence
from typing import Any
from core.sandbox.debug import sandbox_debug
from core.sandbox.manager import SandboxManager
from core.virtual_environment.__base.command_future import CommandCancelledError, CommandTimeoutError
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
@ -83,6 +84,9 @@ class CommandNode(Node[CommandNodeData]):
try:
command = shlex.split(raw_command)
sandbox_debug("command_node", "command", command)
future = sandbox.run_command(connection_handle, command, cwd=working_directory)
result = future.result(timeout=timeout)

View File

@ -13,7 +13,7 @@ from sqlalchemy import select
from core.agent.entities import AgentEntity, AgentLog, AgentResult, AgentToolEntity, ExecutionContext
from core.agent.patterns import StrategyFactory
from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity
from core.app.entities.app_invoke_entities import InvokeFrom, ModelConfigWithCredentialsEntity
from core.file import File, FileTransferMethod, FileType, file_manager
from core.helper.code_executor import CodeExecutor, CodeLanguage
from core.llm_generator.output_parser.errors import OutputParserError
@ -1581,6 +1581,35 @@ class LLMNode(Node[LLMNodeData]):
result = yield from self._process_tool_outputs(outputs)
return result
def _prepare_sandbox_tools(self) -> list[Tool]:
"""Prepare sandbox tools."""
tool_instances = []
for tool in self._node_data.tools or []:
try:
# Get tool runtime from ToolManager
tool_runtime = ToolManager.get_tool_runtime(
tenant_id=self.tenant_id,
tool_name=tool.tool_name,
provider_id=tool.provider_name,
provider_type=tool.type,
invoke_from=InvokeFrom.AGENT,
credential_id=tool.credential_id,
)
# Apply custom description from extra field if available
if tool.extra.get("description") and tool_runtime.entity.description:
tool_runtime.entity.description.llm = (
tool.extra.get("description") or tool_runtime.entity.description.llm
)
tool_instances.append(tool_runtime)
except Exception as e:
logger.warning("Failed to load tool %s: %s", tool, str(e))
continue
return tool_instances
def _invoke_llm_with_sandbox(
self,
model_instance: ModelInstance,
@ -1592,7 +1621,7 @@ class LLMNode(Node[LLMNodeData]):
if not workflow_execution_id:
raise LLMNodeError("workflow_execution_id is required for sandbox runtime mode")
configured_tools = self._prepare_tool_instances(variable_pool)
configured_tools = self._prepare_sandbox_tools()
result: LLMGenerationData | None = None