Files
dify/api/services/dify_builder/tool_setup.py

94 lines
3.6 KiB
Python

"""Reconcile Builder-owned setup reminders without granting Tool execution.
Only verified installed inventory entries may reach this owner. Generated
credential IDs are untrusted: preserve an exact actor/provider/tenant-visible
selection, otherwise remove it and keep the real installed Tool unconfigured.
Descriptions preserve user text. Only unchanged recognizable generated blocks
are removed/replaced; edits to a block remain user text. Fresh native admission
never reads these descriptions or stores a readiness Boolean on the node.
"""
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any
from services.tools.draft_tool_setup import (
ToolDeclaration,
inventory_tool_nodes,
selected_credential_is_available,
tool_key,
)
if TYPE_CHECKING:
from services.dify_builder.agent.resources import ResourceRef
_START = "[Builder tool setup]"
_END = "[/Builder tool setup]"
_SETUP_TEXT = "Configure or select authorized credentials before testing or publishing."
_UNVERIFIED_TEXT = (
"Credential setup could not be verified. Configure or select authorized credentials before testing or publishing."
)
def _description(description: str, *, action: str, needs_setup: bool, unverified: bool) -> str:
"""Replace only exact generated blocks; leave modified text verbatim."""
label = " ".join(action.split())
blocks = [f"{_START}\n{label}: {text}\n{_END}" for text in (_SETUP_TEXT, _UNVERIFIED_TEXT)]
for block in blocks:
description = description.replace("\n\n" + block, "").replace(block, "")
if needs_setup:
note = blocks[1 if unverified else 0]
return description + ("\n\n" if description else "") + note
return description
def reconcile_generated_tool_setup(
*,
tenant_id: str,
actor_id: str | None,
graph: Mapping[str, Any],
tools: Mapping[tuple[str, str], "ResourceRef"],
) -> None:
"""Sanitize generated references and annotate delayed setup before preflight."""
for node in inventory_tool_nodes(graph):
data = node["data"]
ref = tools.get(tool_key(data))
if ref is None:
# Unknown installed identities will be refused by the generation
# allowlist; do not echo a generated credential into diagnostics.
data.pop("credential_id", None)
continue
declaration = ToolDeclaration(
provider=tool_key(data)[0],
action=tool_key(data)[1],
label=ref.label,
needs_credentials=ref.needs_credentials or ref.readiness == "missing_config",
is_plugin=bool(ref.plugin_id),
)
unverified = False
if not declaration.needs_credentials:
# Native credential-free declarations do not use a selected ID.
data.pop("credential_id", None)
available = True
else:
try:
from extensions.ext_application_services import application_services
available = selected_credential_is_available(
query=application_services().credential_queries,
tenant_id=tenant_id,
actor_id=actor_id,
declaration=declaration,
credential_id=data.get("credential_id"),
)
except Exception:
available = False
unverified = True
if not available:
data.pop("credential_id", None)
data["desc"] = _description(
str(data.get("desc") or ""),
action=ref.label,
needs_setup=not available,
unverified=unverified,
)