"""Pydantic request and response schemas for the Console Dify Builder API.""" from datetime import datetime from typing import Annotated, Any, Literal from uuid import UUID from pydantic import BaseModel, ConfigDict, Field, RootModel, StrictBool, StrictInt, StringConstraints, model_validator from core.dify_builder.contract import ( ActionKind, AssistantTurnItem, BuilderErrorCode, Decision, DecisionItem, ExecutionActivityState, ExecutionProgressStatus, FormCard, InteractionResponseItem, NoticeItem, Phase, PlanCard, PreflightContextCard, RecoveryRef, ResourceSelectCard, RunContextCard, RunStatus, SessionModel, TestResultCard, UserItem, ) from core.dify_builder.models import TaskSummary from core.dify_builder.real_call_consent import ConsentState from fields.base import ResponseModel from fields.workflow_stream_fields import WorkflowStreamPayload from runtime_contracts.builder.presentation import BuilderText from services.dify_builder.native_testing.public_contracts import NativeTestInputBinding, NativeTestPublicReceipt NonEmptyString = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] ClientTurnId = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=128)] # Real-call consent identifiers are bound byte-exactly by the consent owner: no trimming or coercion. RealCallId = Annotated[str, StringConstraints(min_length=1, max_length=128)] ScopeDigest = Annotated[str, StringConstraints(pattern=r"^[a-f0-9]{64}$")] IdempotencyKey = Annotated[str, StringConstraints(min_length=1, max_length=128)] ReasonCode = Annotated[str, StringConstraints(pattern=r"^[a-z][a-z0-9_]{0,127}$")] class DifyBuilderPayload(BaseModel): model_config = ConfigDict(extra="forbid", populate_by_name=True) class DifyBuilderAttachmentPayload(DifyBuilderPayload): """An opaque native upload reference; authority is reconstructed server-side.""" upload_file_id: UUID class DifyBuilderMaterialPayload(DifyBuilderPayload): attachments: list[DifyBuilderAttachmentPayload] = Field(default_factory=list, max_length=5) class DifyBuilderGoalPayload(DifyBuilderMaterialPayload): goal_text: Annotated[str, StringConstraints(strip_whitespace=True, max_length=20000)] = "" @model_validator(mode="after") def require_goal_or_material(self) -> "DifyBuilderGoalPayload": if not self.goal_text and not self.attachments: raise ValueError("goal_text or attachments are required") return self class DifyBuilderCreateBuildSessionPayload(DifyBuilderGoalPayload): app_id: NonEmptyString scenario: Literal["build"] # True when the app was created from this same prompt and still carries the # name cut from it, so Builder may rewrite it once it understands the goal. # False is create-from-blank: the user named the app, Builder inherits it. derive_app_name: bool = False model_config_data: SessionModel | None = Field(default=None, alias="model_config") class DifyBuilderCreateEditSessionPayload(DifyBuilderGoalPayload): app_id: NonEmptyString scenario: Literal["edit"] model_config_data: SessionModel | None = Field(default=None, alias="model_config") class DifyBuilderCreateFixSessionPayload(DifyBuilderMaterialPayload): app_id: NonEmptyString scenario: Literal["fix"] failed_run_id: NonEmptyString session_id: NonEmptyString | None = None client_action_id: ClientTurnId | None = None model_config_data: SessionModel | None = Field(default=None, alias="model_config") class DifyBuilderChecklistErrorPayload(DifyBuilderPayload): node_id: str node_type: str title: str messages: list[str] unconnected: bool plugin_missing: bool class DifyBuilderCreateChecklistFixSessionPayload(DifyBuilderMaterialPayload): app_id: NonEmptyString scenario: Literal["fix"] checklist_errors: Annotated[list[DifyBuilderChecklistErrorPayload], Field(min_length=1)] failed_run_id: NonEmptyString | None = None session_id: NonEmptyString | None = None client_action_id: ClientTurnId | None = None model_config_data: SessionModel | None = Field(default=None, alias="model_config") class DifyBuilderCreateSessionPayload( RootModel[ DifyBuilderCreateBuildSessionPayload | DifyBuilderCreateEditSessionPayload | DifyBuilderCreateFixSessionPayload | DifyBuilderCreateChecklistFixSessionPayload ] ): """Create a Build, Edit, failed-run Fix, or checklist Fix session.""" class DifyBuilderReceiptFieldSnapshot(DifyBuilderPayload): key: NonEmptyString label: str display_value: str class DifyBuilderReceiptSnapshot(DifyBuilderPayload): """Localized display only; submitted values remain in action.payload.""" interaction_kind: Literal["choice", "form", "resource"] reply_language: Annotated[str, StringConstraints(min_length=1, max_length=35)] # Display metadata must not impose limits absent from action.payload. question: str answer: str = "" fields: list[DifyBuilderReceiptFieldSnapshot] = Field(default_factory=list) class DifyBuilderTaskControlPayload(DifyBuilderPayload): """Control the current task with a durable client replay identity.""" base_version: StrictInt client_action_id: ClientTurnId base_app_revision: str = "" class DifyBuilderConversationLifecyclePayload(DifyBuilderPayload): """Lifecycle control uses Session CAS, never a graph revision.""" base_version: StrictInt = Field(ge=1) client_action_id: ClientTurnId expected_task_id: str = Field(default="", max_length=255) class DifyBuilderClearConversationPayload(DifyBuilderConversationLifecyclePayload): confirmed: StrictBool @model_validator(mode="after") def require_confirmation(self) -> "DifyBuilderClearConversationPayload": if self.confirmed is not True: raise ValueError("Explicit clear confirmation is required") return self class DifyBuilderFixIntentPayload(DifyBuilderPayload): base_version: StrictInt = Field(ge=1) client_action_id: ClientTurnId failed_run_id: NonEmptyString | None = None checklist_errors: list[DifyBuilderChecklistErrorPayload] = Field(default_factory=list, max_length=100) model_config_data: SessionModel | None = Field(default=None, alias="model_config") @model_validator(mode="after") def require_one_source(self) -> "DifyBuilderFixIntentPayload": if bool(self.failed_run_id) == bool(self.checklist_errors): raise ValueError("Exactly one failed run or checklist source is required") return self class DifyBuilderFixControlQuery(DifyBuilderPayload): operation: Literal["fix", "dismiss_fix", "stop_and_switch_fix"] client_action_id: ClientTurnId base_version: int = Field(ge=1) fix_intent_id: NonEmptyString | None = None expected_task_id: NonEmptyString | None = None @model_validator(mode="after") def require_decision_identity(self) -> "DifyBuilderFixControlQuery": if self.operation != "fix" and (not self.fix_intent_id or not self.expected_task_id): raise ValueError("Fix decision identity is required") return self class DifyBuilderFixControlResponse(ResponseModel): session_id: str operation: Literal["fix", "dismiss_fix", "stop_and_switch_fix"] client_action_id: str base_version: int fix_intent_id: str target_task_id: str status: str acknowledged: bool class DifyBuilderConversationLifecycleResponse(ResponseModel): session_id: str version: int last_command_id: str task_id: str task_status: Literal["abandoned"] conversation_cleared: bool conversation_last_seq: int settlement: Literal["settled", "pending", "unknown"] class DifyBuilderSubmitActionPayload(DifyBuilderPayload): action_id: NonEmptyString client_action_id: ClientTurnId | None = None receipt_snapshot: DifyBuilderReceiptSnapshot | None = None payload: dict[str, Any] = Field( default_factory=dict, description=( "Existing action payload. Native controls: confirm_test_operation/deny_test_operation take " "request_id,pending_operation_id,pending_version,scope_digest; resume_test takes request_id," "expected_generation; cancel_test takes request_id. Copy payload from the current server action." ), ) base_version: StrictInt base_app_revision: NonEmptyString class DifyBuilderSubmitMessagePayload(DifyBuilderMaterialPayload): text: Annotated[str, StringConstraints(strip_whitespace=True, max_length=20000)] = "" base_version: StrictInt client_turn_id: ClientTurnId @model_validator(mode="after") def require_text_or_material(self) -> "DifyBuilderSubmitMessagePayload": if not self.text and not self.attachments: raise ValueError("text or attachments are required") return self class DifyBuilderConversationListQuery(DifyBuilderPayload): before_seq: int | None = Field(default=None, ge=0, description="Load groups before this sequence") after_seq: int | None = Field(default=None, ge=-1, description="Load groups after this sequence") limit: int = Field(default=20, ge=1, le=100, description="Number of conversation groups to return") @model_validator(mode="after") def validate_cursor(self) -> "DifyBuilderConversationListQuery": if self.before_seq is not None and self.after_seq is not None: raise ValueError("before_seq and after_seq are mutually exclusive") return self # Real-call consent requests name what a person reviews or approves. They never # carry Tool parameters, credentials, graph, execution context or guards: the # consent owner reconstructs all of those server-side from authoritative rows. class DifyBuilderPrepareRealCallPayload(DifyBuilderPayload): node_id: RealCallId test_input_id: RealCallId credential_id: RealCallId at_version: StrictInt = Field(ge=1) base_app_revision: RealCallId idempotency_key: IdempotencyKey class DifyBuilderConfirmRealCallPayload(DifyBuilderPayload): decision: Literal["confirm", "decline"] scope_digest: ScopeDigest at_version: StrictInt = Field(ge=1) idempotency_key: IdempotencyKey class DifyBuilderExecuteRealCallPayload(DifyBuilderPayload): scope_digest: ScopeDigest at_version: StrictInt = Field(ge=1) idempotency_key: IdempotencyKey class DifyBuilderConversationItemBase(ResponseModel): seq: int at_version: int class DifyBuilderUserConversationItemResponse(DifyBuilderConversationItemBase): kind: Literal["user"] payload: UserItem class DifyBuilderDecisionConversationItemResponse(DifyBuilderConversationItemBase): kind: Literal["decision"] payload: DecisionItem class DifyBuilderInteractionResponseConversationItemResponse(DifyBuilderConversationItemBase): kind: Literal["interaction_response"] payload: InteractionResponseItem class DifyBuilderNoticeConversationItemResponse(DifyBuilderConversationItemBase): kind: Literal["notice"] payload: NoticeItem class DifyBuilderRunContextConversationItemResponse(DifyBuilderConversationItemBase): kind: Literal["run_context"] payload: RunContextCard class DifyBuilderPreflightContextConversationItemResponse(DifyBuilderConversationItemBase): kind: Literal["preflight_context"] payload: PreflightContextCard class DifyBuilderAssistantTurnConversationItemResponse(DifyBuilderConversationItemBase): kind: Literal["assistant_turn"] payload: AssistantTurnItem class DifyBuilderPlanConversationItemResponse(DifyBuilderConversationItemBase): kind: Literal["plan"] payload: PlanCard class DifyBuilderFormConversationItemResponse(DifyBuilderConversationItemBase): kind: Literal["form"] payload: FormCard class DifyBuilderResourceSelectConversationItemResponse(DifyBuilderConversationItemBase): kind: Literal["resource_select"] payload: ResourceSelectCard class DifyBuilderTestResultConversationItemResponse(DifyBuilderConversationItemBase): kind: Literal["test_result"] payload: TestResultCard DifyBuilderConversationItem = Annotated[ DifyBuilderUserConversationItemResponse | DifyBuilderDecisionConversationItemResponse | DifyBuilderInteractionResponseConversationItemResponse | DifyBuilderNoticeConversationItemResponse | DifyBuilderRunContextConversationItemResponse | DifyBuilderPreflightContextConversationItemResponse | DifyBuilderAssistantTurnConversationItemResponse | DifyBuilderPlanConversationItemResponse | DifyBuilderFormConversationItemResponse | DifyBuilderResourceSelectConversationItemResponse | DifyBuilderTestResultConversationItemResponse, Field(discriminator="kind"), ] # Complete assistant presentation arrives as a committed item. Native streamed # assistant text is restricted to agent_message by the event validator. DifyBuilderAppendedConversationItem = Annotated[ DifyBuilderUserConversationItemResponse | DifyBuilderDecisionConversationItemResponse | DifyBuilderInteractionResponseConversationItemResponse | DifyBuilderNoticeConversationItemResponse | DifyBuilderRunContextConversationItemResponse | DifyBuilderPreflightContextConversationItemResponse | DifyBuilderAssistantTurnConversationItemResponse | DifyBuilderPlanConversationItemResponse | DifyBuilderFormConversationItemResponse | DifyBuilderResourceSelectConversationItemResponse | DifyBuilderTestResultConversationItemResponse, Field(discriminator="kind"), ] class DifyBuilderActiveInteractionResponse(ResponseModel): action_id: str card_seq: int valid_at_version: int class DifyBuilderActionResponse(ResponseModel): payload: dict[str, Any] = Field(default_factory=dict) id: str label: BuilderText kind: ActionKind class DifyBuilderAppRevisionResponse(ResponseModel): current: str conflicted: bool class DifyBuilderAttachmentStateResponse(ResponseModel): upload_file_id: UUID name: str extension: str size: int status: Literal["pending", "consumed", "truncated", "rejected"] digest: str = "" reason: str = "" class DifyBuilderMaterialReceiptResponse(ResponseModel): id: UUID seq: int app_id: str session_id: str task_id: str generation: int client_turn_id: str status: Literal["pending", "consumed", "truncated", "rejected", "superseded"] consumed_version: int | None test_input_candidate: bool attachments: list[DifyBuilderAttachmentStateResponse] = Field(max_length=5) class DifyBuilderSessionStateResponse(ResponseModel): attachments: list[DifyBuilderMaterialReceiptResponse] = Field(default_factory=list, max_length=100) test_execution: NativeTestPublicReceipt | None = None diagnostics: list[dict[str, Any]] = Field(default_factory=list, max_length=33) session_id: str reply_language: str = Field( description="Detected user-input language (BCP-47); empty until a user language is detected." ) version: int canvas_read_only: bool run_status: RunStatus command_pending: bool = False interrupted: bool conversation_last_seq: int phase: Phase actions: list[DifyBuilderActionResponse] = Field(default_factory=list) decision: Decision | None = None active_interaction: DifyBuilderActiveInteractionResponse | None = None recovery: RecoveryRef | None = None model: SessionModel | None = None app_revision: DifyBuilderAppRevisionResponse | None = None task_id: str = "" task_status: Literal["active", "paused", "completed", "abandoned"] = "active" task_summaries: list[TaskSummary] = Field(default_factory=list, max_length=8) conversation_cleared: bool = False lifecycle_settlement: Literal["settled", "pending", "unknown"] | None = Field( default=None, description=( "Known execution state of the current clear/abandon control, not physical cleanup proof. " "Null means no applicable control; missing applicable evidence is unknown. " "Clear retains its durable acknowledgment state without inferring later cleanup." ), ) pending_fix_context: RunContextCard | PreflightContextCard | None = None class DifyBuilderSessionViewResponse(DifyBuilderSessionStateResponse): last_command_id: str = "" class DifyBuilderCreatedSessionResponse(ResponseModel): session: DifyBuilderSessionViewResponse requires_start: bool class DifyBuilderCommandSubmissionResponse(ResponseModel): command_id: str pending: bool session: DifyBuilderSessionViewResponse | None = None class DifyBuilderFixIntentResponse(ResponseModel): fix_intent_id: str status: str session: DifyBuilderSessionViewResponse class DifyBuilderConversationPageResponse(ResponseModel): data: list[DifyBuilderConversationItem] has_more: bool first_seq: int | None last_seq: int | None class DifyBuilderCommandFinishedEventData(DifyBuilderSessionStateResponse): command_id: str post_canvas_action_id: str | None = None class DifyBuilderCommandStartedEventData(ResponseModel): session_id: str command_id: str version: int phase: Phase run_status: RunStatus class DifyBuilderCommandStartedEventResponse(ResponseModel): event: Literal["command_started"] data: DifyBuilderCommandStartedEventData class DifyBuilderWorkflowEventData(ResponseModel): session_id: str operation_id: str at_version: int revision: int payload: WorkflowStreamPayload class DifyBuilderWorkflowEventResponse(ResponseModel): event: Literal["workflow"] data: DifyBuilderWorkflowEventData class DifyBuilderCanvasEventBase(ResponseModel): session_id: str operation_id: str at_version: int revision: int class DifyBuilderGraphChangedEventData(DifyBuilderCanvasEventBase): event: Literal["graph_changed"] cause: Literal["apply", "restore"] class DifyBuilderEditTargetsChangedEventData(DifyBuilderCanvasEventBase): event: Literal["edit_targets_changed"] node_ids: list[str] class DifyBuilderPublishedEventData(DifyBuilderCanvasEventBase): event: Literal["publish_workflow"] DifyBuilderCanvasEventData = Annotated[ DifyBuilderGraphChangedEventData | DifyBuilderEditTargetsChangedEventData | DifyBuilderPublishedEventData, Field(discriminator="event"), ] class DifyBuilderCanvasEventResponse(ResponseModel): event: Literal["canvas"] data: DifyBuilderCanvasEventData class DifyBuilderExecutionActivityResponse(ResponseModel): id: str label: BuilderText state: ExecutionActivityState parent_id: str | None = None class DifyBuilderExecutionProgressResponse(ResponseModel): status: ExecutionProgressStatus activities: list[DifyBuilderExecutionActivityResponse] = Field(default_factory=list) class DifyBuilderAgentMessageEventData(ResponseModel): session_id: str command_id: str operation_id: str turn_id: str delta: str seq: int at_version: int revision: int done: bool text_bytes: int execution: DifyBuilderExecutionProgressResponse | None = None cards: list[str] = Field(default_factory=list) class DifyBuilderAgentMessageEventResponse(ResponseModel): event: Literal["agent_message"] data: DifyBuilderAgentMessageEventData class DifyBuilderConversationItemAppendedEventData(ResponseModel): session_id: str command_id: str item: DifyBuilderAppendedConversationItem @model_validator(mode="after") def complete_assistant_only(self): if self.item.kind == "assistant_turn" and self.item.payload.reply_mode != "complete": raise ValueError("streamed assistant turns use agent_message") return self class DifyBuilderConversationItemAppendedEventResponse(ResponseModel): event: Literal["conversation_item_appended"] data: DifyBuilderConversationItemAppendedEventData class DifyBuilderReasoningEventData(ResponseModel): session_id: str operation_id: str at_version: int revision: int delta: str class DifyBuilderReasoningEventResponse(ResponseModel): event: Literal["reasoning"] data: DifyBuilderReasoningEventData class DifyBuilderProgressEventData(ResponseModel): session_id: str operation_id: str at_version: int revision: int status: ExecutionProgressStatus activity: DifyBuilderExecutionActivityResponse | None reply_language: str = "" class DifyBuilderProgressEventResponse(ResponseModel): event: Literal["progress"] data: DifyBuilderProgressEventData class DifyBuilderProgressSnapshotEventData(ResponseModel): """Complete observed activity state before subsequent live progress deltas.""" session_id: str operation_id: str at_version: int revision: int reply_language: str execution: DifyBuilderExecutionProgressResponse class DifyBuilderProgressSnapshotEventResponse(ResponseModel): event: Literal["progress_snapshot"] data: DifyBuilderProgressSnapshotEventData class DifyBuilderCommandFinishedEventResponse(ResponseModel): event: Literal["command_finished"] data: DifyBuilderCommandFinishedEventData class DifyBuilderSessionReadyEventResponse(ResponseModel): event: Literal["session_ready"] data: DifyBuilderSessionViewResponse class DifyBuilderSessionStateEventResponse(ResponseModel): event: Literal["session_state"] data: DifyBuilderSessionViewResponse class DifyBuilderErrorEventData(ResponseModel): session_id: str | None = None command_id: str | None = None code: str | None = None message: str reason_code: ReasonCode | None = None recoverable: bool | None = None class DifyBuilderErrorEventResponse(ResponseModel): event: Literal["error"] data: DifyBuilderErrorEventData class DifyBuilderNativeExecutionEventData(ResponseModel): session_id: str cursor: ScopeDigest receipt: NativeTestPublicReceipt class DifyBuilderNativeExecutionEventResponse(ResponseModel): event: Literal["native_execution"] data: DifyBuilderNativeExecutionEventData class DifyBuilderStreamEventResponse( RootModel[ Annotated[ DifyBuilderCommandStartedEventResponse | DifyBuilderWorkflowEventResponse | DifyBuilderCanvasEventResponse | DifyBuilderAgentMessageEventResponse | DifyBuilderConversationItemAppendedEventResponse | DifyBuilderReasoningEventResponse | DifyBuilderProgressEventResponse | DifyBuilderProgressSnapshotEventResponse | DifyBuilderCommandFinishedEventResponse | DifyBuilderSessionReadyEventResponse | DifyBuilderSessionStateEventResponse | DifyBuilderErrorEventResponse | DifyBuilderNativeExecutionEventResponse, Field(discriminator="event"), ] ] ): """One JSON object carried by an SSE ``data:`` frame.""" class DifyBuilderErrorResponse(ResponseModel): reason_code: ReasonCode | None = None code: BuilderErrorCode message: str | None = None recoverable: bool | None = None class DifyBuilderRealCallDisclosureResponse(ResponseModel): """The approved public disclosure of one real Tool call; no credential revision or secret.""" action: Literal["get_message"] credential_id: str credential_alias: str credential_type: str identity_basis: Literal["workspace_credential"] = Field( description="The alias names a workspace credential, not a verified remote account identity" ) remote_account_status: Literal["unknown"] message_id: str request_method: Literal["GET"] request_url: str response_format: Literal["full"] include_body: bool include_attachments: bool oauth_refresh: Literal["if_needed"] = Field( description=( "One OAuth token refresh request is made only if the stored access token must be refreshed " "before the call; otherwise no refresh request is sent" ) ) oauth_refresh_method: Literal["POST"] oauth_refresh_url: Literal["https://oauth2.googleapis.com/token"] native_attempt_limit: Literal[1] refresh_limit: Literal[1] automatic_replay: Literal[False] quota_usage: Literal["possible"] monetary_estimate: Literal["unknown"] output_retention: Literal["native_run_retention"] data_description: Literal["gmail_message_headers_snippet_and_selected_body_or_attachment_metadata"] expires_at: datetime prior_uncertain_consent_id: str | None = None class DifyBuilderRealCallConsentResponse(ResponseModel): """Current or saved receipt of one real-call consent.""" # The discriminator is always emitted, so document it as required. model_config = ConfigDict(json_schema_serialization_defaults_required=True) result: Literal["receipt"] = "receipt" consent_id: str scope_digest: str state: ConsentState = Field( description=( "Consent lifecycle state. 'unknown' is final: a claimed call whose real effect could not be " "confirmed. It is bounded by the disclosed limits of one native attempt and at most one token " "refresh, is never replayed automatically, and any further real call needs a new human-approved " "consent" ) ) at_version: int disclosure: DifyBuilderRealCallDisclosureResponse command_id: str | None execution_request_id: str | None native_run_id: str | None native_node_execution_id: str | None reason_code: ReasonCode | None = Field(description="Fixed server reason code; never provider text") refresh_claimed: bool business_claimed: bool can_retry: bool = Field( description="Whether a new consent may be prepared; always false once a launch is reserved or claimed" ) DifyBuilderRealCallRefusalCode = Literal[ "needs_setup", "needs_permission", "unsupported_action", "unavailable_metadata", "conflict" ] DifyBuilderRealCallRefusalReason = Literal[ # needs_setup "needs_setup", "credential_required", "credential_binding_mismatch", "credential_provider_mismatch", "unsupported_test_input", # needs_permission "credential_reference_unavailable", "credential_policy_refused", # unsupported_action "unsupported_action", "unsupported_tool_node", "unsupported_tool_parameters", "unsupported_tool_configuration", "unsafe_message_id", "unsupported_session_state", "unsupported_graph", "unsupported_workflow", "unsupported_node_retry", "unsupported_default_output", "unsupported_selector", "unsupported_node_implementation", "unsupported_node_configuration", "invalid_node_configuration", "unsupported_start_input", "unsupported_input_value", "unsupported_features", "unsupported_variables", "unsupported_external_tracing", # unavailable_metadata "installed_tool_unavailable", "invalid_graph_metadata", "session_state_unavailable", "real_call_unavailable", # conflict "stale_session_version", "stale_app_revision", "stale_consent_version", "stale_execution_revision", "test_input_binding_mismatch", "consent_scope_mismatch", "consent_scope_changed", "consent_invalidated", "consent_expired", "consent_not_prepared", "consent_not_confirmed", "consent_already_claimed", "consent_decision_conflict", "idempotency_conflict", "execution_already_launched", "command_lineage_changed", "session_busy", "session_conflict", ] class DifyBuilderRealCallRefusalResponse(ResponseModel): """Sanitized refusal of a real-call consent operation; carries fixed codes only.""" model_config = ConfigDict(json_schema_serialization_defaults_required=True) result: Literal["refusal"] = "refusal" code: DifyBuilderRealCallRefusalCode reason_code: DifyBuilderRealCallRefusalReason class DifyBuilderRealCallPermissionRefusalResponse(ResponseModel): """Execution permission refused after ownership was established; nothing foreign is disclosed.""" model_config = ConfigDict(json_schema_serialization_defaults_required=True) result: Literal["refusal"] = "refusal" code: Literal["needs_permission"] reason_code: Literal["execution_permission_denied"] class DifyBuilderFeatureUnavailableResponse(ResponseModel): """Builder is not enabled for the workspace (the shared ``dify_builder_required`` gate).""" code: Literal["feature_unavailable"] class DifyBuilderConsoleForbiddenResponse(ResponseModel): """Console permission-guard denial as serialized by the shared console HTTP error handler.""" code: Literal["forbidden"] message: str status: Literal[403] class DifyBuilderRealCallForbiddenResponse( RootModel[ Annotated[ DifyBuilderRealCallPermissionRefusalResponse | DifyBuilderFeatureUnavailableResponse | DifyBuilderConsoleForbiddenResponse, Field(discriminator="code"), ] ] ): """Every HTTP 403 body a real-call consent route can return.""" class DifyBuilderNativeTestResponse(ResponseModel): """Strict public native receipt; domain DTO also owns generated schemas.""" data: NativeTestPublicReceipt class DifyBuilderNativeTestInputResponse(ResponseModel): data: NativeTestInputBinding