"""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//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//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//skills//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//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//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//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//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