Files
dify/api/controllers/inner_api/plugin/agent_config.py
T

277 lines
9.9 KiB
Python

"""Inner API for Agent Soul-backed config assets.
These endpoints are called by the dify-agent server with the inner API key.
They resolve the requested Agent config version directly from Agent Soul JSON
and authorize downloads with the existing Config target, tenant, and source
ownership semantics. Download requests return metadata plus a short-lived,
origin-free ``/files/*`` URI, never file bytes; the Sandbox fetches those bytes
directly from the Dify API data plane.
"""
from __future__ import annotations
from typing import Literal, cast
from flask import g, request
from flask_restx import Resource
from pydantic import BaseModel, ConfigDict, ValidationError, model_validator
from werkzeug.local import LocalProxy
from controllers.console.wraps import setup_required
from controllers.inner_api import inner_api_ns
from controllers.inner_api.wraps import AgentRuntimeRefused, agent_endpoint_api_only
from models.agent_config_entities import validate_config_name, validate_config_skill_name
from services.agent_config_service import (
AgentConfigService,
AgentConfigServiceError,
AgentConfigVersionKind,
ConfigPushPayload,
)
class _ConfigTargetQuery(BaseModel):
tenant_id: str
user_id: str | None = None
config_version_id: str
config_version_kind: AgentConfigVersionKind
class _ConfigMutationRequest(BaseModel):
tenant_id: str
user_id: str
config_version_id: str
config_version_kind: AgentConfigVersionKind
class _ConfigDownloadSource(BaseModel):
model_config = ConfigDict(extra="forbid")
kind: Literal["file", "skill"]
name: str
@model_validator(mode="after")
def validate_name(self) -> _ConfigDownloadSource:
self.name = validate_config_skill_name(self.name) if self.kind == "skill" else validate_config_name(self.name)
return self
class _ConfigDownloadRequest(_ConfigTargetQuery):
model_config = ConfigDict(extra="forbid")
config: _ConfigDownloadSource
class _ConfigPushRequest(_ConfigMutationRequest):
files: list[dict] = []
skills: list[dict] = []
env_text: str | None = None
note: str | None = None
def to_payload(self) -> ConfigPushPayload:
return ConfigPushPayload.model_validate(
{
"files": self.files,
"skills": self.skills,
"env_text": self.env_text,
"note": self.note,
}
)
class _ConfigEnvUpdateRequest(_ConfigMutationRequest):
env_text: str
class _ConfigNoteUpdateRequest(_ConfigMutationRequest):
note: str
def _target_query_from_request() -> _ConfigTargetQuery:
return _ConfigTargetQuery.model_validate(
{
"tenant_id": request.args.get("tenant_id"),
"user_id": request.args.get("user_id"),
"config_version_id": request.args.get("config_version_id"),
"config_version_kind": request.args.get("config_version_kind"),
}
)
def _error_response(exc: AgentConfigServiceError) -> tuple[dict[str, str], int]:
return {"code": exc.code, "message": exc.message}, exc.status_code
def _authorize_runtime_config_read(
agent_id: str, query: _ConfigTargetQuery, source: dict[str, str] | None = None
) -> None:
"""Spend no second permit; revalidate the claimed parent operation before reads."""
context = (
g.get("agent_runtime_context")
if g.get("agent_runtime_request") is cast(LocalProxy, request)._get_current_object()
else None
)
if context is None:
return
from runtime_contracts.builder.execution import BuilderExecutionPolicyError
from services.dify_builder.native_testing.agent_config_access import validate_config_read
from services.dify_builder.native_testing.agent_operations import agent_operations_for_ref
try:
envelope = g.get("agent_runtime_dispatch")
if envelope is None:
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
validate_config_read(
context=context,
envelope=envelope,
operations=agent_operations_for_ref(context.ref),
agent_id=agent_id,
tenant_id=query.tenant_id,
user_id=query.user_id,
config_version_id=query.config_version_id,
config_version_kind=query.config_version_kind,
source=source,
)
except BuilderExecutionPolicyError:
raise AgentRuntimeRefused() from None
@inner_api_ns.route("/agent-config/<string:agent_id>/manifest")
class AgentConfigManifestApi(Resource):
@setup_required
@agent_endpoint_api_only
@inner_api_ns.doc("agent_config_manifest")
def get(self, agent_id: str):
try:
query = _target_query_from_request()
_authorize_runtime_config_read(agent_id, query)
return AgentConfigService().manifest(
tenant_id=query.tenant_id,
agent_id=agent_id,
user_id=query.user_id,
config_version_id=query.config_version_id,
config_version_kind=query.config_version_kind,
)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except AgentConfigServiceError as exc:
return _error_response(exc)
@inner_api_ns.route("/agent-config/<string:agent_id>/download-request")
class AgentConfigDownloadRequestApi(Resource):
@setup_required
@agent_endpoint_api_only
@inner_api_ns.doc("agent_config_download_request")
def post(self, agent_id: str):
try:
body = _ConfigDownloadRequest.model_validate(request.get_json(silent=True) or {})
_authorize_runtime_config_read(agent_id, body, body.config.model_dump())
result = AgentConfigService().request_download(
tenant_id=body.tenant_id,
agent_id=agent_id,
user_id=body.user_id,
config_version_id=body.config_version_id,
config_version_kind=body.config_version_kind,
kind=body.config.kind,
name=body.config.name,
)
return {
"filename": result.filename,
"mime_type": result.mime_type,
"size": result.size,
"download_uri": result.download_uri,
}
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except AgentConfigServiceError as exc:
return _error_response(exc)
@inner_api_ns.route("/agent-config/<string:agent_id>/skills/<string:name>/inspect")
class AgentConfigSkillInspectApi(Resource):
@setup_required
@agent_endpoint_api_only
@inner_api_ns.doc("agent_config_skill_inspect")
def get(self, agent_id: str, name: str):
try:
query = _target_query_from_request()
_authorize_runtime_config_read(agent_id, query, {"kind": "skill", "name": name})
return AgentConfigService().inspect_skill(
tenant_id=query.tenant_id,
agent_id=agent_id,
user_id=query.user_id,
config_version_id=query.config_version_id,
config_version_kind=query.config_version_kind,
name=name,
)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except AgentConfigServiceError as exc:
return _error_response(exc)
@inner_api_ns.route("/agent-config/<string:agent_id>/push")
class AgentConfigPushApi(Resource):
@setup_required
@agent_endpoint_api_only
@inner_api_ns.doc("agent_config_push")
def post(self, agent_id: str):
try:
body = _ConfigPushRequest.model_validate(request.get_json(silent=True) or {})
return AgentConfigService().push(
tenant_id=body.tenant_id,
agent_id=agent_id,
user_id=body.user_id,
config_version_id=body.config_version_id,
config_version_kind=body.config_version_kind,
payload=body.to_payload(),
)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except AgentConfigServiceError as exc:
return _error_response(exc)
@inner_api_ns.route("/agent-config/<string:agent_id>/env")
class AgentConfigEnvApi(Resource):
@setup_required
@agent_endpoint_api_only
@inner_api_ns.doc("agent_config_env")
def patch(self, agent_id: str):
try:
body = _ConfigEnvUpdateRequest.model_validate(request.get_json(silent=True) or {})
return AgentConfigService().update_env(
tenant_id=body.tenant_id,
agent_id=agent_id,
user_id=body.user_id,
config_version_id=body.config_version_id,
config_version_kind=body.config_version_kind,
env_text=body.env_text,
)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except AgentConfigServiceError as exc:
return _error_response(exc)
@inner_api_ns.route("/agent-config/<string:agent_id>/note")
class AgentConfigNoteApi(Resource):
@setup_required
@agent_endpoint_api_only
@inner_api_ns.doc("agent_config_note")
def put(self, agent_id: str):
try:
body = _ConfigNoteUpdateRequest.model_validate(request.get_json(silent=True) or {})
return AgentConfigService().update_note(
tenant_id=body.tenant_id,
agent_id=agent_id,
user_id=body.user_id,
config_version_id=body.config_version_id,
config_version_kind=body.config_version_kind,
note=body.note,
)
except ValidationError as exc:
return {"code": "invalid_request", "message": str(exc)}, 400
except AgentConfigServiceError as exc:
return _error_response(exc)