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

338 lines
13 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
from flask import g, request
from flask.globals import request_ctx
from flask_restx import Resource
from pydantic import BaseModel, ConfigDict, ValidationError, model_validator
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
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, plugin_inner_api_only
from fields.base import ResponseModel
from libs.exception import RelayedHTTPError
from libs.helper import dump_response
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
class AgentConfigPromptUpdatePayload(_ConfigMutationRequest):
"""Replace the raw system prompt; missing/null text is never a clear request."""
model_config = ConfigDict(extra="forbid")
prompt: str
class AgentConfigPromptResponse(ResponseModel):
prompt: str
register_schema_models(inner_api_ns, AgentConfigPromptUpdatePayload)
register_response_schema_models(inner_api_ns, AgentConfigPromptResponse)
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 request_ctx.request 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)
@inner_api_ns.route("/agent-config/<string:agent_id>/prompt")
class AgentConfigPromptApi(Resource):
@setup_required
@plugin_inner_api_only
@inner_api_ns.doc("agent_config_prompt_get", params=query_params_from_model(_ConfigTargetQuery))
@inner_api_ns.response(200, "Raw system prompt", inner_api_ns.models[AgentConfigPromptResponse.__name__])
def get(self, agent_id: str):
try:
query = _target_query_from_request()
result = AgentConfigService().get_prompt(
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,
)
return dump_response(AgentConfigPromptResponse, result)
except ValidationError as exc:
raise RelayedHTTPError(status=400, error_code="invalid_request", message=str(exc)) from exc
except AgentConfigServiceError as exc:
raise RelayedHTTPError(status=exc.status_code, error_code=exc.code, message=exc.message) from exc
@setup_required
@plugin_inner_api_only
@inner_api_ns.doc("agent_config_prompt_put")
@inner_api_ns.expect(inner_api_ns.models[AgentConfigPromptUpdatePayload.__name__])
@inner_api_ns.response(200, "Saved system prompt", inner_api_ns.models[AgentConfigPromptResponse.__name__])
def put(self, agent_id: str):
try:
body = AgentConfigPromptUpdatePayload.model_validate(request.get_json(silent=True) or {})
result = AgentConfigService().update_prompt(
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,
prompt=body.prompt,
)
return dump_response(AgentConfigPromptResponse, result)
except ValidationError as exc:
raise RelayedHTTPError(status=400, error_code="invalid_request", message=str(exc)) from exc
except AgentConfigServiceError as exc:
raise RelayedHTTPError(status=exc.status_code, error_code=exc.code, message=exc.message) from exc