mirror of
https://github.com/langgenius/dify.git
synced 2026-09-24 23:22:26 +08:00
feat(agent): Sandbox / CLI Agent (dify.shell) + read-only sandbox file inspector (#36984)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
autofix-ci[bot]
parent
d3058d63bd
commit
44725dde74
@@ -12,24 +12,24 @@ SUPPORTED_AGENT_BACKEND_FEATURES = frozenset(
|
||||
"model",
|
||||
"structured_output",
|
||||
"tools.dify_tools",
|
||||
"tools.cli_tools",
|
||||
"env",
|
||||
"sandbox",
|
||||
}
|
||||
)
|
||||
|
||||
RESERVED_AGENT_BACKEND_FEATURES = frozenset(
|
||||
{
|
||||
"skills_files",
|
||||
"tools.cli_tools",
|
||||
"knowledge",
|
||||
"human",
|
||||
"env",
|
||||
"sandbox",
|
||||
"memory",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def build_runtime_feature_manifest(agent_soul: AgentSoulConfig) -> dict[str, Any]:
|
||||
"""Describe PRD capabilities that are persisted but not executed in phase 3."""
|
||||
"""Describe PRD capabilities supported by or still reserved from Agent backend runtime."""
|
||||
warnings: list[dict[str, str]] = []
|
||||
soul_dump = agent_soul.model_dump(mode="json", exclude_none=True, exclude_defaults=True)
|
||||
for section in sorted(RESERVED_AGENT_BACKEND_FEATURES):
|
||||
@@ -48,6 +48,9 @@ def build_runtime_feature_manifest(agent_soul: AgentSoulConfig) -> dict[str, Any
|
||||
|
||||
reserved_status = dict.fromkeys(sorted(RESERVED_AGENT_BACKEND_FEATURES), "reserved_not_executed")
|
||||
reserved_status["tools.dify_tools"] = "supported_when_config_valid"
|
||||
reserved_status["tools.cli_tools"] = "supported_by_shell_bootstrap"
|
||||
reserved_status["env"] = "supported_by_shell_bootstrap"
|
||||
reserved_status["sandbox"] = "forwarded_to_shell_layer_config"
|
||||
|
||||
return {
|
||||
"supported": sorted(SUPPORTED_AGENT_BACKEND_FEATURES),
|
||||
|
||||
@@ -2,11 +2,19 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Literal, Protocol, cast
|
||||
from typing import Any, Literal, Protocol, assert_never, cast
|
||||
|
||||
from agenton.compositor import CompositorSessionSnapshot
|
||||
from dify_agent.layers.execution_context import DifyExecutionContextLayerConfig
|
||||
from dify_agent.layers.shell import (
|
||||
DifyShellCliToolConfig,
|
||||
DifyShellEnvVarConfig,
|
||||
DifyShellLayerConfig,
|
||||
DifyShellSandboxConfig,
|
||||
DifyShellSecretRefConfig,
|
||||
)
|
||||
from dify_agent.protocol import CreateRunRequest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from clients.agent_backend import (
|
||||
AgentBackendModelConfig,
|
||||
@@ -15,6 +23,7 @@ from clients.agent_backend import (
|
||||
AgentBackendWorkflowNodeRunInput,
|
||||
redact_for_agent_backend_log,
|
||||
)
|
||||
from configs import dify_config
|
||||
from core.app.entities.app_invoke_entities import DifyRunContext, InvokeFrom
|
||||
from core.workflow.system_variables import SystemVariableKey, get_system_text
|
||||
from graphon.variables.segments import Segment
|
||||
@@ -123,10 +132,12 @@ class WorkflowAgentRuntimeRequestBuilder:
|
||||
)
|
||||
except WorkflowAgentPluginToolsBuildError as error:
|
||||
raise WorkflowAgentRuntimeRequestBuildError(error.error_code, str(error)) from error
|
||||
if tools_layer is not None:
|
||||
if tools_layer is not None or agent_soul.tools.cli_tools:
|
||||
metadata["agent_tools"] = {
|
||||
"dify_tool_count": len(tools_layer.tools),
|
||||
"dify_tool_names": [tool.name or tool.tool_name for tool in tools_layer.tools],
|
||||
"dify_tool_count": len(tools_layer.tools) if tools_layer is not None else 0,
|
||||
"dify_tool_names": [tool.name or tool.tool_name for tool in tools_layer.tools]
|
||||
if tools_layer is not None
|
||||
else [],
|
||||
"cli_tool_count": len(agent_soul.tools.cli_tools),
|
||||
}
|
||||
|
||||
@@ -165,6 +176,8 @@ class WorkflowAgentRuntimeRequestBuilder:
|
||||
user_prompt=user_prompt,
|
||||
output=self._build_output_config(node_job.declared_outputs),
|
||||
tools=tools_layer,
|
||||
include_shell=dify_config.AGENT_SHELL_ENABLED,
|
||||
shell_config=build_shell_layer_config(agent_soul),
|
||||
session_snapshot=context.session_snapshot,
|
||||
idempotency_key=self._idempotency_key(context),
|
||||
metadata=metadata,
|
||||
@@ -372,7 +385,9 @@ class WorkflowAgentRuntimeRequestBuilder:
|
||||
"mime_type": {"type": "string"},
|
||||
"url": {"type": "string"},
|
||||
},
|
||||
"required": ["file_id"],
|
||||
}
|
||||
assert_never(output_type)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_credentials(credentials: Mapping[str, Any]) -> dict[str, str | int | float | bool | None]:
|
||||
@@ -383,3 +398,73 @@ class WorkflowAgentRuntimeRequestBuilder:
|
||||
else:
|
||||
normalized[key] = str(value)
|
||||
return normalized
|
||||
|
||||
|
||||
def build_shell_layer_config(agent_soul: AgentSoulConfig) -> DifyShellLayerConfig:
|
||||
"""Map Agent Soul shell-adjacent fields into the Agent backend shell config."""
|
||||
sandbox_config = _plain_mapping(agent_soul.sandbox.config)
|
||||
return DifyShellLayerConfig(
|
||||
cli_tools=[tool for tool in (_shell_cli_tool(item) for item in agent_soul.tools.cli_tools) if tool is not None],
|
||||
env=[env for env in (_shell_env_var(item) for item in agent_soul.env.variables) if env is not None],
|
||||
secret_refs=[
|
||||
secret for secret in (_shell_secret_ref(item) for item in agent_soul.env.secret_refs) if secret is not None
|
||||
],
|
||||
sandbox=DifyShellSandboxConfig(
|
||||
provider=agent_soul.sandbox.provider,
|
||||
config=sandbox_config,
|
||||
)
|
||||
if agent_soul.sandbox.provider or sandbox_config
|
||||
else None,
|
||||
)
|
||||
|
||||
|
||||
def _shell_cli_tool(item: object) -> DifyShellCliToolConfig | None:
|
||||
data = _plain_mapping(item)
|
||||
commands: list[str] = []
|
||||
raw_commands = data.get("install_commands")
|
||||
if isinstance(raw_commands, list):
|
||||
commands.extend(str(command) for command in raw_commands if str(command).strip())
|
||||
for key in ("install_command", "install", "setup_command"):
|
||||
raw_command = data.get(key)
|
||||
if isinstance(raw_command, str) and raw_command.strip():
|
||||
commands.append(raw_command)
|
||||
name = data.get("name") or data.get("tool_name") or data.get("label")
|
||||
if not commands and not isinstance(name, str):
|
||||
return None
|
||||
return DifyShellCliToolConfig(name=name if isinstance(name, str) else None, install_commands=commands)
|
||||
|
||||
|
||||
def _shell_env_var(item: object) -> DifyShellEnvVarConfig | None:
|
||||
data = _plain_mapping(item)
|
||||
name = _name_from_mapping(data)
|
||||
if name is None:
|
||||
return None
|
||||
value = data.get("value", data.get("default", ""))
|
||||
if not isinstance(value, str):
|
||||
value = str(value)
|
||||
return DifyShellEnvVarConfig(name=name, value=value)
|
||||
|
||||
|
||||
def _shell_secret_ref(item: object) -> DifyShellSecretRefConfig | None:
|
||||
data = _plain_mapping(item)
|
||||
name = _name_from_mapping(data)
|
||||
if name is None:
|
||||
return None
|
||||
ref = data.get("ref") or data.get("id") or data.get("credential_id") or data.get("provider_credential_id")
|
||||
return DifyShellSecretRefConfig(name=name, ref=str(ref) if ref is not None else None)
|
||||
|
||||
|
||||
def _plain_mapping(item: object) -> dict[str, Any]:
|
||||
if isinstance(item, BaseModel):
|
||||
return item.model_dump(mode="python", exclude_none=True, exclude_defaults=True)
|
||||
if isinstance(item, Mapping):
|
||||
return dict(item)
|
||||
return {}
|
||||
|
||||
|
||||
def _name_from_mapping(item: Mapping[str, Any]) -> str | None:
|
||||
for key in ("name", "key", "env_name", "variable"):
|
||||
value = item.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()
|
||||
return None
|
||||
|
||||
@@ -303,8 +303,18 @@ class WorkflowAgentNodeValidator:
|
||||
f"Workflow Agent node {binding.node_id} has duplicate Dify Plugin Tool name {exposed_name}."
|
||||
)
|
||||
exposed_names.add(exposed_name)
|
||||
# CLI tools remain saved-but-not-executed. They are allowed at publish
|
||||
# time so existing Agent Soul drafts are not blocked by a reserved field.
|
||||
|
||||
cli_tool_names: set[str] = set()
|
||||
for cli_tool in agent_soul.tools.cli_tools:
|
||||
name = cli_tool.get("name") or cli_tool.get("tool_name") or cli_tool.get("label")
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
continue
|
||||
normalized_name = name.strip()
|
||||
if normalized_name in cli_tool_names:
|
||||
raise WorkflowAgentNodeValidationError(
|
||||
f"Workflow Agent node {binding.node_id} has duplicate CLI Tool name {normalized_name}."
|
||||
)
|
||||
cli_tool_names.add(normalized_name)
|
||||
|
||||
@staticmethod
|
||||
def _validate_file_ref(
|
||||
|
||||
Reference in New Issue
Block a user