Files
dify/api/core/dify_builder/verification.py
T
twwu 89504b7dce refactor(builder): localize fixed copy on the frontend
Move fixed copy into the difyBuilder i18n namespace and resolve scene IDs
using reply_language. Preserve dynamic streaming, historical receipts,
and submitted values.

Show fixed replies immediately with a short transition, detect language
before initial progress, and retire new notices and recovery messages.
Keep recovery and publication validation in place.

Update generated contracts and regression coverage, including progress
snapshot and execution context expectations and test type annotations.
2026-10-14 17:10:20 +08:00

183 lines
8.8 KiB
Python

"""Pure publication policy and execution presentation; no native graph interpretation."""
from core.dify_builder.contract import TestResultCard
from core.dify_builder.models import PublicationDecision, Run
from services.dify_builder.copy_ids import BuilderCopyId
from services.dify_builder.execution_messages import FALLBACK, execution_refusal_message
from services.dify_builder.json_execution_presentation import json_execution_blocks_repair, json_failed_reason
from services.dify_builder.presentation import copy_text
REVIEW_NOTE = copy_text(BuilderCopyId.REPLIES_OTHER_PATHS_AND_GOAL_ACCEPTANCE_WERE_NOT_VERIFIED)
SUCCESS_REPLY = copy_text(BuilderCopyId.LABELS_EXECUTION_SUCCEEDED_OUTPUT_NEEDS_REVIEW)
def is_execution_policy_blocker(run: Run) -> bool:
"""Known policy denials and incomplete execution proof cannot enter repair."""
if run.execution_refusal is not None:
return True
summary = run.verification.execution_evidence if run.verification else None
if summary is None:
return run.kind == "verify"
return (
bool(summary.blocked_node_ids)
or not summary.sealed
or json_execution_blocks_repair(summary, run.status)
or summary.safety_outcome
in {"unsupported_safe_execution", "execution_blocked", "execution_evidence_unknown", "simulation_completed"}
)
def publication_decision(
run: Run | None, *, session_id: str, current_revision: str, current_graph_revision: str
) -> PublicationDecision:
if run is None or run.session_id != session_id or run.kind != "verify" or not run.immutable:
return PublicationDecision(allowed=False, reason="no_verified_run")
summary = run.verification.execution_evidence if run.verification else None
if run.execution_refusal is not None or (summary and summary.blocked_node_ids):
return PublicationDecision(allowed=False, reason="execution_policy_blocked")
if summary is None or not summary.sealed or not summary.native_run_id or summary.native_run_id != run.dify_run_id:
return PublicationDecision(allowed=False, reason="execution_provenance_unbound")
if summary.safety_outcome in {"unsupported_safe_execution", "execution_blocked"}:
return PublicationDecision(allowed=False, reason="execution_policy_blocked")
if summary.safety_outcome == "execution_evidence_unknown":
return PublicationDecision(allowed=False, reason="execution_evidence_unknown")
if summary.safety_outcome == "simulation_completed":
return PublicationDecision(allowed=False, reason="simulation_only")
restricted_success = summary.safety_outcome == "restricted_execution_completed" and summary.mode == "restricted"
# Confirmation is not publication approval: only the sealed exact-revision
# confirmed success passes, and every check below still applies.
confirmed_success = (
summary.safety_outcome == "confirmed_tool_completed"
and summary.mode == "confirmed_tool"
and summary.policy_version == "builder-confirmed-gmail-read-v1"
and summary.action_state == "completed"
and summary.consent_id is not None
)
if not (restricted_success or confirmed_success):
return PublicationDecision(allowed=False, reason="execution_failed")
if run.status == "failed":
return PublicationDecision(allowed=False, reason="execution_failed")
if run.status != "succeeded":
return PublicationDecision(allowed=False, reason="execution_unknown")
evidence = run.verification
if (
evidence is None
or not evidence.execution_revision
or not evidence.executed_graph_revision
or not current_revision
or not current_graph_revision
):
return PublicationDecision(allowed=False, reason="revision_unbound")
if evidence.execution_revision != current_revision or evidence.executed_graph_revision != current_graph_revision:
return PublicationDecision(allowed=False, reason="stale_revision")
if any(f.state == "unresolved" for f in evidence.output_findings):
return PublicationDecision(allowed=False, reason="required_output_unresolved")
if evidence.no_output_dead_branch:
return PublicationDecision(allowed=False, reason="dead_branch_without_output")
return PublicationDecision(allowed=True, reason="eligible")
def has_output_blocker(run: Run) -> bool:
evidence = run.verification
return evidence is not None and (
evidence.no_output_dead_branch or any(f.state == "unresolved" for f in evidence.output_findings)
)
CONFIRMED_COMPLETED_NOTE = copy_text(BuilderCopyId.CONFIRMED_COMPLETED_NOTE)
CONFIRMED_FAILED_REASON = copy_text(BuilderCopyId.CONFIRMED_FAILED)
CONFIRMED_UNKNOWN_REASON = copy_text(BuilderCopyId.CONFIRMED_UNKNOWN)
def _confirmed_result_card(run: Run) -> TestResultCard:
"""IDs, statuses, counts, reason codes and the Run link only; never native content or error text."""
evidence = run.verification
summary = evidence.execution_evidence if evidence else None
assert summary is not None
completed = summary.safety_outcome == "confirmed_tool_completed" and run.status == "succeeded"
card = TestResultCard(
status="succeeded" if completed else "failed",
dify_run_id=run.dify_run_id,
review_note=CONFIRMED_COMPLETED_NOTE,
terminal_outputs=None,
executed_node_ids=list(evidence.executed_node_ids) if evidence else [],
execution_mode=summary.mode,
safety_outcome=summary.safety_outcome,
policy_version=summary.policy_version,
consent_id=summary.consent_id,
action_state=summary.action_state,
action_reason_code=summary.action_reason_code,
)
if completed and has_output_blocker(run):
card.status = "failed"
card.outcome = "required_output_unresolved"
card.failure_reason = copy_text(BuilderCopyId.REPLIES_A_REQUIRED_OUTPUT_REFERENCE_WAS_UNRESOLVED)
elif completed:
card.outcome = "execution_succeeded_needs_review"
elif summary.safety_outcome == "native_failed":
card.outcome = "execution_failed"
card.failure_reason = CONFIRMED_FAILED_REASON
else:
card.outcome = "execution_unknown"
card.failure_reason = CONFIRMED_UNKNOWN_REASON
return card
def result_card(run: Run) -> TestResultCard:
evidence = run.verification
summary = evidence.execution_evidence if evidence else None
if summary is not None and summary.mode == "confirmed_tool":
return _confirmed_result_card(run)
card = TestResultCard(
status="succeeded" if run.status == "succeeded" else "failed",
dify_run_id=run.dify_run_id,
review_note=REVIEW_NOTE,
terminal_outputs=evidence.terminal_outputs if evidence else None,
executed_node_ids=evidence.executed_node_ids if evidence else [],
)
summary = evidence.execution_evidence if evidence else None
if summary:
card.execution_mode = summary.mode
card.safety_outcome = summary.safety_outcome
card.policy_version = summary.policy_version
card.simulated_node_ids = list(summary.simulated_node_ids)
card.blocked_node_ids = list(summary.blocked_node_ids)
elif run.execution_refusal:
card.safety_outcome = run.execution_refusal.safety_outcome
else:
card.safety_outcome = "execution_evidence_unknown"
if is_execution_policy_blocker(run):
failed_reason = json_failed_reason(summary, run.status)
card.outcome = "execution_failed" if failed_reason else "execution_unknown"
card.failure_reason = (
execution_refusal_message(run.execution_refusal.reason_code)
if run.execution_refusal
else failed_reason or FALLBACK
)
if failed_reason:
card.terminal_outputs = None
if run.execution_refusal is not None:
card.terminal_outputs = None
if not run.dify_run_id:
card.executed_node_ids = []
return card
if run.status == "succeeded" and has_output_blocker(run):
card.status = "failed"
card.outcome = "required_output_unresolved"
card.failure_reason = copy_text(BuilderCopyId.REPLIES_A_REQUIRED_OUTPUT_REFERENCE_WAS_UNRESOLVED)
elif run.status == "succeeded":
card.outcome = "execution_succeeded_needs_review"
if evidence is None or not evidence.execution_revision:
card.review_note = copy_text(BuilderCopyId.REPLIES_OTHER_PATHS_AND_GOAL_ACCEPTANCE_WERE)
elif run.status == "failed":
card.outcome = "execution_failed"
card.failure_reason = run.error or next(
(n.error for n in run.per_node if n.error), copy_text(BuilderCopyId.LABELS_EXECUTION_FAILED)
)
else:
card.outcome = "execution_unknown"
card.failure_reason = run.error or copy_text(
BuilderCopyId.REPLIES_EXECUTION_OUTCOME_IS_UNKNOWN_RUN_AGAIN_BEFORE_PUBLISHING
)
return card