mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 13:51:25 +08:00
2094 lines
98 KiB
Python
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
|