Files
dify/api/services/dify_builder/native_testing/agent_operations.py

2094 lines
98 KiB
Python

"""Confirmed Agent operations at the existing API physical tool boundary.
Control endpoints never consume dispatch permission. This owner claims once at
its actual synchronous native invocation and records its real result before the
HTTP response; response loss therefore cannot turn a completed effect unknown.
"""
from collections.abc import Callable
from contextlib import suppress
from dataclasses import dataclass
from datetime import UTC, datetime
from typing import TYPE_CHECKING, NotRequired, TypedDict
from dify_agent.adapters.native_testing.client import AgentPhysicalDispatchPayload
from dify_agent.protocol.native_testing import (
AgentEffectIntent,
AgentNativeTestRef,
AgentOperationKey,
AgentShellJobReceipt,
AgentShellJobStatus,
)
from flask import current_app, g
from pydantic import Field, JsonValue, StrictBool
from sqlalchemy import select
from sqlalchemy.orm import sessionmaker
from configs import dify_config
from core.dify_builder.models import Actor
from core.tools.entities.tool_entities import ToolProviderType
from extensions.ext_database import db
from models.agent import Agent, AgentConfigSnapshot
from models.agent_config_entities import AgentSoulConfig
from models.dify_builder_native_tests import (
NativeEffectDispatchClaim,
NativeEffectOperation,
NativeEffectOutcome,
NativeInvocationState,
NativePayloadObject,
NativePendingAction,
NativeTestInvocation,
)
from runtime_contracts.builder.execution import BuilderExecutionPolicyError, canonical_digest, canonical_json
from runtime_contracts.builder.native.approval_contracts import NativeApprovalProposal, NativeApprovalTerms
from runtime_contracts.builder.native.contracts import (
EffectIntent,
FiniteJSON,
FrozenDTO,
NativeTestContextRef,
OperationKey,
operation_digest,
)
from runtime_contracts.builder.native.observation import NativeModelFailureStage
from services.dify_builder.native_testing.agent_bridge import (
AgentNativeTestingBridge,
NativeAgentCredentialMaterializer,
api_intent,
api_permit,
api_ref,
)
from services.dify_builder.native_testing.agent_lineage import NativeAgentEndpointContext
from services.dify_builder.native_testing.agent_runtime_config import NativeAgentRuntimeConfigBuilder
from services.dify_builder.native_testing.agent_tool_declarations import AgentToolMetadata, _metadata
from services.dify_builder.native_testing.authorization import EffectAuthorizationService
from services.dify_builder.native_testing.private_reads import validate_invocation_workflow
from services.dify_builder.native_testing.public_contracts import PendingActionDisclosure
from services.dify_builder.native_testing.repository import (
NativeBinding,
OwnedNativeTestLoader,
admitted_workflow_revision,
native_workflow_revision,
)
from services.dify_builder.native_testing.state_store import NativeExecutionStateStore, ObjectReceipt, owner_query
from services.entities.agent_llm_inner import AgentLLMInvokeCaller
from services.entities.agent_tool_inner import AgentToolInvokeCaller
from services.tools.builtin_tools_manage_service import BuiltinToolManageService
if TYPE_CHECKING:
from services.dify_builder.native_testing.agent_recovery import SelectedRecovery
@dataclass
class AgentToolDispatchOwner:
authorization: EffectAuthorizationService
state_store: NativeExecutionStateStore
revalidate: Callable
def invoke(self, *, envelope: AgentPhysicalDispatchPayload, invoke: Callable[[], JsonValue]) -> JsonValue:
ref, intent, permit = api_ref(envelope.ref), api_intent(envelope.intent), api_permit(envelope.permit)
self.authorization.validate_dispatch(ref=ref, intent=intent, permit=permit)
self.revalidate(ref=ref, intent=intent)
self.authorization.claim_dispatch(ref=ref, intent=intent, permit=permit)
try:
result = invoke()
result_ref = self.state_store.store_result(ref=ref, key=intent.key, result=result)
except BaseException:
self.authorization.record_outcome(permit=permit, outcome="unknown", result_ref=None)
raise
self.authorization.record_outcome(permit=permit, outcome="completed", result_ref=result_ref)
return result
class AgentCallAssessment(FrozenDTO):
"""Builder reasoning describes a call; it has no approval or permission field."""
action: str = Field(min_length=1, max_length=1024)
account_label: str | None
target_summary: FiniteJSON
parameters_summary: FiniteJSON
risk_notice: str = Field(min_length=1, max_length=8192)
destructive: StrictBool
_SHELL_BOUNDARIES = frozenset({"shell_run", "shell_wait", "shell_input", "shell_interrupt", "bootstrap", "config_pull"})
@dataclass(frozen=True)
class AgentShellCapability:
tool_name: str
provider_id: str
capability_description: str
account_ref: str | None = None
account_label: str | None = None
# None means this caller supplied no verified execution-scope facts.
execution_context: dict[str, JsonValue] | None = None
@dataclass(frozen=True)
class _ShellWorkspaceContext:
ref: NativeTestContextRef
tenant_id: str
actor_id: str
app_id: str
workflow_id: str
native_run_id: str | None
graph: dict
_RESOURCE_KEY = "_agent_operations_v1"
_OPACITY = "Approval covers one bounded tool invocation. Provider internal effects remain opaque."
class _ShellLayerConfigs(TypedDict):
shell: dict[str, JsonValue]
runtime: dict[str, JsonValue]
config: dict[str, JsonValue]
workspace_stamp: NotRequired[str]
recovery: NotRequired["SelectedRecovery | None"]
@dataclass(frozen=True)
class AgentResolvedTool:
"""Inert current configured declaration and explicit account, never credentials."""
provider_type: str
provider_id: str
tool_name: str
credential_id: str | None
metadata: AgentToolMetadata
runtime_parameters: dict
account_ref: str | None
credential_version: str | None
verified_plugin: bool
account_label: str | None = None
@dataclass(frozen=True)
class PreparedAgentOperation:
"""Trusted producer data shared by ordinary and inert recovery persistence."""
intent: EffectIntent
proposal: NativeApprovalProposal
assessment: AgentCallAssessment
descriptor: dict
binding: NativeBinding
def _resource_key(key: OperationKey) -> str:
return "agent-intent:" + operation_digest(key)
def _sql_resource(*, session, owned, descriptor, recovery: "SelectedRecovery | None" = None):
"""SQL-only pin/account/selected-proof recheck inside the existing root writer.
Any recovery proof must originate in a protected read outside this writer.
Generation-one shell resources refuse an omitted, forged or stale proof;
parsing descriptor/workspace data alone never supplies that authority.
"""
workflow = validate_invocation_workflow(
session, tenant_id=owned.tenant_id, app_id=owned.app_id, workflow_id=owned.workflow_id
)
revision_context = owned.revision_context
revision = (
admitted_workflow_revision(workflow, revision_context)
if revision_context
else native_workflow_revision(workflow)
)
if revision != descriptor["graph_revision"]:
raise BuilderExecutionPolicyError("native_revision_drift")
snapshot = session.get(AgentConfigSnapshot, descriptor["snapshot_id"])
agent = session.get(Agent, descriptor["agent_id"])
if (
snapshot is None
or agent is None
or (
snapshot.tenant_id,
snapshot.agent_id,
agent.tenant_id,
agent.active_config_snapshot_id,
canonical_digest(AgentSoulConfig.model_validate(snapshot.config_snapshot_dict).model_dump(mode="json")),
)
!= (
owned.tenant_id,
descriptor["agent_id"],
owned.tenant_id,
descriptor["snapshot_id"],
descriptor["snapshot_digest"],
)
):
raise BuilderExecutionPolicyError("native_agent_pin_mismatch")
if "shell_boundary" in descriptor:
from services.dify_builder.native_testing.agent_confirmed_workspace import (
ALLOCATION,
current_confirmed_workspace,
)
allocation = owned.bindings.get(ALLOCATION)
if allocation != descriptor["workspace_allocation"]:
raise BuilderExecutionPolicyError("native_workspace_owner_mismatch")
workspace, stamp = current_confirmed_workspace(
session=session,
owned=owned,
context=_ShellWorkspaceContext(
owned.ref,
owned.tenant_id,
owned.actor_id,
owned.app_id,
owned.workflow_id,
descriptor["workflow_native_run_id"],
{"nodes": [descriptor["graph_node"]]},
),
allocation=allocation,
recovery=recovery,
)
if stamp != descriptor["workspace_stamp"] or workspace.model_dump(mode="json") != descriptor["workspace"]:
raise BuilderExecutionPolicyError("native_workspace_owner_mismatch")
if descriptor["credential_id"] is not None:
from configs import dify_config
from services.tools.builder_credential_reference import select_builder_credential
selected = select_builder_credential(
session,
actor=Actor(account_id=owned.actor_id, tenant_id=owned.tenant_id),
provider_id=descriptor["provider_id"],
credential_id=descriptor["credential_id"],
verified_plugin=descriptor["verified_plugin"],
revision_key=dify_config.SECRET_KEY.encode(),
)
if canonical_digest(selected.reference.model_dump(mode="json")) != descriptor["account_ref"]:
raise BuilderExecutionPolicyError("native_credential_drift")
def _utc_expiry(value: datetime) -> str:
"""Preserve the instant of an aware scope deadline; naive values are UTC."""
return (value.replace(tzinfo=UTC) if value.tzinfo is None else value.astimezone(UTC)).isoformat()
class AgentOperationBindingResolver:
"""Current SQL resources; no decryption, OAuth, runtime schema or business I/O."""
def __init__(self, recovery=None, checkpoint=None):
self.recovery, self.checkpoint = recovery, checkpoint
def for_operation(self, *, store, ref, key):
"""Return a per-call resolver after protected reads outside the writer."""
from models.dify_builder_native_tests import NativeContinuation
from runtime_contracts.builder.native.contracts import CheckpointRef
from services.dify_builder.native_testing.agent_recovery import recovery_record
from services.dify_builder.native_testing.state_store import invocation_state, owner_query
owned = store.loader.load(ref=ref)
record = recovery_record(owned.bindings)
if record is None:
return AgentOperationBindingResolver()
with store.loader.session_factory() as session:
continuation = session.scalar(
owner_query(NativeContinuation, ref, owned.tenant_id).where(
NativeContinuation.checkpoint_id == invocation_state(session, ref, owned.tenant_id).checkpoint_id
)
)
if continuation is None:
raise BuilderExecutionPolicyError("native_agent_recovery_unready")
checkpoint = CheckpointRef(
checkpoint_id=continuation.checkpoint_id, payload_digest=continuation.payload_digest
)
state = store.load_recovery_checkpoint_state(ref=ref, checkpoint=checkpoint)
if state is None or key.invocation_id != ref.invocation_id:
raise BuilderExecutionPolicyError("native_agent_recovery_unready")
return AgentOperationBindingResolver(recovery=state, checkpoint=checkpoint)
def resolve_binding(self, *, session, owned, key):
from services.dify_builder.native_testing.agent_recovery import require_recovery_operation
require_recovery_operation(bindings=owned.bindings, key=key)
from services.dify_builder.native_testing.agent_recovery import recovery_record
from services.dify_builder.native_testing.state_store import NativeExecutionStateStore
if recovery_record(owned.bindings) is not None:
if self.recovery is None or self.checkpoint is None:
raise BuilderExecutionPolicyError("native_agent_recovery_unready")
NativeExecutionStateStore.recheck_recovery_checkpoint_state(
session=session,
owned=owned,
state=self.recovery,
checkpoint=self.checkpoint,
)
identity = operation_digest(key)
descriptor = owned.bindings.get(_RESOURCE_KEY, {}).get(identity)
if descriptor is None:
from services.dify_builder.native_testing.http_binding import NativeHttpBindingResolver
return NativeHttpBindingResolver().resolve_binding(session=session, owned=owned, key=key)
frozen = NativeBinding.model_validate(owned.bindings[identity])
if key.invocation_id != owned.ref.invocation_id or key.occurrence_path[:1] != (frozen.execution_id,):
raise BuilderExecutionPolicyError("native_binding_mismatch")
_sql_resource(
session=session,
owned=owned,
descriptor=descriptor,
recovery=None if self.recovery is None else self.recovery.graph,
)
return frozen
def resolve_pending(self, **_kwargs):
raise BuilderExecutionPolicyError("native_pending_requires_resolution")
class AgentOperationService:
"""One existing SQL operation ledger, with encrypted actual call/proposal data."""
def __init__(self, *, loader, state_store, assessor):
self.loader, self.state_store, self.assessor = loader, state_store, assessor
self.bridge = AgentNativeTestingBridge(
authorization=EffectAuthorizationService(loader, state_store=state_store),
readiness=None,
materializer=None,
fence=None,
)
self.bridge.fence = self.bridge.authorization
self.bridge.settlement = self.settle
self.bridge.validate_call = self.validate_call
self.disclosure = self
def _endpoint(self, ref):
endpoint = g.get("agent_runtime_context")
if not isinstance(endpoint, NativeAgentEndpointContext) or api_ref(endpoint.ref) != ref:
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
if endpoint.native_test_profile != "confirmed":
raise BuilderExecutionPolicyError("native_agent_profile_mismatch")
from models.dify_builder import DifyBuilderExecutionRequest
self.loader.load(ref=ref)
with self.loader.session_factory() as session:
request = session.get(DifyBuilderExecutionRequest, ref.request_id)
if request is None or request.context.get("profile", {}).get("profile_id") != "builder-agent-confirmed-v1":
raise BuilderExecutionPolicyError("native_agent_profile_mismatch")
return endpoint
def _definition(self, *, ref, descriptor=None, for_projection: bool = False):
context = (
self.state_store.load_projection_context(ref=ref)
if for_projection
else self.state_store.load_context(ref=ref)
)
if descriptor is None:
endpoint = self._endpoint(ref)
config = endpoint.config.config
if not isinstance(config, dict) or not isinstance(config.get("composition"), dict):
raise BuilderExecutionPolicyError("native_agent_config_mismatch")
composition = config["composition"]
assert isinstance(composition, dict)
layers = composition.get("layers")
if not isinstance(layers, list):
raise BuilderExecutionPolicyError("native_agent_config_mismatch")
contexts = [
item["config"]
for item in layers
if isinstance(item, dict)
and item.get("name") == "execution_context"
and isinstance(item.get("config"), dict)
]
if len(contexts) != 1:
raise BuilderExecutionPolicyError("native_agent_config_mismatch")
node_id = contexts[0].get("node_id")
else:
node_id = descriptor["node_id"]
if not isinstance(node_id, str) or not node_id:
raise BuilderExecutionPolicyError("native_agent_config_mismatch")
definition = NativeAgentRuntimeConfigBuilder(loader=self.loader).load_definition(
context=context, node_id=node_id
)
return context, definition
def _tool(self, *, ref, target, descriptor=None, for_projection: bool = False):
context, definition = self._definition(ref=ref, descriptor=descriptor, for_projection=for_projection)
if not isinstance(target, dict) or set(target) != {"provider_type", "provider_id", "tool_name"}:
raise BuilderExecutionPolicyError("native_agent_target_mismatch")
selected = [
item
for item in definition.soul.tools.dify_tools
if item.enabled
and item.provider_type.value == target["provider_type"]
and (item.provider_id or f"{item.plugin_id}/{item.provider}") == target["provider_id"]
and item.tool_name in (None, target["tool_name"])
]
if len(selected) != 1:
raise BuilderExecutionPolicyError("agent_tool_declaration_not_found")
configured = selected[0]
if configured.provider_type not in {ToolProviderType.PLUGIN, ToolProviderType.BUILT_IN}:
# Their dynamic runtime/configuration owners need separate effect phases.
raise BuilderExecutionPolicyError("native_agent_tool_preparation_unavailable")
declarations = _metadata(
tenant_id=context.tenant_id,
provider_type=configured.provider_type,
provider_id=target["provider_id"],
tool_name=target["tool_name"],
)
if len(declarations) != 1 or declarations[0].entity.has_runtime_parameters:
raise BuilderExecutionPolicyError("native_agent_tool_preparation_unavailable")
metadata = declarations[0]
credential_id = configured.credential_ref.id if configured.credential_ref else None
selected_account = None
verified = False
if metadata.requires_credentials:
if credential_id is None:
raise BuilderExecutionPolicyError("needs_setup")
from core.plugin.plugin_service import PluginService
verified = PluginService.is_plugin_verified(context.tenant_id, metadata.implementation_revision) is True
selected_account = BuiltinToolManageService.load_builder_credential(
actor=self.loader.actor,
provider_id=target["provider_id"],
credential_id=credential_id,
verified_plugin=verified,
)
elif credential_id is not None:
raise BuilderExecutionPolicyError("native_credential_drift")
account = (
NativeAgentCredentialMaterializer.account_ref(selected_account) if selected_account is not None else None
)
tool = AgentResolvedTool(
configured.provider_type.value,
target["provider_id"],
target["tool_name"],
credential_id,
metadata,
dict(configured.runtime_parameters),
account,
account,
verified,
selected_account.snapshot.reference.alias if selected_account is not None else None,
)
return context, definition, tool, selected_account
def prepare_intent(
self, *, ref: AgentNativeTestRef, key: AgentOperationKey, boundary: str, target, parameters
) -> AgentEffectIntent:
native_ref = api_ref(ref)
native_key = OperationKey.model_validate(key.model_dump(mode="json"))
if (
boundary not in {"core_tool", "model", *_SHELL_BOUNDARIES}
or native_key.invocation_id != native_ref.invocation_id
or not isinstance(parameters, dict)
):
raise BuilderExecutionPolicyError("native_agent_boundary_unavailable")
endpoint = self._endpoint(native_ref)
parent = endpoint.config.parent_node
if native_key.occurrence_path[:1] != (parent.execution_id,):
raise BuilderExecutionPolicyError("native_agent_owner_mismatch")
existing = self.loader.load(ref=native_ref)
if operation_digest(native_key) in existing.bindings:
saved, _proposal, _assessment = self.snapshot(ref=native_ref, key=native_key)
if canonical_json(saved.target) != canonical_json(target) or canonical_json(
saved.parameters
) != canonical_json(parameters):
raise BuilderExecutionPolicyError("native_intent_conflict")
self.revalidate(ref=native_ref, intent=saved)
return AgentEffectIntent.model_validate(saved.model_dump(mode="json"))
if boundary in _SHELL_BOUNDARIES:
return self._prepare_shell(
ref=native_ref, key=native_key, boundary=boundary, target=target, parameters=parameters, parent=parent
)
if boundary == "model":
return self._prepare_model(
ref=native_ref, key=native_key, target=target, parameters=parameters, parent=parent
)
prepared = self._tool_operation(
ref=native_ref, key=native_key, target=target, parameters=parameters, parent=parent, descriptor=None
)
return self._persist(
ref=native_ref,
intent=prepared.intent,
proposal=prepared.proposal,
assessment=prepared.assessment,
descriptor=prepared.descriptor,
binding=prepared.binding,
)
def _tool_operation(self, *, ref, key, target, parameters, parent, descriptor) -> PreparedAgentOperation:
"""Assess current actual arguments without persistence or business dispatch.
Parent and descriptor originate in a protected owner, never HTTP flags.
Failure creates no ledger, grant or permit. The caller rechecks SQL pins
before acknowledging the returned inert preparation.
"""
context, definition, tool, selected_account = self._tool(ref=ref, target=target, descriptor=descriptor)
# Expiry inspection is metadata only. Never let ordinary runtime silently refresh.
if selected_account is not None and NativeAgentCredentialMaterializer.refresh_due(selected_account):
raise BuilderExecutionPolicyError("native_refresh_continuation_required")
if any(
parameter.type.value == "secret-input" and parameter.name in tool.runtime_parameters
for parameter in tool.metadata.entity.parameters or []
):
raise BuilderExecutionPolicyError("native_agent_tool_preparation_unavailable")
assessment = AgentCallAssessment.model_validate(
self.assessor(context=context, definition=definition, tool=tool, target=target, parameters=parameters)
)
return self._tool_preparation(
context=context,
definition=definition,
tool=tool,
target=target,
parameters=parameters,
parent=parent,
key=key,
assessment=assessment,
expires_at=self.loader.load(ref=ref).expires_at,
)
@staticmethod
def _tool_preparation(
*,
context,
definition,
tool: AgentResolvedTool,
target,
parameters,
parent,
key,
assessment: AgentCallAssessment,
expires_at: datetime,
) -> PreparedAgentOperation:
"""Pure declared schema/validated assessment to intent, terms and binding.
Inputs are trusted current owner data; decoding this result grants no
permission. The control wrapper always performs actual metadata lookup
and fresh assessment before calling it. SQL persistence rechecks pins.
"""
intent = EffectIntent(
key=key,
kind="tool",
resource_ref=canonical_digest(
{"snapshot": definition.snapshot_id, "target": target, "credential": tool.credential_id}
),
implementation_revision=tool.metadata.implementation_revision,
account_ref=tool.account_ref,
target=target,
parameters=parameters,
input_digest=parent.input_digest,
capability_digest=canonical_digest(
{
"definition": definition.stamp,
"declaration": tool.metadata.entity.model_dump(mode="json"),
"runtime": tool.runtime_parameters,
}
),
)
proposal = NativeApprovalProposal(
key=key,
terms=NativeApprovalTerms(
max_calls=1,
max_dispatches=1,
max_cost_units=1,
expires_at=_utc_expiry(expires_at),
),
)
descriptor = {
"node_id": definition.node_id,
"agent_id": definition.agent_id,
"snapshot_id": definition.snapshot_id,
"snapshot_digest": canonical_digest(definition.soul.model_dump(mode="json")),
"graph_revision": context.graph_revision,
"credential_id": tool.credential_id,
"provider_id": tool.provider_id,
"account_ref": tool.account_ref,
"verified_plugin": tool.verified_plugin,
}
binding = NativeBinding(
intent_digest=canonical_digest(intent.model_dump(mode="json")),
kind="tool",
resource_ref=intent.resource_ref,
implementation_revision=intent.implementation_revision,
account_ref=intent.account_ref,
capability_digest=intent.capability_digest,
resource_version=definition.stamp,
credential_version=tool.credential_version,
isolated=False,
egress=True,
new_data=True,
destructive=assessment.destructive,
approval_scope="operation",
cost_units=1,
budget={},
execution_id=parent.execution_id,
invocation_digest=canonical_digest(intent.model_dump(mode="json")),
)
return PreparedAgentOperation(intent, proposal, assessment, descriptor, binding)
def _shell(self, *, ref, boundary, target, parameters, descriptor=None, for_projection: bool = False):
"""Read immutable composition and recheck current complete workspace ownership."""
from services.dify_builder.native_testing.agent_confirmed_workspace import (
ALLOCATION,
current_confirmed_workspace,
)
context, definition = self._definition(ref=ref, descriptor=descriptor, for_projection=for_projection)
if descriptor is None:
endpoint = self._endpoint(ref)
prepared, recovery = self.state_store.load_prepared_config_selection(
ref=ref, parent_node=endpoint.config.parent_node, config_ref=endpoint.config.config_ref
)
if prepared != endpoint.config:
raise BuilderExecutionPolicyError("native_agent_owner_mismatch")
else:
# Public approval reads have no runtime HTTP request. Reconstruct
# only from the persisted descriptor and the protected state owner.
from runtime_contracts.builder.native.agent_contracts import AgentParentNodeRef
parent = AgentParentNodeRef.model_validate(descriptor["parent_node"])
if parent.ref != ref:
raise BuilderExecutionPolicyError("native_agent_owner_mismatch")
prepared, recovery = self.state_store.load_prepared_config_selection(
ref=ref, parent_node=parent, config_ref=descriptor["config_ref"], for_projection=for_projection
)
endpoint = NativeAgentEndpointContext(
ref=AgentNativeTestRef.model_validate(ref.model_dump(mode="json")),
config=prepared,
association=None,
native_test_profile="confirmed",
)
composition = prepared.config.get("composition") if isinstance(prepared.config, dict) else None
layers = composition.get("layers") if isinstance(composition, dict) else None
if not isinstance(layers, list):
raise BuilderExecutionPolicyError("native_agent_config_mismatch")
checked_layers = layers
def selected_config(name: str) -> dict[str, JsonValue]:
selected = [
item.get("config") for item in checked_layers if isinstance(item, dict) and item.get("name") == name
]
if len(selected) != 1 or not isinstance(selected[0], dict):
raise BuilderExecutionPolicyError("native_agent_config_mismatch")
return selected[0]
configs: _ShellLayerConfigs = {
"shell": selected_config("shell"),
"runtime": selected_config("runtime"),
"config": selected_config("config"),
}
if configs["runtime"].get("backend_binding_ref") != prepared.workspace.backend_binding_ref:
raise BuilderExecutionPolicyError("native_workspace_owner_mismatch")
expected: dict[str, JsonValue] = {"shell": "shell"}
if boundary == "bootstrap":
expected["runtime"] = configs["shell"].get("runtime")
elif boundary == "config_pull":
if not isinstance(target, dict) or target.get("kind") not in {"skill", "file"}:
raise BuilderExecutionPolicyError("native_agent_target_mismatch")
expected["kind"] = target["kind"]
if not configs["config"].get("mentioned_" + target["kind"] + "_names"):
raise BuilderExecutionPolicyError("native_agent_config_asset_unavailable")
if target != expected or boundary not in _SHELL_BOUNDARIES:
raise BuilderExecutionPolicyError("native_agent_target_mismatch")
if boundary in {"bootstrap", "config_pull", "shell_run"} and not isinstance(parameters.get("script"), str):
raise BuilderExecutionPolicyError("native_agent_target_mismatch")
with self.loader.locked(ref=ref) as (session, owned, _scope):
workspace, stamp = current_confirmed_workspace(
session=session,
owned=owned,
context=context,
allocation=owned.bindings.get(ALLOCATION),
recovery=recovery,
)
if workspace != prepared.workspace:
raise BuilderExecutionPolicyError("native_workspace_owner_mismatch")
configs["workspace_stamp"] = stamp
configs["recovery"] = recovery
return context, definition, endpoint, configs
def _prepare_shell(self, *, ref, key, boundary, target, parameters, parent):
from services.dify_builder.native_testing.agent_confirmed_workspace import ALLOCATION
context, definition, endpoint, configs = self._shell(
ref=ref, boundary=boundary, target=target, parameters=parameters
)
owned = self.loader.load(ref=ref)
assessment = AgentCallAssessment.model_validate(
self.assessor(
context=context,
definition=definition,
tool=AgentShellCapability(
boundary,
"workspace-shell",
"Execute a bounded shell operation in the Agent workspace. "
"Configured CLI tools, skills and files: "
+ canonical_json({"shell": configs["shell"], "config": configs.get("config")}).decode(),
execution_context={
"initial_working_directory": "request_owned_agent_workspace",
"may_contain_cloned_files": True,
"initial_working_directory_is_confinement": False,
"runtime_filesystem_isolation": "not_attested_to_assessor",
"configured_asset_cli": {
"skills_default_relative_directory": ".dify_conf/skills",
"files_default_relative_directory": ".dify_conf/files",
"may_overwrite_local_copies": True,
"may_download_remote_assets": True,
},
},
),
target=target,
parameters=parameters,
)
)
workspace = endpoint.config.workspace.model_dump(mode="json")
intent = EffectIntent(
key=key,
kind="bootstrap" if boundary == "bootstrap" else "shell",
resource_ref=endpoint.config.workspace.binding_id,
implementation_revision=definition.snapshot_id,
account_ref=None,
target=target,
parameters=parameters,
input_digest=parent.input_digest,
capability_digest=canonical_digest(
{
"definition": definition.stamp,
"workspace": workspace,
"composition": endpoint.config.config_digest,
"boundary": boundary,
}
),
)
descriptor = {
"node_id": definition.node_id,
"agent_id": definition.agent_id,
"snapshot_id": definition.snapshot_id,
"snapshot_digest": canonical_digest(definition.soul.model_dump(mode="json")),
"graph_revision": context.graph_revision,
"credential_id": None,
"provider_id": "workspace-shell",
"account_ref": None,
"verified_plugin": False,
"shell_boundary": boundary,
"workspace": workspace,
"workspace_allocation": owned.bindings.get(ALLOCATION),
"workspace_stamp": configs.get("workspace_stamp"),
"workflow_native_run_id": context.native_run_id,
"graph_node": next(node for node in context.graph["nodes"] if node["id"] == definition.node_id),
"composition_digest": endpoint.config.config_digest,
"parent_node": parent.model_dump(mode="json"),
"config_ref": endpoint.config.config_ref,
}
binding = NativeBinding(
intent_digest=canonical_digest(intent.model_dump(mode="json")),
kind=intent.kind,
resource_ref=intent.resource_ref,
implementation_revision=intent.implementation_revision,
account_ref=None,
capability_digest=intent.capability_digest,
resource_version=definition.stamp,
credential_version=None,
isolated=False,
egress=True,
new_data=True,
destructive=assessment.destructive,
approval_scope="operation",
cost_units=1,
budget={},
execution_id=parent.execution_id,
invocation_digest=canonical_digest(intent.model_dump(mode="json")),
)
proposal = NativeApprovalProposal(
key=key,
terms=NativeApprovalTerms(
max_calls=1,
max_dispatches=1,
max_cost_units=1,
expires_at=_utc_expiry(owned.expires_at),
),
)
return self._persist(
ref=ref,
intent=intent,
proposal=proposal,
assessment=assessment,
descriptor=descriptor,
binding=binding,
recovery=configs.get("recovery"),
)
def claim_shell_dispatch(self, *, envelope):
"""The run-bound runtime spends once immediately before its leased command."""
ref, intent, permit = api_ref(envelope.ref), api_intent(envelope.intent), api_permit(envelope.permit)
self._endpoint(ref)
if intent.kind not in {"shell", "bootstrap"}:
raise BuilderExecutionPolicyError("native_agent_boundary_unavailable")
self.revalidate(ref=ref, intent=intent)
self.bridge.authorization.claim_dispatch(ref=ref, intent=intent, permit=permit)
def validate_claimed_shell(self, *, envelope):
"""Subordinate config reads require an active claimed parent shell operation."""
ref, intent, permit = api_ref(envelope.ref), api_intent(envelope.intent), api_permit(envelope.permit)
endpoint = self._endpoint(ref)
if intent.kind not in {"shell", "bootstrap"}:
raise BuilderExecutionPolicyError("native_agent_boundary_unavailable")
self.revalidate(ref=ref, intent=intent)
self.bridge.authorization.validate_dispatch(ref=ref, intent=intent, permit=permit)
owned = self.loader.load(ref=ref)
with self.loader.session_factory() as session:
claim = session.scalar(
owner_query(NativeEffectDispatchClaim, ref, owned.tenant_id).where(
NativeEffectDispatchClaim.operation_id == permit.operation_id,
NativeEffectDispatchClaim.attempt_id == permit.attempt_id,
)
)
outcome = session.scalar(
owner_query(NativeEffectOutcome, ref, owned.tenant_id).where(
NativeEffectOutcome.operation_id == permit.operation_id,
NativeEffectOutcome.attempt_id == permit.attempt_id,
)
)
if claim is None or outcome is not None:
raise BuilderExecutionPolicyError("invalid_dispatch_permit")
return endpoint
@staticmethod
def _shell_ack_key(envelope):
return "agent-shell-ack:" + canonical_digest(
{"operation_id": envelope.permit.operation_id, "attempt_id": envelope.permit.attempt_id}
)
@staticmethod
def _shell_ack_payload(*, envelope, endpoint, result_ref, shell_job):
# Capture the original selected owner/binding/snapshot, not a later
# reconstruction that could attach a running job to another workspace.
return {
"schema": "agent-shell-ack-v1",
"ref": envelope.ref.model_dump(mode="json"),
"operation_id": envelope.permit.operation_id,
"attempt_id": envelope.permit.attempt_id,
"intent_digest": canonical_digest(envelope.intent.model_dump(mode="json")),
"result_ref": result_ref,
"prepared": endpoint.config.model_dump(mode="json", exclude={"config"}),
"shell_job": shell_job.model_dump(mode="json"),
}
def _completed_shell_ack(self, *, envelope, endpoint):
ref, intent, permit = api_ref(envelope.ref), api_intent(envelope.intent), api_permit(envelope.permit)
result_ref = self.bridge.authorization.validate_completed_config_claim(ref=ref, intent=intent, permit=permit)
owned = self.loader.load(ref=ref)
with self.loader.session_factory() as session:
row = session.scalar(
owner_query(NativePayloadObject, ref, owned.tenant_id).where(
NativePayloadObject.kind == "result",
NativePayloadObject.logical_key == self._shell_ack_key(envelope),
)
)
actual_result = session.scalar(
owner_query(NativePayloadObject, ref, owned.tenant_id).where(
NativePayloadObject.id == result_ref,
NativePayloadObject.kind == "result",
NativePayloadObject.logical_key == operation_digest(intent.key),
)
)
if row is None or actual_result is None:
raise BuilderExecutionPolicyError("native_agent_job_unavailable")
receipt = ObjectReceipt.from_payload_object(row)
value = self.state_store._read_object(receipt)
try:
if not isinstance(value, dict):
raise ValueError("ACK")
job = AgentShellJobReceipt.model_validate(value.get("shell_job"))
if (
job.done
or job.exit_code is not None
or job.status not in {"created", "starting", "running"}
or job.binding_ref != endpoint.config.workspace.backend_binding_ref
or value
!= self._shell_ack_payload(envelope=envelope, endpoint=endpoint, result_ref=result_ref, shell_job=job)
):
raise ValueError("ACK relation")
except ValueError:
raise BuilderExecutionPolicyError("native_agent_job_unavailable") from None
# Encrypted object reads can themselves perform network I/O. Leave no
# storage read after the final SQL-only scope/owner/expiry check.
current_result = self.bridge.authorization.validate_completed_config_claim(
ref=ref, intent=intent, permit=permit
)
if current_result != result_ref:
raise BuilderExecutionPolicyError("native_agent_job_unavailable")
return job
@staticmethod
def _read_shell_job(*, binding_ref, job_id):
from dify_agent.protocol import ReadExecutionBindingJobRequest
from clients.agent_backend.factory import create_agent_backend_client
if not dify_config.AGENT_BACKEND_BASE_URL:
raise BuilderExecutionPolicyError("native_agent_job_unavailable")
with create_agent_backend_client(
base_url=dify_config.AGENT_BACKEND_BASE_URL,
api_token=dify_config.AGENT_BACKEND_API_TOKEN,
) as client:
return client.read_execution_binding_job_sync(
ReadExecutionBindingJobRequest(binding_ref=binding_ref, job_id=job_id)
)
def validate_config_read_shell(self, *, envelope):
"""Allow pinned config reads under an unsettled claim or a proven live ACK.
The completed branch grants no dispatch, settlement, lease refresh or
other filesystem access. All authority is checked again after status I/O.
"""
try:
return self.validate_claimed_shell(envelope=envelope)
except BuilderExecutionPolicyError:
pass
ref, intent = api_ref(envelope.ref), api_intent(envelope.intent)
endpoint = self._endpoint(ref)
self.revalidate(ref=ref, intent=intent)
job = self._completed_shell_ack(envelope=envelope, endpoint=endpoint)
try:
response = self._read_shell_job(binding_ref=job.binding_ref, job_id=job.job_id)
live = AgentShellJobStatus.model_validate(response.model_dump(mode="json"))
if live.job_id != job.job_id or live.done or live.exit_code is not None:
raise ValueError("not the same live job")
except Exception:
raise BuilderExecutionPolicyError("native_agent_job_unavailable") from None
current = self._endpoint(ref)
self.revalidate(ref=ref, intent=intent)
checked = self._completed_shell_ack(envelope=envelope, endpoint=current)
if current != endpoint or checked != job:
raise BuilderExecutionPolicyError("native_agent_job_unavailable")
return current
@staticmethod
def _model_target(definition):
from core.plugin.provider_identity import normalize_plugin_daemon_provider_identity
from models.provider_ids import ModelProviderID
model = definition.soul.model
if model is None:
raise BuilderExecutionPolicyError("native_agent_model_unavailable")
plugin_id, provider = normalize_plugin_daemon_provider_identity(
ModelProviderID(model.model_provider), model.plugin_id
)
return {
"plugin_id": plugin_id,
"provider": provider if provider.count("/") == 2 else f"{plugin_id}/{provider}",
"model": model.model,
}
def _prepare_model(self, *, ref, key, target, parameters, parent):
prepared = self._model_operation(
ref=ref, key=key, target=target, parameters=parameters, parent=parent, descriptor=None
)
return self._persist(
ref=ref,
intent=prepared.intent,
proposal=prepared.proposal,
assessment=prepared.assessment,
descriptor=prepared.descriptor,
binding=prepared.binding,
)
def _model_operation(self, *, ref, key, target, parameters, parent, descriptor) -> PreparedAgentOperation:
"""Build the selected model disclosure and binding without invoking a model.
Current definition and exact model target must match. Persistence and
authorization remain with the caller's separately locked owner.
"""
context, definition = self._definition(ref=ref, descriptor=descriptor)
if target != self._model_target(definition):
raise BuilderExecutionPolicyError("native_agent_model_mismatch")
owned = self.loader.load(ref=ref)
intent = EffectIntent(
key=key,
kind="model",
resource_ref=definition.agent_id,
implementation_revision=definition.snapshot_id,
account_ref=None,
target=target,
parameters=parameters,
input_digest=parent.input_digest,
capability_digest=definition.stamp,
)
descriptor = {
"node_id": definition.node_id,
"agent_id": definition.agent_id,
"snapshot_id": definition.snapshot_id,
"snapshot_digest": canonical_digest(definition.soul.model_dump(mode="json")),
"graph_revision": context.graph_revision,
"credential_id": None,
"provider_id": target["provider"],
"account_ref": None,
"verified_plugin": False,
"task_model": True,
"model_target": target,
}
binding = NativeBinding(
intent_digest=canonical_digest(intent.model_dump(mode="json")),
kind="model",
resource_ref=intent.resource_ref,
implementation_revision=intent.implementation_revision,
account_ref=None,
capability_digest=intent.capability_digest,
resource_version=definition.stamp,
credential_version=None,
isolated=False,
egress=True,
new_data=True,
destructive=False,
approval_scope="operation",
cost_units=1,
budget={},
execution_id=parent.execution_id,
invocation_digest=canonical_digest(intent.model_dump(mode="json")),
)
proposal = NativeApprovalProposal(
key=key,
terms=NativeApprovalTerms(
max_calls=1,
max_dispatches=1,
max_cost_units=1,
expires_at=_utc_expiry(owned.expires_at),
),
)
assessment = AgentCallAssessment(
action="Invoke selected Agent model",
account_label=None,
target_summary=target,
parameters_summary={"input_digest": canonical_digest(parameters)},
risk_notice="Uses the published Agent model within the bounded outer confirmed Test Run.",
destructive=False,
)
return PreparedAgentOperation(intent, proposal, assessment, descriptor, binding)
def validate_caller(self, *, ref, caller: AgentToolInvokeCaller | AgentLLMInvokeCaller):
endpoint = self._endpoint(ref)
context = self.state_store.load_context(ref=ref)
if not isinstance(caller, (AgentToolInvokeCaller, AgentLLMInvokeCaller)) or (
caller.tenant_id,
caller.app_id,
caller.user_id,
caller.user_from,
caller.workflow_id,
caller.workflow_run_id,
caller.node_execution_id,
) != (
context.tenant_id,
context.app_id,
context.actor_id,
"account",
context.workflow_id,
context.native_run_id,
endpoint.config.parent_node.execution_id,
):
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
def validate_model(self, *, envelope):
self.bridge.authorization.validate_dispatch(
ref=api_ref(envelope.ref), intent=api_intent(envelope.intent), permit=api_permit(envelope.permit)
)
self.revalidate(ref=api_ref(envelope.ref), intent=api_intent(envelope.intent))
def validate_model_request(self, *, request):
envelope = request.native_dispatch
if envelope is None:
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
self.validate_caller(ref=api_ref(envelope.ref), caller=request.caller)
target = {
"plugin_id": "/".join(request.target.provider.split("/")[:2]),
"provider": request.target.provider,
"model": request.target.model,
}
parameters = {**request.target.model_dump(mode="json", exclude={"provider", "model"}), "stream": True}
if (
envelope.intent.kind != "model"
or canonical_json(envelope.intent.target) != canonical_json(target)
or canonical_json(envelope.intent.parameters) != canonical_json(parameters)
):
raise BuilderExecutionPolicyError("native_endpoint_arguments_mismatch")
self.validate_model(envelope=envelope)
def invoke_model(self, *, envelope, invoke, on_failure: Callable[[NativeModelFailureStage, Exception], None]):
"""Claim a validated model dispatch and yield original provider chunks.
Persist a bounded receipt of the public SSE DTO/defaults before marking
completion. Any incomplete stream retains unknown settlement; failures
propagate through the existing observer without replaying the provider.
"""
from dify_agent.adapters.llm.provider import ProtectedModelChunkReceipt, model_chunk_to_wire
from graphon.model_runtime.entities.llm_entities import LLMResultChunk
ref, intent, permit = api_ref(envelope.ref), api_intent(envelope.intent), api_permit(envelope.permit)
self.validate_model(envelope=envelope)
self.bridge.authorization.claim_dispatch(ref=ref, intent=intent, permit=permit)
complete = False
receipt = ProtectedModelChunkReceipt()
stage: NativeModelFailureStage = "invoke"
try:
for chunk in invoke():
# Record exactly the public SSE DTO and SDK defaults. Keep the
# original provider chunk/history untouched for the stream owner.
stage = "receipt_append"
receipt.append(LLMResultChunk.model_validate(model_chunk_to_wire(chunk)))
yield chunk
stage = "invoke"
stage = "result_store"
result_ref = self.state_store.store_result(ref=ref, key=intent.key, result=receipt.value)
stage = "outcome_complete"
self.bridge.authorization.record_outcome(permit=permit, outcome="completed", result_ref=result_ref)
complete = True
except Exception as error:
with suppress(Exception):
on_failure(stage, error)
raise
finally:
if not complete:
try:
self.bridge.authorization.record_outcome(permit=permit, outcome="unknown", result_ref=None)
except Exception as error:
with suppress(Exception):
on_failure("unknown_settlement", error)
raise
def _persist(
self, *, ref, intent, proposal, assessment, descriptor, binding, recovery: "SelectedRecovery | None" = None
):
"""Acknowledge prepared metadata after the same selected resource proof.
Recovery proof is server-read data, never a request permission flag.
The root recheck refuses any changed selection before acknowledgment;
no grants, permits or physical calls occur in this persistence owner.
"""
owned = self.loader.load(ref=ref)
receipt = self.state_store.prepare_object(
ref=ref,
tenant_id=owned.tenant_id,
kind="result",
logical_key=_resource_key(intent.key),
payload={
"intent": intent.model_dump(mode="json"),
"proposal": proposal.model_dump(mode="json"),
"assessment": assessment.model_dump(mode="json"),
},
)
with self.loader.locked(ref=ref) as (session, current, _scope):
_sql_resource(session=session, owned=current, descriptor=descriptor, recovery=recovery)
invocation = session.scalar(
select(NativeTestInvocation).where(
NativeTestInvocation.tenant_id == current.tenant_id,
NativeTestInvocation.request_id == ref.request_id,
NativeTestInvocation.invocation_id == ref.invocation_id,
NativeTestInvocation.active.is_(True),
)
)
if invocation is None:
raise BuilderExecutionPolicyError("native_invocation_mismatch")
identity = operation_digest(intent.key)
existing = invocation.bindings.get(identity)
if existing is not None and NativeBinding.model_validate(existing) != binding:
raise BuilderExecutionPolicyError("native_intent_conflict")
self.state_store.acknowledge_object(session, receipt)
invocation.bindings = {
**invocation.bindings,
identity: binding.model_dump(mode="json"),
_RESOURCE_KEY: {**invocation.bindings.get(_RESOURCE_KEY, {}), identity: descriptor},
}
return AgentEffectIntent.model_validate(intent.model_dump(mode="json"))
def _staged_recovery_operation(self, *, ref, expected, key) -> PreparedAgentOperation | None:
"""Read an already acknowledged fresh disclosure for stable retry.
This grants no current metadata or execution authority. The production
staging caller revalidates it through the ordinary resource owner before
the allocated-root writer checks every receipt, binding and pending row.
A retry never replaces acknowledged risk prose with another assessment.
"""
from services.dify_builder.native_testing.agent_recovery import recovery_record
owned = self.loader.load(ref=ref)
if expected.phase != "allocated" or recovery_record(owned.bindings) != expected:
raise BuilderExecutionPolicyError("native_agent_recovery_conflict")
identity = operation_digest(key)
descriptor = owned.bindings.get(_RESOURCE_KEY, {}).get(identity)
binding = owned.bindings.get(identity)
if descriptor is None and binding is None:
return None
if descriptor is None or binding is None:
raise BuilderExecutionPolicyError("native_agent_recovery_conflict")
intent, proposal, assessment = self.snapshot(ref=ref, key=key)
return PreparedAgentOperation(intent, proposal, assessment, descriptor, NativeBinding.model_validate(binding))
def stage_recovery_pending(self, *, ref, expected):
"""Create fresh inert approval rows from one acknowledged allocated wait.
Reuse ordinary current assessment/descriptor/binding producers outside
the root lock. Inside it, recheck the exact retained owner and never-
dispatched predecessor. No before_call, grant, permit, old operation
mutation or pending-list selection occurs. Replays reuse SQL identities.
"""
from dify_agent.protocol.native_continuation import AgentCheckpoint
from services.dify_builder.native_testing.agent_recovery import (
recovery_record,
)
if expected.phase != "allocated" or recovery_record(self.loader.load(ref=ref).bindings) != expected:
raise BuilderExecutionPolicyError("native_agent_recovery_conflict")
prepared, evidence = self.state_store.prepare_recovery_attempt(ref=ref, expected=expected)
association = self.state_store._agent_binding(prepared, expected.predecessor.association_object_ref)
from runtime_contracts.builder.native.agent_contracts import AgentWaitAssociation
checkpoint = AgentCheckpoint.model_validate(
self.state_store._agent_wait_relation(
prepared, association, AgentWaitAssociation.model_validate(evidence["wait"])
)
)
staged = []
for call in checkpoint.pending_calls:
old_intent = api_intent(call.intent)
saved, _old_proposal, _old_assessment = self.snapshot(ref=ref, key=old_intent.key)
descriptor = self.loader.load(ref=ref).bindings.get(_RESOURCE_KEY, {}).get(operation_digest(old_intent.key))
if saved != old_intent or descriptor is None:
raise BuilderExecutionPolicyError("native_agent_recovery_conflict")
key = OperationKey.model_validate(
{
**old_intent.key.model_dump(mode="json"),
"occurrence_path": [
prepared.parent.execution_id,
"recovery",
str(expected.next_workspace_generation),
*old_intent.key.occurrence_path[-2:],
],
}
)
reused = self._staged_recovery_operation(ref=ref, expected=expected, key=key)
if reused is not None:
self.revalidate(ref=ref, intent=reused.intent)
fresh = reused
elif call.stage == "model" and call.intent.kind == "model":
fresh = self._model_operation(
ref=ref,
key=key,
target=old_intent.target,
parameters=old_intent.parameters,
parent=prepared.parent,
descriptor=descriptor,
)
elif (
call.stage == "tool"
and call.intent.kind == "tool"
and old_intent.key.occurrence_path[-2] == "core_tool"
):
fresh = self._tool_operation(
ref=ref,
key=key,
target=old_intent.target,
parameters=old_intent.parameters,
parent=prepared.parent,
descriptor=descriptor,
)
else:
raise BuilderExecutionPolicyError("native_agent_recovery_checkpoint_unsupported")
staged.append(
self._stage_recovery_pending_one(
ref=ref, expected=expected, predecessor_pending_id=call.pending_operation_id, fresh=fresh
)
)
return tuple(staged)
def _stage_recovery_pending_one(self, *, ref, expected, predecessor_pending_id, fresh: PreparedAgentOperation):
"""Persist one typed inert preparation after exact allocated-owner proof.
No assessor callback or caller permission flag is accepted. Production
calls this only after fresh ordinary preparation. Root checks preserve
original arguments/key cursor, exact descriptor/pins, fresh scope and
zero attempts/grants; original pending rows/list stay unchanged.
"""
from dify_agent.protocol.native_continuation import AgentCheckpoint
from models.dify_builder_native_tests import NativeEffectGrant
from runtime_contracts.builder.native.agent_contracts import AgentWaitAssociation
from services.dify_builder.native_testing.agent_recovery import (
RecoveryStagedPending,
recovery_record,
require_never_dispatched_pending,
)
from services.dify_builder.native_testing.authorization import _scope_digest, validate_binding
if expected.phase != "allocated" or recovery_record(self.loader.load(ref=ref).bindings) != expected:
raise BuilderExecutionPolicyError("native_agent_recovery_conflict")
prepared, evidence = self.state_store.prepare_recovery_attempt(ref=ref, expected=expected)
association = self.state_store._agent_binding(prepared, expected.predecessor.association_object_ref)
checkpoint = AgentCheckpoint.model_validate(
self.state_store._agent_wait_relation(
prepared, association, AgentWaitAssociation.model_validate(evidence["wait"])
)
)
calls = [call for call in checkpoint.pending_calls if call.pending_operation_id == predecessor_pending_id]
if len(calls) != 1:
raise BuilderExecutionPolicyError("native_agent_recovery_conflict")
old_intent = api_intent(calls[0].intent)
saved, _proposal, _assessment = self.snapshot(ref=ref, key=old_intent.key)
source = self.state_store._agent_object(prepared, logical_key=_resource_key(old_intent.key))
assert source is not None
if not isinstance(source[1], dict) or source[1].get("intent") != old_intent.model_dump(mode="json"):
raise BuilderExecutionPolicyError("native_agent_recovery_conflict")
original = self.loader.load(ref=ref)
old_descriptor = original.bindings.get(_RESOURCE_KEY, {}).get(operation_digest(old_intent.key))
old_binding_value = original.bindings.get(operation_digest(old_intent.key))
if old_descriptor is None or old_binding_value is None or fresh.descriptor != old_descriptor:
raise BuilderExecutionPolicyError("native_agent_recovery_conflict")
old_binding = NativeBinding.model_validate(old_binding_value)
validate_binding(old_intent, old_binding)
preserved_binding = {"intent_digest", "invocation_digest"}
if fresh.binding.model_dump(exclude=preserved_binding) != old_binding.model_dump(exclude=preserved_binding):
raise BuilderExecutionPolicyError("native_binding_drift")
key = fresh.intent.key
if (
saved != old_intent
or key
!= OperationKey.model_validate(
{
**old_intent.key.model_dump(mode="json"),
"occurrence_path": [
prepared.parent.execution_id,
"recovery",
str(expected.next_workspace_generation),
*old_intent.key.occurrence_path[-2:],
],
}
)
or fresh.proposal.key != key
):
raise BuilderExecutionPolicyError("native_agent_recovery_conflict")
expected_terms = NativeApprovalTerms(
max_calls=1,
max_dispatches=1,
max_cost_units=1,
expires_at=_utc_expiry(self.loader.load(ref=ref).expires_at),
)
if fresh.proposal.terms != expected_terms:
raise BuilderExecutionPolicyError("native_agent_recovery_conflict")
if fresh.intent.model_dump(exclude={"key"}) != old_intent.model_dump(exclude={"key"}):
raise BuilderExecutionPolicyError("native_binding_drift")
validate_binding(fresh.intent, fresh.binding)
value = {
"intent": fresh.intent.model_dump(mode="json"),
"proposal": fresh.proposal.model_dump(mode="json"),
"assessment": fresh.assessment.model_dump(mode="json"),
}
receipt = self.state_store.prepare_object(
ref=ref,
tenant_id=prepared.context.tenant_id,
kind="result",
logical_key=_resource_key(key),
payload=value,
)
self.state_store._agent_refresh_access(prepared)
with self.loader.locked(ref=ref) as (session, owned, scope):
if recovery_record(owned.bindings) != expected:
raise BuilderExecutionPolicyError("native_agent_recovery_conflict")
self.state_store._agent_recheck(session, owned, scope, prepared, writing=False)
self.state_store.recheck_recovery_wait(
session=session, owned=owned, prepared=prepared, evidence=evidence, expected=expected
)
if (
owned.bindings.get(_RESOURCE_KEY, {}).get(operation_digest(old_intent.key)),
owned.bindings.get(operation_digest(old_intent.key)),
) != (old_descriptor, old_binding_value):
raise BuilderExecutionPolicyError("native_binding_drift")
predecessor_pending = session.scalar(
select(NativePendingAction).where(
NativePendingAction.tenant_id == owned.tenant_id,
NativePendingAction.request_id == ref.request_id,
NativePendingAction.invocation_id == ref.invocation_id,
NativePendingAction.id == predecessor_pending_id,
)
)
predecessor_operation = (
None
if predecessor_pending is None
else session.scalar(
select(NativeEffectOperation).where(
NativeEffectOperation.tenant_id == owned.tenant_id,
NativeEffectOperation.request_id == ref.request_id,
NativeEffectOperation.invocation_id == ref.invocation_id,
NativeEffectOperation.id == predecessor_pending.operation_id,
)
)
)
if (
predecessor_pending is None
or predecessor_operation is None
or (
predecessor_operation.operation_digest,
predecessor_operation.intent_digest,
predecessor_operation.scope_digest,
predecessor_pending.reference,
predecessor_pending.reference_digest,
predecessor_pending.owner_version,
)
!= (
operation_digest(old_intent.key),
old_binding.intent_digest,
_scope_digest(old_intent, old_binding),
predecessor_operation.id,
canonical_digest(predecessor_operation.id),
_scope_digest(old_intent, old_binding),
)
):
raise BuilderExecutionPolicyError("native_agent_recovery_conflict")
_sql_resource(session=session, owned=owned, descriptor=fresh.descriptor)
invocation = session.scalar(
select(NativeTestInvocation).where(
NativeTestInvocation.tenant_id == owned.tenant_id,
NativeTestInvocation.request_id == ref.request_id,
NativeTestInvocation.invocation_id == ref.invocation_id,
NativeTestInvocation.active.is_(True),
)
)
if invocation is None:
raise BuilderExecutionPolicyError("native_invocation_mismatch")
existing_binding = invocation.bindings.get(operation_digest(key))
if existing_binding is not None and NativeBinding.model_validate(existing_binding) != fresh.binding:
raise BuilderExecutionPolicyError("native_intent_conflict")
scoped = _scope_digest(fresh.intent, fresh.binding)
if (
session.scalar(
select(NativeEffectGrant.id).where(
NativeEffectGrant.tenant_id == owned.tenant_id,
NativeEffectGrant.request_id == ref.request_id,
NativeEffectGrant.scope_digest == scoped,
)
)
is not None
):
raise BuilderExecutionPolicyError("native_agent_recovery_dispatched")
operation = session.scalar(
select(NativeEffectOperation).where(
NativeEffectOperation.tenant_id == owned.tenant_id,
NativeEffectOperation.request_id == ref.request_id,
NativeEffectOperation.invocation_id == ref.invocation_id,
NativeEffectOperation.operation_digest == operation_digest(key),
)
)
if operation is None:
operation = NativeEffectOperation(
tenant_id=owned.tenant_id,
request_id=ref.request_id,
invocation_id=ref.invocation_id,
operation_digest=operation_digest(key),
intent_digest=canonical_digest(fresh.intent.model_dump(mode="json")),
scope_digest=scoped,
state="pending",
result_ref=None,
)
session.add(operation)
session.flush()
if (operation.intent_digest, operation.scope_digest) != (
canonical_digest(fresh.intent.model_dump(mode="json")),
scoped,
):
raise BuilderExecutionPolicyError("native_intent_conflict")
pending = EffectAuthorizationService._pending(session, owned, operation)
require_never_dispatched_pending(
session=session,
tenant_id=owned.tenant_id,
ref=ref,
pending_ids=(pending.pending_operation_id,),
generation=expected.parent_generation,
)
self.state_store.acknowledge_object(session, receipt)
invocation.bindings = {
**invocation.bindings,
operation_digest(key): fresh.binding.model_dump(mode="json"),
_RESOURCE_KEY: {
**invocation.bindings.get(_RESOURCE_KEY, {}),
operation_digest(key): fresh.descriptor,
},
}
return RecoveryStagedPending(
predecessor_pending_id=predecessor_pending_id,
pending_id=pending.pending_operation_id,
intent=fresh.intent,
proposal=fresh.proposal,
)
def snapshot(self, *, ref, key):
native_ref = api_ref(ref) if isinstance(ref, AgentNativeTestRef) else ref
native_key = OperationKey.model_validate(key.model_dump(mode="json"))
owned = self.loader.load(ref=native_ref)
with self.loader.session_factory() as session:
row = session.scalar(
select(NativePayloadObject).where(
NativePayloadObject.tenant_id == owned.tenant_id,
NativePayloadObject.request_id == native_ref.request_id,
NativePayloadObject.invocation_id == native_ref.invocation_id,
NativePayloadObject.kind == "result",
NativePayloadObject.logical_key == _resource_key(native_key),
)
)
if row is None:
raise BuilderExecutionPolicyError("native_agent_intent_unavailable")
receipt = ObjectReceipt.from_payload_object(row)
payload = self.state_store._read_object(receipt)
self.loader.load(ref=native_ref)
return (
EffectIntent.model_validate(payload["intent"]),
NativeApprovalProposal.model_validate(payload["proposal"]),
AgentCallAssessment.model_validate(payload["assessment"]),
)
def revalidate(self, *, ref, intent, for_projection: bool = False):
owned = self.loader.load(ref=ref)
descriptor = owned.bindings.get(_RESOURCE_KEY, {}).get(operation_digest(intent.key))
if descriptor is None:
raise BuilderExecutionPolicyError("native_agent_intent_unavailable")
if "shell_boundary" in descriptor:
saved, _proposal, assessment = self.snapshot(ref=ref, key=intent.key)
if assessment.destructive:
raise BuilderExecutionPolicyError("destructive_effect_forbidden")
if saved != intent or intent.key.occurrence_path[1:2] != (descriptor["shell_boundary"],):
raise BuilderExecutionPolicyError("native_binding_drift")
_context, definition, endpoint, _configs = self._shell(
ref=ref,
boundary=descriptor["shell_boundary"],
target=intent.target,
parameters=intent.parameters,
descriptor=descriptor,
for_projection=for_projection,
)
if (
definition.snapshot_id,
endpoint.config.config_digest,
endpoint.config.workspace.model_dump(mode="json"),
) != (intent.implementation_revision, descriptor["composition_digest"], descriptor["workspace"]):
raise BuilderExecutionPolicyError("native_binding_drift")
return None
if intent.kind == "model":
_context, definition = self._definition(ref=ref, descriptor=descriptor, for_projection=for_projection)
if intent.implementation_revision != definition.snapshot_id or intent.target != self._model_target(
definition
):
raise BuilderExecutionPolicyError("native_binding_drift")
return None
current, definition, tool, selected_account = self._tool(
ref=ref, target=intent.target, descriptor=descriptor, for_projection=for_projection
)
saved, _proposal, assessment = self.snapshot(ref=ref, key=intent.key)
if assessment.destructive:
raise BuilderExecutionPolicyError("destructive_effect_forbidden")
if canonical_json(saved.model_dump(mode="json")) != canonical_json(intent.model_dump(mode="json")) or (
tool.account_ref,
tool.metadata.implementation_revision,
canonical_digest(
{
"definition": definition.stamp,
"declaration": tool.metadata.entity.model_dump(mode="json"),
"runtime": tool.runtime_parameters,
}
),
) != (intent.account_ref, intent.implementation_revision, intent.capability_digest):
raise BuilderExecutionPolicyError("native_binding_drift")
if selected_account is not None and NativeAgentCredentialMaterializer.refresh_due(selected_account):
raise BuilderExecutionPolicyError("native_refresh_continuation_required")
return tool
def invoke_tool(self, *, request, session, invoke):
envelope = request.native_dispatch
if envelope is None:
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
self.validate_caller(ref=api_ref(envelope.ref), caller=request.caller)
target = {
"provider_type": request.tool.provider_type,
"provider_id": request.tool.provider_id,
"tool_name": request.tool.tool_name,
}
if canonical_json(envelope.intent.target) != canonical_json(target) or canonical_json(
envelope.intent.parameters
) != canonical_json({**request.tool.runtime_parameters, **request.tool.tool_parameters}):
raise BuilderExecutionPolicyError("native_endpoint_arguments_mismatch")
runtime = _native_tool_runtime(self, envelope=envelope, request=request)
response = None
def business():
nonlocal response
response = invoke(request, session=session, prepared_runtime=runtime)
return response.observation
# Runtime construction above is inert except authorized credential decryption.
# The SQL dispatch claim is committed immediately before ToolEngine's owner.
AgentToolDispatchOwner(
authorization=self.bridge.authorization, state_store=self.state_store, revalidate=self.revalidate
).invoke(envelope=envelope, invoke=business)
assert response is not None
return response
def store_result(self, *, ref, key, result, native_dispatch=None, shell_job=None):
if native_dispatch is not None:
try:
envelope = AgentPhysicalDispatchPayload.model_validate(native_dispatch)
except ValueError:
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch") from None
if envelope.ref != ref or envelope.intent.key != key:
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
endpoint = self.validate_claimed_shell(envelope=envelope)
if shell_job is not None:
try:
shell_job = AgentShellJobReceipt.model_validate(shell_job.model_dump(mode="json"))
except (ValueError, AttributeError):
raise BuilderExecutionPolicyError("native_agent_job_unavailable") from None
if shell_job.binding_ref != endpoint.config.workspace.backend_binding_ref:
raise BuilderExecutionPolicyError("native_agent_job_unavailable")
result_ref = self.state_store.store_result(
ref=api_ref(ref), key=api_intent(envelope.intent).key, result=result
)
receipt_failed = False
try:
if shell_job is not None:
native_ref = api_ref(ref)
owned = self.loader.load(ref=native_ref)
receipt = self.state_store.prepare_object(
ref=native_ref,
tenant_id=owned.tenant_id,
kind="result",
logical_key=self._shell_ack_key(envelope),
payload=self._shell_ack_payload(
envelope=envelope, endpoint=endpoint, result_ref=result_ref, shell_job=shell_job
),
)
# Write once / compare if existing, using the ordinary encrypted
# object acknowledgement; the replayable result stays unwrapped.
with self.loader.locked(ref=native_ref, settlement=True) as (session, _, _):
self.state_store.acknowledge_object(session, receipt)
except Exception:
# The actual result is already durable. Metadata uncertainty
# must not relabel that known ACK unknown or reopen its effect.
receipt_failed = True
self.bridge.authorization.record_outcome(
permit=api_permit(envelope.permit), outcome="completed", result_ref=result_ref
)
if receipt_failed:
raise BuilderExecutionPolicyError("native_agent_job_unavailable")
return result_ref
if shell_job is not None:
raise BuilderExecutionPolicyError("native_agent_job_unavailable")
native_ref = api_ref(ref)
self._endpoint(native_ref)
native_key = OperationKey.model_validate(key.model_dump(mode="json"))
owned = self.loader.load(ref=native_ref)
with self.loader.session_factory() as session:
operation = session.scalar(
select(NativeEffectOperation).where(
NativeEffectOperation.tenant_id == owned.tenant_id,
NativeEffectOperation.request_id == native_ref.request_id,
NativeEffectOperation.invocation_id == native_ref.invocation_id,
NativeEffectOperation.operation_digest == operation_digest(native_key),
NativeEffectOperation.state == "completed",
)
)
if operation is None or operation.result_ref is None:
raise BuilderExecutionPolicyError("native_result_unavailable")
result_ref = operation.result_ref
actual = self.state_store.load_result(ref=native_ref, result_ref=result_ref)
if canonical_json(actual) != canonical_json(result):
raise BuilderExecutionPolicyError("native_result_conflict")
return result_ref
def read_result(self, *, ref, result_ref):
native_ref = api_ref(ref)
self._endpoint(native_ref)
return self.state_store.load_result(ref=native_ref, result_ref=result_ref)
def validate_call(self, *, ref, intent):
native_ref, effect = api_ref(ref), api_intent(intent)
self._endpoint(native_ref)
self.revalidate(ref=native_ref, intent=effect)
def settle(self, *, ref, permit, outcome, result_ref):
from dify_agent.protocol.native_testing import AgentEffectReceipt
native_ref, native_permit = api_ref(ref), api_permit(permit)
if self.bridge.authorization._permit_ref(native_permit) != native_ref:
raise BuilderExecutionPolicyError("invalid_dispatch_permit")
with self.loader.session_factory() as session:
existing = session.scalar(
select(NativeEffectOutcome).where(
NativeEffectOutcome.tenant_id == self.loader.actor.tenant_id,
NativeEffectOutcome.request_id == native_ref.request_id,
NativeEffectOutcome.invocation_id == native_ref.invocation_id,
NativeEffectOutcome.operation_id == native_permit.operation_id,
NativeEffectOutcome.attempt_id == native_permit.attempt_id,
)
)
if existing is not None:
# A backend lost-response report cannot downgrade an API-owned
# completed result. Exact permit proof above remains mandatory.
if (outcome, result_ref) != (existing.outcome, existing.result_ref) and not (
outcome == "unknown" and result_ref is None and existing.outcome == "completed"
):
raise BuilderExecutionPolicyError("outcome_conflict")
return AgentEffectReceipt(
operation_id=existing.operation_id,
attempt_id=existing.attempt_id,
outcome=existing.outcome,
result_ref=existing.result_ref,
)
if outcome == "completed":
raise BuilderExecutionPolicyError("native_result_unavailable")
receipt = self.bridge.authorization.record_outcome(permit=native_permit, outcome=outcome, result_ref=result_ref)
return AgentEffectReceipt.model_validate(receipt.model_dump(mode="json"))
def describe(self, *, actor, resolved):
if actor != self.loader.actor:
raise BuilderExecutionPolicyError("native_owner_mismatch")
intent, proposal, assessment = self.snapshot(ref=resolved.ref, key=resolved.intent.key)
if intent != resolved.intent:
raise BuilderExecutionPolicyError("native_pending_conflict")
self.revalidate(ref=resolved.ref, intent=intent, for_projection=True)
with self.loader.session_factory() as session:
owned, _scope = self.loader._load(session, ref=resolved.ref, lock=False)
pending = session.scalar(
select(NativePendingAction)
.join(NativeEffectOperation, NativeEffectOperation.id == NativePendingAction.operation_id)
.where(
NativePendingAction.tenant_id == owned.tenant_id,
NativePendingAction.request_id == resolved.ref.request_id,
NativePendingAction.invocation_id == resolved.ref.invocation_id,
NativePendingAction.category == "approval",
NativeEffectOperation.operation_digest == operation_digest(intent.key),
NativeEffectOperation.intent_digest == canonical_digest(intent.model_dump(mode="json")),
)
)
if pending is None or pending.generation != resolved.generation:
raise BuilderExecutionPolicyError("native_pending_conflict")
return PendingActionDisclosure(
pending_operation_id=pending.id,
wait_reason="approval",
version=resolved.generation + 1,
scope_digest=pending.owner_version,
account_label=assessment.account_label,
action=assessment.action,
target=assessment.target_summary,
parameters_summary=assessment.parameters_summary,
risk_notice=assessment.risk_notice + " " + _OPACITY,
required_configuration=(),
native_form_ref=None,
approval_limits=proposal.terms,
)
def build_native_agent_operations(*, actor: Actor, assessor: Callable) -> AgentOperationService:
loader = OwnedNativeTestLoader(
sessionmaker(bind=db.engine, expire_on_commit=False), actor=actor, resolver=AgentOperationBindingResolver()
)
return AgentOperationService(loader=loader, state_store=NativeExecutionStateStore(loader), assessor=assessor)
def agent_operations_for_ref(ref):
from services.dify_builder.native_testing.agent_bridge import agent_bridge_for_ref
bridge = agent_bridge_for_ref(ref)
factory = current_app.extensions.get("native_agent_operation_factory")
if factory is None:
from services.dify_builder.wiring import build_native_agent_operation_service
factory = build_native_agent_operation_service
service = factory(actor=bridge.authorization.loader.actor)
if not isinstance(service, AgentOperationService) or service.loader.actor != bridge.authorization.loader.actor:
raise BuilderExecutionPolicyError("native_agent_operation_unavailable")
return service
def _native_tool_runtime(service, *, envelope, request):
"""Build static plugin runtime after approval; business phase cannot refresh."""
from core.app.entities.app_invoke_entities import InvokeFrom
from core.plugin.entities.plugin_daemon import CredentialType
from core.tools.__base.tool_runtime import ToolRuntime
from core.tools.entities.tool_entities import ToolInvokeFrom
from core.tools.plugin_tool.tool import PluginTool
from services.dify_builder.native_testing.agent_bridge import AgentCredentialBinding
ref, intent, permit = api_ref(envelope.ref), api_intent(envelope.intent), api_permit(envelope.permit)
service.bridge.authorization.validate_dispatch(ref=ref, intent=intent, permit=permit)
tool = service.revalidate(ref=ref, intent=intent)
if request.tool.credential_id != tool.credential_id or canonical_json(
request.tool.runtime_parameters
) != canonical_json(tool.runtime_parameters):
raise BuilderExecutionPolicyError("native_credential_drift")
if tool.provider_type not in {"plugin", "builtin"} or tool.provider_id.count("/") != 2:
raise BuilderExecutionPolicyError("native_agent_tool_preparation_unavailable")
materializer = NativeAgentCredentialMaterializer(
authorization=service.bridge.authorization,
resolve_binding=lambda **_kwargs: AgentCredentialBinding(
actor=service.loader.actor,
tool_ref=intent.resource_ref,
provider_id=tool.provider_id,
credential_id=tool.credential_id,
verified_plugin=tool.verified_plugin,
implementation_revision=tool.metadata.implementation_revision,
requirements_ref=intent.resource_ref,
),
)
if tool.credential_id is None:
credentials, credential_type = {}, CredentialType.UNAUTHORIZED
else:
materialized = materializer.materialize(
ref=ref, intent=intent, permit=permit, tool_ref=intent.resource_ref, phase="business"
)
credentials, credential_type = materialized.credentials, CredentialType.of(materialized.credential_type)
return PluginTool(
entity=tool.metadata.entity,
runtime=ToolRuntime(
tenant_id=request.caller.tenant_id,
user_id=request.caller.user_id,
credentials=credentials,
credential_type=credential_type,
runtime_parameters=dict(tool.runtime_parameters),
invoke_from=InvokeFrom.value_of(request.caller.invoke_from),
tool_invoke_from=ToolInvokeFrom.AGENT,
),
tenant_id=request.caller.tenant_id,
icon="",
plugin_unique_identifier=tool.metadata.implementation_revision,
)
def confirmed_task_model(*, session, owned, intent) -> bool:
if intent.kind != "model":
return False
from models.dify_builder import DifyBuilderExecutionRequest
request = session.get(DifyBuilderExecutionRequest, owned.ref.request_id)
descriptor = owned.bindings.get(_RESOURCE_KEY, {}).get(operation_digest(intent.key))
return bool(
request is not None
and request.context.get("profile", {}).get("profile_id") == "builder-agent-confirmed-v1"
and descriptor is not None
and descriptor.get("task_model") is True
and descriptor.get("model_target") == intent.target
and descriptor.get("snapshot_id") == intent.implementation_revision
)
def confirmed_deferred_tool_approval(*, session, owned, intent, binding, fence) -> bool:
"""Register another actual call behind this Agent's own approval fence only.
The current exact binding was already revalidated by C02. This returns no
execution authority; the caller must return the new call's pending approval.
"""
if (
intent.kind != "tool"
or binding.approval_scope != "operation"
or intent.key.occurrence_path[:1] != (binding.execution_id,)
or fence.category != "approval"
or fence.invocation_id != owned.ref.invocation_id
):
return False
descriptor = owned.bindings.get(_RESOURCE_KEY, {}).get(operation_digest(intent.key))
if descriptor is None or descriptor.get("task_model") is True:
return False
from models.dify_builder import DifyBuilderExecutionRequest
request = session.get(DifyBuilderExecutionRequest, owned.ref.request_id)
if request is None or request.context.get("profile", {}).get("profile_id") != "builder-agent-confirmed-v1":
return False
state = session.scalar(owner_query(NativeInvocationState, owned.ref, owned.tenant_id))
if state is None or state.wait_reason != "approval" or fence.generation != state.generation:
return False
rows = session.execute(
select(NativePendingAction, NativeEffectOperation)
.join(NativeEffectOperation, NativeEffectOperation.id == NativePendingAction.operation_id)
.where(
NativePendingAction.tenant_id == owned.tenant_id,
NativePendingAction.request_id == owned.ref.request_id,
NativePendingAction.id.in_(state.pending_ids),
)
).all()
if len(rows) != len(state.pending_ids):
return False
for pending, operation in rows:
previous = owned.bindings.get(_RESOURCE_KEY, {}).get(operation.operation_digest)
previous_binding = owned.bindings.get(operation.operation_digest)
if (
(pending.invocation_id, pending.generation, pending.category)
!= (owned.ref.invocation_id, state.generation, "approval")
or (operation.tenant_id, operation.request_id, operation.invocation_id, operation.state)
!= (owned.tenant_id, owned.ref.request_id, owned.ref.invocation_id, "pending")
or previous is None
or previous.get("task_model") is True
or previous_binding is None
or previous_binding.get("execution_id") != binding.execution_id
or previous_binding.get("approval_scope") != "operation"
or any(
not descriptor.get(field) or previous.get(field) != descriptor[field]
for field in ("node_id", "agent_id", "snapshot_id", "snapshot_digest", "graph_revision")
)
):
return False
return True
def admit_confirmed_agent_execution(
*, actor, session_id, test_input_id, expected_version, native_run_id, idempotency_key, limits
):
"""Reserve existing C02/C03 owners for one ordinary confirmed Workflow Run.
Caller has validated the outer Test Run confirmation and owns the Run ID.
This never launches a generator, qualifies isolation, or authorizes tool I/O.
Admission may precede the WorkflowRun row; actual private effects require it.
"""
from datetime import timedelta
from uuid import NAMESPACE_URL, uuid5
from models.dify_builder import DifyBuilderExecutionRequest, DifyBuilderSession
from models.dify_builder_native_tests import NativeTestScope
from services.dify_builder.native_testing.context_binding import NativeAdmissionCompiler, NativeRuntimeProfile
from services.dify_builder.native_testing.repository import utc_now
sessions = sessionmaker(bind=db.engine, expire_on_commit=False)
profile = NativeRuntimeProfile(
profile_id="builder-agent-confirmed-v1", implementation_revision="builder-agent-confirmed-v1", limits=limits
)
if not {"calls", "dispatches", "cost_units"}.issubset(limits):
raise BuilderExecutionPolicyError("native_profile_unavailable")
def resources(*, actor, app_id, workflow_id, graph, profile):
result = []
with sessions() as session:
for node in graph.get("nodes", []):
data = node.get("data", {})
if data.get("type") != "agent" or data.get("version") != "2":
continue
pin = data.get("agent_binding", {})
snapshot = session.get(AgentConfigSnapshot, pin.get("current_snapshot_id"))
agent = session.get(Agent, pin.get("agent_id"))
if (
snapshot is None
or agent is None
or (snapshot.tenant_id, snapshot.agent_id, agent.tenant_id, agent.active_config_snapshot_id)
!= (actor.tenant_id, agent.id, actor.tenant_id, snapshot.id)
):
raise BuilderExecutionPolicyError("native_agent_pin_mismatch")
soul = AgentSoulConfig.model_validate(snapshot.config_snapshot_dict)
result.append(
{
"kind": "agent",
"app_id": app_id,
"workflow_id": workflow_id,
"node_id": node["id"],
"agent_id": agent.id,
"snapshot_id": snapshot.id,
"snapshot_digest": canonical_digest(soul.model_dump(mode="json")),
}
)
return tuple(result)
compiler = NativeAdmissionCompiler(session_factory=sessions, resources=resources)
request_id = str(
uuid5(
NAMESPACE_URL,
canonical_digest(
{
"profile": profile.profile_id,
"tenant": actor.tenant_id,
"actor": actor.account_id,
"session": session_id,
"key": idempotency_key,
}
),
)
)
invocation_id = str(uuid5(NAMESPACE_URL, request_id + ":root"))
compilation = compiler.compile(
actor=actor,
session_id=session_id,
test_input_id=test_input_id,
expected_version=expected_version,
mode="native",
request_id=request_id,
invocation_id=invocation_id,
native_run_id=native_run_id,
profile=profile,
revision_basis="confirmed-canvas-v1",
)
context = compilation.context
from services.dify_builder.native_testing.repository import native_observation_transaction
with native_observation_transaction(sessions, session_id=lambda: session_id) as session:
session.scalar(
select(DifyBuilderSession)
.where(
DifyBuilderSession.id == session_id,
DifyBuilderSession.tenant_id == actor.tenant_id,
DifyBuilderSession.owner_account_id == actor.account_id,
)
.with_for_update()
)
compiler.recheck_local(session=session, actor=actor, compilation=compilation)
existing = session.get(DifyBuilderExecutionRequest, request_id)
if existing is not None:
if (
existing.tenant_id,
existing.actor_id,
existing.session_id,
existing.test_input_id,
existing.context_digest,
existing.context,
) != (
actor.tenant_id,
actor.account_id,
session_id,
test_input_id,
context.ref.context_digest,
compilation.request_context,
):
raise BuilderExecutionPolicyError("native_idempotency_conflict")
else:
session.add(
DifyBuilderExecutionRequest(
id=request_id,
tenant_id=actor.tenant_id,
actor_id=actor.account_id,
app_id=context.app_id,
workflow_id=context.workflow_id,
session_id=session_id,
test_input_id=test_input_id,
context=compilation.request_context,
context_digest=context.ref.context_digest,
state="running",
native_run_id=None,
observations=[],
)
)
session.add(
NativeTestScope(
request_id=request_id,
tenant_id=actor.tenant_id,
generation=0,
task_approved=True,
cancelled=False,
# One admission wall-clock cap, including confirmation waits.
# Reconnect and existing admissions never renew this scope.
expires_at=utc_now()
+ timedelta(
seconds=min(dify_config.APP_MAX_EXECUTION_TIME, dify_config.WORKFLOW_MAX_EXECUTION_TIME)
),
max_calls=limits["calls"],
max_dispatches=limits["dispatches"],
max_cost_units=limits["cost_units"],
spent_calls=0,
spent_dispatches=0,
spent_cost_units=0,
budget_limits=limits,
budget_spent={},
profile=dict(limits),
)
)
session.add(
NativeTestInvocation(
tenant_id=actor.tenant_id,
request_id=request_id,
invocation_id=invocation_id,
active=True,
bindings={},
)
)
loader = OwnedNativeTestLoader(sessions, actor=actor, resolver=AgentOperationBindingResolver())
NativeExecutionStateStore(loader).initialize_context(context=context)
return context