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

1600 lines
74 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 typing import 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
@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]
@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
def _resource_key(key: OperationKey) -> str:
return "agent-intent:" + operation_digest(key)
def _sql_resource(*, session, owned, descriptor):
"""SQL-only current pin/account recheck inside the existing C02 transaction."""
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,
)
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")
class AgentOperationBindingResolver:
"""Current SQL resources; no decryption, OAuth, runtime schema or business I/O."""
def resolve_binding(self, *, session, owned, key):
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)
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), 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
)
context, definition, tool, selected_account = self._tool(ref=native_ref, target=target)
# 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)
)
intent = EffectIntent(
key=native_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,
}
),
)
owned = self.loader.load(ref=native_ref)
proposal = NativeApprovalProposal(
key=native_key,
terms=NativeApprovalTerms(
max_calls=1, max_dispatches=1, max_cost_units=1, expires_at=owned.expires_at.isoformat()
),
)
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 self._persist(
ref=native_ref,
intent=intent,
proposal=proposal,
assessment=assessment,
descriptor=descriptor,
binding=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 = self.state_store.load_prepared_config(
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 = self.state_store.load_prepared_config(
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)
)
if workspace != prepared.workspace:
raise BuilderExecutionPolicyError("native_workspace_owner_mismatch")
configs["workspace_stamp"] = stamp
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=owned.expires_at.isoformat()
),
)
return self._persist(
ref=ref, intent=intent, proposal=proposal, assessment=assessment, descriptor=descriptor, binding=binding
)
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):
context, definition = self._definition(ref=ref)
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=owned.expires_at.isoformat()
),
)
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 self._persist(
ref=ref, intent=intent, proposal=proposal, assessment=assessment, descriptor=descriptor, binding=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]):
from dify_agent.adapters.llm.provider import ProtectedModelChunkReceipt
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():
# Share the SDK's bounded canonical receipt before exposing a chunk.
stage = "receipt_append"
receipt.append(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):
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)
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 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