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

217 lines
9.1 KiB
Python

"""Exact installed metadata for the qualified JSON Process parse action.
The fixed qualifications describe reviewed administrator-installed runtimes,
not atomic leases on daemon bytes. Callers read metadata before and after dispatch.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from core.plugin.entities.plugin import PluginInstallationSource
from core.plugin.impl.plugin import PluginInstaller
from core.plugin.impl.tool import PluginToolManager
from runtime_contracts.builder.execution import (
BuilderExecutionPolicyError,
QualifiedJsonToolBinding,
canonical_digest,
)
if TYPE_CHECKING:
from core.plugin.entities.plugin_daemon import PluginToolProviderEntity
from core.tools.entities.tool_entities import ToolEntity, ToolProviderEntityWithPlugin
PLUGIN_ID = "langgenius/json_process"
PROVIDER_ID = "langgenius/json_process/json_process"
NATIVE_PROVIDER_NAME = "json_process"
TOOL_NAME = "parse"
VERSION = "0.0.8"
CHECKSUM = "3333687ec0412e1f2a1798d632fc5f3187f36ebd4bf2432a599a96c74e92f503"
UNIQUE_IDENTIFIER = f"{PLUGIN_ID}:{VERSION}@{CHECKSUM}"
PROVIDER_SCHEMA_DIGEST = "90493459b7c95b78148c085cb33833c932c671f5e7296c0e7d8b050f2a6998a3"
TOOL_SCHEMA_DIGEST = "bf4897c1c2e3b3a04e9abc8bfdd4d7715abbd7bc5b2148ad4067b6b9fe11e449"
QUALIFICATION_REVISION = "local-json-process-0.0.8-sdk-0.10.2-jsonpath-ng-1.8.0-python-3.12.3-v1"
QUALIFICATION_DIGEST = canonical_digest(
{
"revision": QUALIFICATION_REVISION,
"identifier": UNIQUE_IDENTIFIER,
"provider_schema_digest": PROVIDER_SCHEMA_DIGEST,
"tool_schema_digest": TOOL_SCHEMA_DIGEST,
"archive_sha256": "a43cbcb37762c512285f0b9ce2a5eb239e205e0b3a2c0fa2d9dba23a5aff6944",
"runtime_type": "local",
}
)
SERVERLESS_QUALIFICATION_REVISION = "serverless-json-process-0.0.8-sdk-0.10.2-jsonpath-ng-1.8.0-python-3.12.14-v1"
SERVERLESS_QUALIFICATION_DIGEST = canonical_digest(
{
"revision": SERVERLESS_QUALIFICATION_REVISION,
"identifier": UNIQUE_IDENTIFIER,
"provider_schema_digest": PROVIDER_SCHEMA_DIGEST,
"tool_schema_digest": TOOL_SCHEMA_DIGEST,
"archive_sha256": "a43cbcb37762c512285f0b9ce2a5eb239e205e0b3a2c0fa2d9dba23a5aff6944",
"runtime_type": "serverless",
}
)
@dataclass(frozen=True, slots=True)
class InstalledJsonParse:
tenant_id: str
installation_id: str
plugin_id: str
provider_id: str
tool_name: str
unique_identifier: str
version: str
checksum: str
provider_schema_digest: str
tool_schema_digest: str
runtime_type: str
qualification_digest: str
def binding_fields(self) -> dict[str, str]:
"""Project only the reviewed installation identity, without reflective access."""
return {
"tenant_id": self.tenant_id,
"installation_id": self.installation_id,
"plugin_id": self.plugin_id,
"provider_id": self.provider_id,
"tool_name": self.tool_name,
"unique_identifier": self.unique_identifier,
"version": self.version,
"checksum": self.checksum,
"provider_schema_digest": self.provider_schema_digest,
"tool_schema_digest": self.tool_schema_digest,
"runtime_type": self.runtime_type,
"qualification_digest": self.qualification_digest,
}
@classmethod
def from_binding(cls, binding: QualifiedJsonToolBinding) -> InstalledJsonParse:
return cls(
tenant_id=binding.tenant_id,
installation_id=binding.installation_id,
plugin_id=binding.plugin_id,
provider_id=binding.provider_id,
tool_name=binding.tool_name,
unique_identifier=binding.unique_identifier,
version=binding.version,
checksum=binding.checksum,
provider_schema_digest=binding.provider_schema_digest,
tool_schema_digest=binding.tool_schema_digest,
runtime_type=binding.runtime_type,
qualification_digest=binding.qualification_digest,
)
@property
def binding_digest(self) -> str:
return canonical_digest(self.binding_fields())
def _validate_declaration(declaration: ToolProviderEntityWithPlugin) -> ToolEntity:
"""Validate the full normalized declaration before any native credential branch."""
if (
declaration.identity.name != PROVIDER_ID
or declaration.plugin_id is not None
or declaration.credentials_schema
or declaration.oauth_schema is not None
or canonical_digest(declaration.model_dump(mode="json")) != PROVIDER_SCHEMA_DIGEST
):
raise BuilderExecutionPolicyError("unsupported_json_tool_installation")
parses = [tool for tool in declaration.tools if tool.identity.name == TOOL_NAME]
if len(parses) != 1:
raise BuilderExecutionPolicyError("unsupported_json_tool_installation")
parse = parses[0]
if (
parse.identity.provider != PROVIDER_ID
or parse.has_runtime_parameters
or parse.output_schema
or canonical_digest(parse.model_dump(mode="json")) != TOOL_SCHEMA_DIGEST
):
raise BuilderExecutionPolicyError("unsupported_json_tool_installation")
return parse
def validate_current_json_controller(controller: object, tool: object, *, expected: InstalledJsonParse) -> None:
"""Check the exact controller/tool returned by ToolManager, including cached results."""
from core.tools.plugin_tool.provider import PluginToolProviderController
from core.tools.plugin_tool.tool import PluginTool
if (
type(controller) is not PluginToolProviderController
or type(tool) is not PluginTool
or controller.tenant_id != expected.tenant_id
or controller.plugin_id != expected.plugin_id
or controller.plugin_unique_identifier != expected.unique_identifier
or controller.need_credentials
):
raise BuilderExecutionPolicyError("json_tool_controller_mismatch")
_validate_declaration(controller.entity)
if (
tool.entity.identity.name != TOOL_NAME
or tool.entity.identity.provider != PROVIDER_ID
or tool.plugin_unique_identifier != expected.unique_identifier
or tool.tenant_id != expected.tenant_id
or canonical_digest(tool.entity.model_dump(mode="json")) != TOOL_SCHEMA_DIGEST
):
raise BuilderExecutionPolicyError("json_tool_controller_mismatch")
def resolve_installed_json_parse(*, tenant_id: str) -> InstalledJsonParse:
"""Read the installation and provider from their native owners, outside SQL."""
if type(tenant_id) is not str or not 1 <= len(tenant_id) <= 128:
raise BuilderExecutionPolicyError("installed_json_tool_unavailable")
try:
installations = PluginInstaller().fetch_plugin_installation_by_ids(tenant_id, [PLUGIN_ID])
provider: PluginToolProviderEntity = PluginToolManager().fetch_tool_provider(tenant_id, PROVIDER_ID)
except Exception:
raise BuilderExecutionPolicyError("installed_json_tool_unavailable") from None
try:
if len(installations) != 1:
raise BuilderExecutionPolicyError("installed_json_tool_unavailable")
installation = installations[0]
if (
installation.tenant_id != tenant_id
or installation.plugin_id != PLUGIN_ID
or installation.plugin_unique_identifier != UNIQUE_IDENTIFIER
or installation.version != VERSION
or installation.checksum != CHECKSUM
or installation.source != PluginInstallationSource.Marketplace
or provider.provider != NATIVE_PROVIDER_NAME
or provider.plugin_id != PLUGIN_ID
or provider.plugin_unique_identifier != UNIQUE_IDENTIFIER
):
raise BuilderExecutionPolicyError("unsupported_json_tool_installation")
if installation.runtime_type == "local":
qualification_digest = QUALIFICATION_DIGEST
elif installation.runtime_type == "serverless":
qualification_digest = SERVERLESS_QUALIFICATION_DIGEST
else:
raise BuilderExecutionPolicyError("unsupported_json_tool_installation")
_validate_declaration(provider.declaration)
return InstalledJsonParse(
tenant_id=tenant_id,
installation_id=installation.id,
plugin_id=PLUGIN_ID,
provider_id=PROVIDER_ID,
tool_name=TOOL_NAME,
unique_identifier=UNIQUE_IDENTIFIER,
version=VERSION,
checksum=CHECKSUM,
provider_schema_digest=PROVIDER_SCHEMA_DIGEST,
tool_schema_digest=TOOL_SCHEMA_DIGEST,
runtime_type=installation.runtime_type,
qualification_digest=qualification_digest,
)
except BuilderExecutionPolicyError:
raise
except Exception:
raise BuilderExecutionPolicyError("installed_json_tool_unavailable") from None
def require_same_json_installation(expected: QualifiedJsonToolBinding, actual: InstalledJsonParse) -> None:
"""Require the persisted expected identity to match this exact metadata read."""
if InstalledJsonParse.from_binding(expected) != actual:
raise BuilderExecutionPolicyError("json_tool_installation_changed")