mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 22:02:23 +08:00
1024 lines
47 KiB
Python
1024 lines
47 KiB
Python
"""Dify's per-delivery composition around the existing native generator.
|
|
|
|
SQL claims, Graphon execution, opaque observation and physical cleanup retain
|
|
separate existing owners. A delivery captures its immutable dispatch identity;
|
|
queue messages never supply actors, checkpoint payloads or cleanup authority.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from threading import RLock
|
|
from typing import TYPE_CHECKING
|
|
|
|
from sqlalchemy import select
|
|
|
|
from core.dify_builder.models import Actor
|
|
from models.dify_builder import DifyBuilderExecutionRequest
|
|
from models.dify_builder_native_tests import NativeContinuation, NativeDispatch, NativePayloadObject
|
|
from models.enums import CreatorUserRole
|
|
from models.workflow import WorkflowRun
|
|
from runtime_contracts.builder.execution import BuilderExecutionPolicyError, canonical_digest
|
|
from runtime_contracts.builder.native.contracts import CheckpointRef
|
|
from services.dify_builder.native_testing.http_binding import build_native_http_profile
|
|
from services.dify_builder.native_testing.lifecycle import NativeDispatchRef, NativeTestLifecycleService
|
|
from services.dify_builder.native_testing.observation_repository import bind_native_repository_observation
|
|
from services.dify_builder.native_testing.state_store import invocation_state, owner_query
|
|
|
|
if TYPE_CHECKING:
|
|
from core.app.entities.app_invoke_entities import WorkflowAppGenerateEntity
|
|
from graphon.engine.filter import ResponseStreamFilter
|
|
from services.dify_builder.native_testing.context_runtime import NativeGraphRuntimeState
|
|
from services.dify_builder.native_testing.host_execution import NativeHostLaunch
|
|
from services.dify_builder.native_testing.host_lifetime import NativeHostLifetimeRegistry
|
|
|
|
|
|
def _profile_matches(raw, qualification):
|
|
from services.dify_builder.native_testing.context_binding import (
|
|
NativeRuntimeProfile,
|
|
)
|
|
from services.dify_builder.native_testing.sandbox.qualification import (
|
|
NativeCodeQualification,
|
|
)
|
|
|
|
if raw == build_native_http_profile().model_dump(mode="json"):
|
|
return True
|
|
if type(qualification) is not NativeCodeQualification:
|
|
return False
|
|
qualification.validate_profile(NativeRuntimeProfile.model_validate(raw))
|
|
return True
|
|
|
|
|
|
def _profile_resolver(raw, qualification):
|
|
from services.dify_builder.native_testing.http_binding import (
|
|
NativeHttpBindingResolver,
|
|
)
|
|
from services.dify_builder.native_testing.sandbox.qualification import (
|
|
NativeCodeBindingResolver,
|
|
)
|
|
|
|
if raw == build_native_http_profile().model_dump(mode="json"):
|
|
return NativeHttpBindingResolver()
|
|
if not _profile_matches(raw, qualification):
|
|
raise BuilderExecutionPolicyError("native_code_qualification_unavailable")
|
|
return NativeCodeBindingResolver(qualification=qualification)
|
|
|
|
|
|
class DifyNativeResumeSettlement:
|
|
"""Bounded SQL-only check in C03's existing resume writer.
|
|
|
|
A committed wait is visible before the acquisition's cleanup and dispatch
|
|
finalization. Neither the wait nor an absent process handle proves those
|
|
acknowledgements. Inspect the source local generation, not scope generation.
|
|
"""
|
|
|
|
def __init__(self, *, code_qualification=None):
|
|
# Existing HTTP callers have no Code qualification owner.
|
|
self.code_qualification = code_qualification
|
|
|
|
def validate(self, *, session, owned, state, checkpoint: CheckpointRef) -> None:
|
|
dispatches = session.scalars(
|
|
owner_query(NativeDispatch, owned.ref, owned.tenant_id).where(NativeDispatch.generation == state.generation)
|
|
).all()
|
|
if state.cleanup_complete is not True or len(dispatches) != 1 or dispatches[0].state != "completed":
|
|
raise BuilderExecutionPolicyError("native_resume_unsettled")
|
|
request = session.scalar(
|
|
select(DifyBuilderExecutionRequest).where(
|
|
DifyBuilderExecutionRequest.id == owned.ref.request_id,
|
|
DifyBuilderExecutionRequest.tenant_id == owned.tenant_id,
|
|
DifyBuilderExecutionRequest.actor_id == owned.actor_id,
|
|
)
|
|
)
|
|
saved = session.scalar(
|
|
owner_query(NativeContinuation, owned.ref, owned.tenant_id).where(
|
|
NativeContinuation.generation == state.generation,
|
|
NativeContinuation.checkpoint_id == checkpoint.checkpoint_id,
|
|
NativeContinuation.payload_digest == checkpoint.payload_digest,
|
|
)
|
|
)
|
|
payload = session.scalar(
|
|
owner_query(NativePayloadObject, owned.ref, owned.tenant_id).where(
|
|
NativePayloadObject.id == checkpoint.checkpoint_id, NativePayloadObject.kind == "checkpoint"
|
|
)
|
|
)
|
|
if (
|
|
request is None
|
|
or not _profile_matches(request.context.get("profile"), self.code_qualification)
|
|
or canonical_digest(request.context) != owned.ref.context_digest
|
|
or saved is None
|
|
or payload is None
|
|
or state.checkpoint_id != checkpoint.checkpoint_id
|
|
):
|
|
raise BuilderExecutionPolicyError("native_resume_unsettled")
|
|
run = session.scalar(
|
|
select(WorkflowRun).where(
|
|
WorkflowRun.id == request.context.get("native_run_id"),
|
|
WorkflowRun.tenant_id == owned.tenant_id,
|
|
WorkflowRun.app_id == state.app_id,
|
|
WorkflowRun.workflow_id == state.workflow_id,
|
|
WorkflowRun.created_by == owned.actor_id,
|
|
WorkflowRun.created_by_role == CreatorUserRole.ACCOUNT,
|
|
WorkflowRun.status == "paused",
|
|
)
|
|
)
|
|
if run is None:
|
|
raise BuilderExecutionPolicyError("native_resume_unsettled")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class NativeHostResumeBindings:
|
|
"""Private exact objects reconstructed only from an authentic claimed C03."""
|
|
|
|
entity: WorkflowAppGenerateEntity
|
|
runtime_state: NativeGraphRuntimeState
|
|
response_filter: ResponseStreamFilter
|
|
checkpoint: CheckpointRef
|
|
payload_digest: str
|
|
|
|
|
|
def restore_claimed_host(*, host: NativeHostLaunch, lifecycle: NativeTestLifecycleService, expected_payload):
|
|
from core.app.entities.app_invoke_entities import WorkflowAppGenerateEntity
|
|
from graphon.engine.filter import ResponseStreamFilter
|
|
from services.dify_builder.native_testing.context_runtime import NativeGraphRuntimeState
|
|
|
|
context = host.lifetime.context
|
|
journal = host._journal
|
|
if (
|
|
type(lifecycle) is not NativeTestLifecycleService
|
|
or journal is None
|
|
or lifecycle.store is not journal._state
|
|
or context.generation <= 0
|
|
or type(lifecycle.resume_settlement) is not DifyNativeResumeSettlement
|
|
):
|
|
raise BuilderExecutionPolicyError("native_resume_owner_mismatch")
|
|
checkpoint = lifecycle.claimed_resume_checkpoint(
|
|
ref=context.ref, generation=context.generation, dispatch_id=host.lifetime.dispatch_id
|
|
)
|
|
actual = lifecycle.store.read_checkpoint(ref=context.ref, checkpoint=checkpoint)
|
|
if actual != expected_payload or actual.native_run_id != context.native_run_id:
|
|
raise BuilderExecutionPolicyError("native_journal_payload_mismatch")
|
|
journal.restore_claimed(
|
|
ref=context.ref,
|
|
generation=context.generation,
|
|
dispatch_id=host.lifetime.dispatch_id,
|
|
lifecycle=lifecycle,
|
|
expected_payload=actual,
|
|
)
|
|
with host.observation_scope():
|
|
entity = WorkflowAppGenerateEntity.model_validate_json(actual.generate_entity_json)
|
|
runtime = NativeGraphRuntimeState.from_snapshot(actual.graph_runtime_state_json)
|
|
response_filter = ResponseStreamFilter()
|
|
response_filter.loads(actual.response_stream_filter_json)
|
|
if (
|
|
type(runtime) is not NativeGraphRuntimeState
|
|
or entity.native_test_required is not True
|
|
or entity.native_test_ref != context.ref
|
|
or entity.workflow_execution_id != context.native_run_id
|
|
or entity.user_id != context.actor_id
|
|
or (entity.app_config.tenant_id, entity.app_config.app_id, entity.app_config.workflow_id)
|
|
!= (context.tenant_id, context.app_id, context.workflow_id)
|
|
or entity.trace_manager is not None
|
|
or entity.builder_execution is not None
|
|
or not entity.stream
|
|
):
|
|
raise BuilderExecutionPolicyError("native_resume_identity_mismatch")
|
|
if (
|
|
lifecycle.store.load_context(ref=context.ref) != context
|
|
or lifecycle.claimed_resume_checkpoint(
|
|
ref=context.ref, generation=context.generation, dispatch_id=host.lifetime.dispatch_id
|
|
)
|
|
!= checkpoint
|
|
):
|
|
raise BuilderExecutionPolicyError("native_generation_conflict")
|
|
return NativeHostResumeBindings(entity, runtime, response_filter, checkpoint, checkpoint.payload_digest)
|
|
|
|
|
|
class DifyNativeRuntime:
|
|
"""One actual acquisition, built inactive before C03 claims its delivery."""
|
|
|
|
def __init__(
|
|
self, *, delivery: NativeDispatchRef, lifecycle, registry: NativeHostLifetimeRegistry, code_owner_factory=None
|
|
):
|
|
from services.dify_builder.native_testing.authorization import EffectAuthorizationService
|
|
from services.dify_builder.native_testing.checkpoint_bridge import NativeCheckpointBridge
|
|
from services.dify_builder.native_testing.host_capacity import NativeHostCapacity
|
|
from services.dify_builder.native_testing.host_execution import NativeHostLaunch
|
|
from services.dify_builder.native_testing.host_lifetime import NativeHostLifetime
|
|
from services.dify_builder.native_testing.http_binding import native_http_config
|
|
from services.dify_builder.native_testing.operation_journal import NodeOperationJournal
|
|
|
|
if (
|
|
type(delivery) is not NativeDispatchRef
|
|
or type(lifecycle.resume_settlement) is not DifyNativeResumeSettlement
|
|
):
|
|
raise BuilderExecutionPolicyError("native_runtime_composition_mismatch")
|
|
context = lifecycle.store.load_context(ref=delivery.ref)
|
|
if context.generation != delivery.generation:
|
|
raise BuilderExecutionPolicyError("native_generation_conflict")
|
|
if lifecycle._dispatch_ref is not None:
|
|
raise BuilderExecutionPolicyError("native_runtime_composition_mismatch")
|
|
lifecycle._dispatch_ref = delivery
|
|
self.delivery, self.lifecycle, self.registry = delivery, lifecycle, registry
|
|
self.context = context
|
|
self.authorization = EffectAuthorizationService(lifecycle.loader)
|
|
journal = NodeOperationJournal(state=lifecycle.store, authorization=self.authorization)
|
|
checkpoints = NativeCheckpointBridge(state=lifecycle.store, lifecycle=lifecycle)
|
|
lifetime = NativeHostLifetime(context=context, dispatch_id=delivery.dispatch_id)
|
|
timeout = native_http_config().default_timeout()
|
|
if timeout.connect is None or timeout.read is None or timeout.write is None:
|
|
raise BuilderExecutionPolicyError("native_http_profile_drift")
|
|
self.host = NativeHostLaunch(
|
|
context=context,
|
|
dispatch_id=delivery.dispatch_id,
|
|
lifetime=lifetime,
|
|
journal=journal,
|
|
checkpoints=checkpoints,
|
|
fence_pending=lifecycle.fence_pending,
|
|
stop_sealer=lifecycle,
|
|
authorization=self.authorization,
|
|
http_baseline=(timeout.connect, timeout.read, timeout.write),
|
|
)
|
|
self.host.bind_cancellation_fence(fence=DifyNativeCancellationFence(lifecycle=lifecycle, delivery=delivery))
|
|
# Existing API delivery remains HTTP-profile-bound. No caller installs
|
|
# Code; its table is queried only for an explicitly composed owner.
|
|
from services.dify_builder.native_testing.sandbox.qualification import (
|
|
NativeCodeBindingResolver,
|
|
)
|
|
|
|
self.code_qualification = (
|
|
lifecycle.loader.resolver.qualification
|
|
if type(lifecycle.loader.resolver) is NativeCodeBindingResolver
|
|
else None
|
|
)
|
|
if self.code_qualification is not None:
|
|
from services.dify_builder.native_testing.sandbox.host import (
|
|
NativeCodeHostOwner,
|
|
)
|
|
|
|
self.code_qualification.check()
|
|
if code_owner_factory is not None:
|
|
raise BuilderExecutionPolicyError("native_code_host_composition_mismatch")
|
|
code_owner_factory = NativeCodeHostOwner
|
|
self.code_owner = None
|
|
if code_owner_factory is not None:
|
|
from services.dify_builder.native_testing.sandbox.host import (
|
|
NativeCodeHostOwner,
|
|
)
|
|
|
|
self.code_owner = code_owner_factory(host=self.host, loader=lifecycle.loader)
|
|
if type(self.code_owner) is not NativeCodeHostOwner or self.code_owner.host is not self.host:
|
|
raise BuilderExecutionPolicyError("native_code_host_composition_mismatch")
|
|
self.capacity = NativeHostCapacity(lifecycle, cleanup_verifier=lifetime, code_cleanup=self.code_owner)
|
|
self._journal, self._checkpoints = journal, checkpoints
|
|
self.host.bind_observation()
|
|
self._started = False
|
|
self._lock = RLock()
|
|
|
|
def _start(self, context):
|
|
from services.dify_builder.native_testing.native_execution import DifyNativeWorkflowOwner
|
|
from services.dify_builder.native_testing.stream_runtime import DifyNativeStreamOwner
|
|
|
|
with self._lock:
|
|
if self._started or context != self.context:
|
|
raise BuilderExecutionPolicyError("native_delivery_replayed")
|
|
# SQL claim ACK preceded entry through lifecycle.execute_dispatch.
|
|
# Re-read the exact claimed row before transferring physical ownership.
|
|
with self.lifecycle.loader.session_factory() as session:
|
|
dispatch = session.scalar(
|
|
owner_query(NativeDispatch, context.ref, context.tenant_id).where(
|
|
NativeDispatch.id == self.delivery.dispatch_id,
|
|
NativeDispatch.generation == self.delivery.generation,
|
|
NativeDispatch.state == "claimed",
|
|
)
|
|
)
|
|
if dispatch is None:
|
|
raise BuilderExecutionPolicyError("native_dispatch_conflict")
|
|
self.registry.activate(lifetime=self.host.lifetime)
|
|
self._started = True
|
|
self.host.bind_observation()
|
|
bridge = build_delivery_bridge(runtime=self)
|
|
stream = DifyNativeStreamOwner(
|
|
host=self.host,
|
|
capacity=self.capacity,
|
|
session_factory=self.lifecycle.loader.session_factory,
|
|
state=self.lifecycle.store,
|
|
authorization=self.authorization,
|
|
journal=self._journal,
|
|
checkpoints=self._checkpoints,
|
|
)
|
|
return DifyNativeWorkflowOwner(
|
|
session_factory=self.lifecycle.loader.session_factory,
|
|
runtime=bridge,
|
|
stream_owner=stream,
|
|
native_host=self.host,
|
|
)
|
|
|
|
def _failed_start(self):
|
|
if not self._started or self.host.lifetime._consumed:
|
|
return
|
|
try:
|
|
self.host.request_close(reason="failed")
|
|
# This actual zero-activity predicate rejects any source/reader,
|
|
# attempted worker, native activity, controls or child history.
|
|
self.host.confirm_never_started()
|
|
self.capacity.record_cleanup(
|
|
ref=self.delivery.ref, generation=self.delivery.generation, dispatch_id=self.delivery.dispatch_id
|
|
)
|
|
self.host.retire()
|
|
except BaseException:
|
|
self.host.mark_unknown()
|
|
|
|
def run_initial(self, *, context):
|
|
if self.delivery.generation != 0:
|
|
raise BuilderExecutionPolicyError("native_generation_conflict")
|
|
try:
|
|
return self._start(context).run(context=context, on_event=lambda event: None)
|
|
except BaseException:
|
|
self._failed_start()
|
|
raise
|
|
|
|
def resume(self, *, context, checkpoint, resolutions):
|
|
try:
|
|
owner = self._start(context)
|
|
self.host.restore_claimed_resume(lifecycle=self.lifecycle, expected_payload=checkpoint)
|
|
return owner.resume_claimed(context=context)
|
|
except BaseException:
|
|
self._failed_start()
|
|
raise
|
|
|
|
def cancel(self, *, refs):
|
|
return () # Request-wide coordinator owns retained acquisitions, never a replacement verifier.
|
|
|
|
def recovery_evidence(self, *, context, dispatch_id):
|
|
return "unknown"
|
|
|
|
|
|
class _UnavailableCollaborator:
|
|
def __call__(self, *args, **kwargs):
|
|
raise BuilderExecutionPolicyError("native_capability_unavailable")
|
|
|
|
def __getattr__(self, name):
|
|
raise BuilderExecutionPolicyError("native_capability_unavailable")
|
|
|
|
|
|
def build_delivery_bridge(*, runtime: DifyNativeRuntime):
|
|
from graphon.nodes.http_request.node import HttpRequestNode
|
|
from services.dify_builder.native_testing.http_binding import (
|
|
DeferredNativeHttpClient,
|
|
NativeHttpBindingProducer,
|
|
RefusingNativeHttpFiles,
|
|
native_http_config,
|
|
)
|
|
from services.dify_builder.native_testing.private_reads import validate_invocation_workflow
|
|
from services.dify_builder.native_testing.repository import native_workflow_revision
|
|
from services.dify_builder.native_testing.runtime_bridge import (
|
|
NativeCollaborators,
|
|
NativePreparedInputs,
|
|
NativeRuntimeBridge,
|
|
)
|
|
|
|
host, lifecycle = runtime.host, runtime.lifecycle
|
|
denied = _UnavailableCollaborator()
|
|
|
|
def collaborators(*, association):
|
|
from core.workflow.template_rendering import CodeExecutorJinja2TemplateRenderer
|
|
from graphon.nodes.code.code_node import CodeNode
|
|
from graphon.nodes.template_transform.template_transform_node import (
|
|
TemplateTransformNode,
|
|
)
|
|
from services.dify_builder.native_testing.call_adapter import NativeCallAdapter
|
|
from services.dify_builder.native_testing.host_execution import (
|
|
HostBoundCodeCalls,
|
|
)
|
|
from services.dify_builder.native_testing.sandbox.executor import (
|
|
NativeCodeExecutor,
|
|
)
|
|
from services.dify_builder.native_testing.sandbox.qualification import (
|
|
BOUNDS,
|
|
NativeCodeBindingProducer,
|
|
)
|
|
|
|
if runtime.code_qualification is not None and issubclass(
|
|
association.node_class, (CodeNode, TemplateTransformNode)
|
|
):
|
|
producer = NativeCodeBindingProducer(
|
|
association=association,
|
|
loader=lifecycle.loader,
|
|
state_store=lifecycle.store,
|
|
journal=runtime._journal,
|
|
qualification=runtime.code_qualification,
|
|
)
|
|
calls = HostBoundCodeCalls(
|
|
host=host,
|
|
adapter=NativeCallAdapter(
|
|
state=lifecycle.store,
|
|
authorization=runtime.authorization,
|
|
journal=runtime._journal,
|
|
max_result_bytes=BOUNDS["result_bytes"],
|
|
),
|
|
allowance=26,
|
|
)
|
|
executor = NativeCodeExecutor(
|
|
producer=producer,
|
|
calls=calls,
|
|
owner=runtime.code_owner,
|
|
on_prepared=lambda: host.register_proposals(producer=producer),
|
|
)
|
|
return NativeCollaborators(
|
|
denied,
|
|
denied,
|
|
denied,
|
|
denied,
|
|
denied,
|
|
executor,
|
|
CodeExecutorJinja2TemplateRenderer(executor=executor),
|
|
denied,
|
|
denied,
|
|
denied,
|
|
)
|
|
if runtime.code_qualification is not None:
|
|
return NativeCollaborators(*(denied for _ in range(10)))
|
|
if not issubclass(association.node_class, HttpRequestNode):
|
|
return NativeCollaborators(*(denied for _ in range(10)))
|
|
producer = NativeHttpBindingProducer(
|
|
association=association,
|
|
loader=lifecycle.loader,
|
|
state_store=lifecycle.store,
|
|
journal=runtime._journal,
|
|
)
|
|
|
|
def prepared(*, binding):
|
|
host.register_proposals(producer=producer)
|
|
host.register_http_request(ref=association.ref, binding=binding)
|
|
|
|
client = DeferredNativeHttpClient(
|
|
producer=producer,
|
|
authorization=runtime.authorization,
|
|
calls_factory=host.make_calls,
|
|
on_prepared=prepared,
|
|
)
|
|
return NativeCollaborators(client, *(denied for _ in range(9)))
|
|
|
|
def arguments(*, node_id, node_data, node_class, collaborators):
|
|
from graphon.nodes.code.code_node import CodeNode
|
|
from graphon.nodes.template_transform.template_transform_node import (
|
|
TemplateTransformNode,
|
|
)
|
|
from services.dify_builder.native_testing.sandbox.qualification import (
|
|
code_limits,
|
|
template_limit,
|
|
)
|
|
|
|
kind = node_data.type
|
|
if runtime.code_qualification is not None:
|
|
if kind == "code" and node_class is CodeNode:
|
|
return {
|
|
"code_executor": collaborators.code,
|
|
"code_limits": code_limits(),
|
|
}
|
|
if kind == "template-transform" and node_class is TemplateTransformNode:
|
|
return {
|
|
"jinja2_template_renderer": collaborators.jinja,
|
|
"max_output_length": template_limit(),
|
|
}
|
|
if kind == "http-request":
|
|
raise BuilderExecutionPolicyError("native_capability_unavailable")
|
|
if kind == "http-request" and node_class is HttpRequestNode:
|
|
files = RefusingNativeHttpFiles()
|
|
return {
|
|
"http_request_config": native_http_config(),
|
|
"http_client": collaborators.http,
|
|
"tool_file_manager_factory": files,
|
|
"file_manager": files,
|
|
"file_reference_factory": denied,
|
|
}
|
|
if kind in {"start", "end", "if-else", "loop", "loop-start", "loop-end", "iteration", "iteration-start"}:
|
|
return {}
|
|
raise BuilderExecutionPolicyError("native_capability_unavailable")
|
|
|
|
def inputs(context):
|
|
if lifecycle.store.load_context(ref=context.ref) != context:
|
|
raise BuilderExecutionPolicyError("native_context_mismatch")
|
|
with lifecycle.loader.session_factory() as session:
|
|
workflow = validate_invocation_workflow(
|
|
session,
|
|
tenant_id=context.tenant_id,
|
|
app_id=context.app_id,
|
|
workflow_id=context.workflow_id,
|
|
)
|
|
if native_workflow_revision(workflow) != context.graph_revision:
|
|
raise BuilderExecutionPolicyError("native_revision_drift")
|
|
# The qualified literal HTTP lane has no environmental secrets.
|
|
# Refuse before their decryption/materialization in ordinary preparation.
|
|
if workflow._environment_variables not in (None, "[]", "{}", ""):
|
|
raise BuilderExecutionPolicyError("native_capability_unavailable")
|
|
return NativePreparedInputs(inputs=context.inputs, files=(), environment_variables=())
|
|
|
|
return NativeRuntimeBridge(
|
|
state=lifecycle.store,
|
|
collaborators=collaborators,
|
|
node_arguments=arguments,
|
|
prepare_inputs=inputs,
|
|
boundary_owner=host.boundary_owner,
|
|
control_sink=host.control_sink,
|
|
)
|
|
|
|
|
|
class DifyNativeCancellationSettlement:
|
|
"""Exact lifecycle's existing App-first SQL proof, without a fabricated host."""
|
|
|
|
def __init__(self, *, lifecycle):
|
|
self.lifecycle = lifecycle
|
|
|
|
def finish_unacquired(self, *, ref):
|
|
from services.dify_builder.native_testing.host_capacity import finish_unacquired_cancellation
|
|
|
|
if self.lifecycle.cancellation_settlement is not self:
|
|
raise BuilderExecutionPolicyError("native_runtime_composition_mismatch")
|
|
finish_unacquired_cancellation(self.lifecycle, ref=ref)
|
|
|
|
|
|
class DifyNativeRuntimeOwner:
|
|
"""Bounded application-scoped current acquisitions, never cleanup by ID alone."""
|
|
|
|
def __init__(self, *, code_capability=None):
|
|
from services.dify_builder.native_testing.sandbox.qualification import (
|
|
NativeCodeQualification,
|
|
)
|
|
|
|
if code_capability is not None:
|
|
if type(code_capability) is not NativeCodeQualification:
|
|
raise BuilderExecutionPolicyError("native_code_qualification_unavailable")
|
|
code_capability.check()
|
|
self.code_qualification = code_capability
|
|
self._lock = RLock()
|
|
self._requests = {}
|
|
self._current = {}
|
|
|
|
def delivery(self, *, sessions, delivery: NativeDispatchRef, actor_hint: Actor | None = None):
|
|
from services.dify_builder.native_testing.api_service import (
|
|
NativeTestApiService,
|
|
)
|
|
from services.dify_builder.native_testing.host_lifetime import (
|
|
NativeHostLifetimeRegistry,
|
|
)
|
|
|
|
with sessions() as session:
|
|
request = session.scalar(
|
|
select(DifyBuilderExecutionRequest).where(
|
|
DifyBuilderExecutionRequest.id == delivery.ref.request_id,
|
|
DifyBuilderExecutionRequest.context_digest == delivery.ref.context_digest,
|
|
)
|
|
)
|
|
if request is None:
|
|
raise BuilderExecutionPolicyError("native_owner_mismatch")
|
|
actor = Actor(account_id=request.actor_id, tenant_id=request.tenant_id)
|
|
profile_data = request.context.get("profile")
|
|
if actor_hint is not None and actor_hint != actor:
|
|
raise BuilderExecutionPolicyError("native_owner_mismatch")
|
|
service = NativeTestApiService(
|
|
session_factory=sessions,
|
|
actor=actor,
|
|
resolver=_profile_resolver(profile_data, self.code_qualification),
|
|
admission=None,
|
|
resolution=None,
|
|
runtime=self,
|
|
disclosure=None,
|
|
)
|
|
lifecycle = service.lifecycle
|
|
lifecycle.resume_settlement = DifyNativeResumeSettlement(code_qualification=self.code_qualification)
|
|
lifecycle.cancellation_settlement = DifyNativeCancellationSettlement(lifecycle=lifecycle)
|
|
# The persisted full chain is checked even for a harmless late duplicate.
|
|
# Settlement reads can inspect a sealed source; they grant no new IO.
|
|
from services.dify_builder.execution_policy_service import check_execution_permission
|
|
|
|
with sessions() as session:
|
|
owned, _ = lifecycle.loader._load(session, ref=delivery.ref, lock=False, settlement=True)
|
|
check_execution_permission(actor, owned.app_id, sessions)
|
|
key = (actor.tenant_id, actor.account_id, delivery.ref.request_id)
|
|
identity = (key, delivery.ref.invocation_id)
|
|
with self._lock:
|
|
prior = self._current.get(identity)
|
|
if prior is not None and prior.delivery == delivery:
|
|
return prior.lifecycle
|
|
if prior is not None and not prior.host.lifetime._consumed:
|
|
raise BuilderExecutionPolicyError("native_capacity_busy")
|
|
# This same delivery process owns these registries. A separate Beat
|
|
# process cannot retire them. Reap only durable closed + actually
|
|
# consumed owners before admitting another acquisition; exact retained
|
|
# duplicate delivery above remains a harmless reuse of its verifier.
|
|
self.retire_closed(sessions=sessions)
|
|
with sessions() as session:
|
|
dispatch = session.scalar(
|
|
owner_query(NativeDispatch, delivery.ref, actor.tenant_id).where(
|
|
NativeDispatch.id == delivery.dispatch_id,
|
|
NativeDispatch.generation == delivery.generation,
|
|
)
|
|
)
|
|
if dispatch is None:
|
|
raise BuilderExecutionPolicyError("native_dispatch_conflict")
|
|
if dispatch.state not in {"queued", "published"}:
|
|
# A foreign-process acquisition cannot be replaced with a new
|
|
# physical verifier. C03's nonqueued claim simply refuses it.
|
|
return lifecycle
|
|
with self._lock:
|
|
registry = self._requests.get(key)
|
|
if registry is None:
|
|
if len(self._requests) >= 128:
|
|
raise BuilderExecutionPolicyError("native_runtime_capacity_busy")
|
|
registry = NativeHostLifetimeRegistry()
|
|
self._requests[key] = registry
|
|
# Current context/profile/object IO stays outside the registry lock.
|
|
runtime = DifyNativeRuntime(delivery=delivery, lifecycle=lifecycle, registry=registry)
|
|
with self._lock:
|
|
current = self._current.get(identity)
|
|
if current is not prior or self._requests.get(key) is not registry:
|
|
if current is not None and current.delivery == delivery:
|
|
return current.lifecycle
|
|
raise BuilderExecutionPolicyError("native_capacity_busy")
|
|
self._current[identity] = runtime
|
|
lifecycle.runtime = runtime
|
|
return lifecycle
|
|
|
|
def run_initial(self, *, context):
|
|
raise BuilderExecutionPolicyError("native_dispatch_identity_required")
|
|
|
|
def resume(self, *, context, checkpoint, resolutions):
|
|
raise BuilderExecutionPolicyError("native_dispatch_identity_required")
|
|
|
|
def cancel(self, *, refs):
|
|
with self._lock:
|
|
retained = tuple(self._current.items())
|
|
for ref in refs:
|
|
selected = next(((key, r) for key, r in retained if r.delivery.ref == ref), None)
|
|
if selected is None:
|
|
continue # Foreign-process/absent handles grant no physical proof.
|
|
identity, current = selected
|
|
host, delivery = current.host, current.delivery
|
|
if host.lifetime._consumed:
|
|
continue # Consumption of an older generation never acknowledges a newer one.
|
|
try:
|
|
# Durable cancellation precedes this read. Select the exact local
|
|
# generation and acquired dispatch; ref alone survives resumes.
|
|
with current.lifecycle.loader.session_factory() as session:
|
|
owned, scope = current.lifecycle.loader._load(session, ref=ref, lock=False, settlement=True)
|
|
state = invocation_state(session, ref, owned.tenant_id)
|
|
acquired = current.capacity._latest_acquired(session, ref, owned.tenant_id)
|
|
if (
|
|
not scope.cancelled
|
|
or state.generation != delivery.generation
|
|
or state.cleanup_complete
|
|
or acquired is None
|
|
or (acquired.id, acquired.generation) != (delivery.dispatch_id, delivery.generation)
|
|
):
|
|
continue
|
|
with self._lock:
|
|
if self._current.get(identity) is not current:
|
|
continue
|
|
host.request_close(reason="cancel")
|
|
host.lifetime.settle() # Original deadline; unknown stays unknown.
|
|
host.release_dead_dispatcher_logs()
|
|
with self._lock:
|
|
replaced = self._current.get(identity) is not current
|
|
if replaced:
|
|
host.mark_unknown()
|
|
continue
|
|
# Original immutable verifier consumes outside the writer, then
|
|
# capacity reselects the full SQL identity under App→request.
|
|
current.capacity.record_cleanup(
|
|
ref=ref, generation=delivery.generation, dispatch_id=delivery.dispatch_id
|
|
)
|
|
host.retire()
|
|
except BaseException:
|
|
host.mark_unknown()
|
|
return () # SQL ACK is authority; invocation IDs are never cleanup receipts.
|
|
|
|
def recovery_evidence(self, *, context, dispatch_id):
|
|
with self._lock:
|
|
retained = tuple(self._current.values())
|
|
for current in retained:
|
|
if (current.delivery.ref, current.delivery.generation, current.delivery.dispatch_id) == (
|
|
context.ref,
|
|
context.generation,
|
|
dispatch_id,
|
|
):
|
|
return "stopped" if current.host.lifetime._consumed else "unknown"
|
|
return "unknown"
|
|
|
|
def retire_closed(self, *, sessions):
|
|
from services.dify_builder.native_testing.host_capacity import finish_unacquired_cancellation
|
|
|
|
# Durable closure precedes disposal. A retained inactive generation-zero
|
|
# candidate additionally needs the existing complete SQL never-acquired
|
|
# proof; missing handles or inactivity alone never grant retirement.
|
|
with self._lock:
|
|
registries = tuple(self._requests.items())
|
|
for key, registry in registries:
|
|
with sessions() as session:
|
|
request = session.scalar(
|
|
select(DifyBuilderExecutionRequest).where(
|
|
DifyBuilderExecutionRequest.tenant_id == key[0],
|
|
DifyBuilderExecutionRequest.actor_id == key[1],
|
|
DifyBuilderExecutionRequest.id == key[2],
|
|
)
|
|
)
|
|
closed = request is not None and request.state in {"sealed", "cancelled", "failed", "expired"}
|
|
if not closed:
|
|
continue
|
|
with self._lock:
|
|
if self._requests.get(key) is not registry:
|
|
continue
|
|
entries = tuple((k, v) for k, v in self._current.items() if k[0] == key)
|
|
eligible = True
|
|
for _, current in entries:
|
|
if current.host.lifetime._consumed:
|
|
continue
|
|
if (
|
|
current.registry is not registry
|
|
or current.delivery.generation != 0
|
|
or current.host.lifetime._registry is not None
|
|
or current.host.lifetime.unknown
|
|
):
|
|
eligible = False
|
|
break
|
|
try:
|
|
# SQL only, outside owner/host locks. This preserves the
|
|
# complete scoped initial outbox/no-effects/context proof.
|
|
if not finish_unacquired_cancellation(current.lifecycle, ref=current.delivery.ref):
|
|
eligible = False
|
|
break
|
|
except Exception:
|
|
eligible = False
|
|
break
|
|
if not eligible:
|
|
continue
|
|
with self._lock:
|
|
if self._requests.get(key) is not registry or entries != tuple(
|
|
(k, v) for k, v in self._current.items() if k[0] == key
|
|
):
|
|
continue # Actual retained owner changed during unlocked SQL.
|
|
try:
|
|
# Atomic with registry.activate: an activated unconsumed
|
|
# lifetime refuses disposal; a disposed registry fences any
|
|
# future activation. Never mark an inactive lifetime consumed.
|
|
registry.dispose()
|
|
except ValueError:
|
|
continue
|
|
for identity, _ in entries:
|
|
del self._current[identity]
|
|
del self._requests[key]
|
|
|
|
|
|
class DifyNativeInitialLaunch:
|
|
def __init__(self, *, lifecycle, publish):
|
|
if (
|
|
type(lifecycle.resume_settlement) is not DifyNativeResumeSettlement
|
|
or type(lifecycle.cancellation_settlement) is not DifyNativeCancellationSettlement
|
|
or lifecycle.cancellation_settlement.lifecycle is not lifecycle
|
|
):
|
|
raise BuilderExecutionPolicyError("native_runtime_composition_mismatch")
|
|
self._lifecycle, self._publish = lifecycle, publish
|
|
|
|
def launch(self, *, ref):
|
|
self._lifecycle.enqueue_initial_dispatch(ref=ref)
|
|
self._lifecycle.publish_pending(publish=self._publish)
|
|
|
|
|
|
def build_native_api(*, sessions, actor, owner: DifyNativeRuntimeOwner, publish):
|
|
from services.dify_builder.native_testing.api_service import (
|
|
NativeAdmissionDependencies,
|
|
NativeTestApiService,
|
|
)
|
|
from services.dify_builder.native_testing.context_binding import (
|
|
NativeAdmissionCompiler,
|
|
)
|
|
from services.dify_builder.native_testing.http_binding import (
|
|
NativeHttpDisclosure,
|
|
NativeHttpResourceSnapshot,
|
|
)
|
|
|
|
if type(owner) is not DifyNativeRuntimeOwner:
|
|
raise BuilderExecutionPolicyError("native_runtime_composition_mismatch")
|
|
qualification = owner.code_qualification
|
|
profile = build_native_http_profile() if qualification is None else qualification.profile()
|
|
service = NativeTestApiService(
|
|
session_factory=sessions,
|
|
actor=actor,
|
|
resolver=_profile_resolver(profile.model_dump(mode="json"), qualification),
|
|
admission=None,
|
|
resolution=None,
|
|
runtime=owner,
|
|
disclosure=None,
|
|
)
|
|
service.lifecycle.resume_settlement = DifyNativeResumeSettlement(code_qualification=qualification)
|
|
service.lifecycle.cancellation_settlement = DifyNativeCancellationSettlement(lifecycle=service.lifecycle)
|
|
from services.dify_builder.native_testing.sandbox.qualification import (
|
|
NativeCodeDisclosure,
|
|
NativeCodeResourceSnapshot,
|
|
)
|
|
|
|
resources = (
|
|
NativeHttpResourceSnapshot(session_factory=sessions)
|
|
if qualification is None
|
|
else NativeCodeResourceSnapshot(session_factory=sessions, qualification=qualification)
|
|
)
|
|
service.admission = NativeAdmissionDependencies(
|
|
compiler=NativeAdmissionCompiler(session_factory=sessions, resources=resources),
|
|
profile=profile,
|
|
launch=DifyNativeInitialLaunch(lifecycle=service.lifecycle, publish=publish),
|
|
)
|
|
service.disclosure = (
|
|
NativeHttpDisclosure(loader=service.loader, state_store=service.store)
|
|
if qualification is None
|
|
else NativeCodeDisclosure(
|
|
loader=service.loader,
|
|
state_store=service.store,
|
|
qualification=qualification,
|
|
)
|
|
)
|
|
return service
|
|
|
|
|
|
def validate_projection_host(*, host, guard, entity=None, queue=None, task_id=None, user_id=None):
|
|
from services.dify_builder.native_testing.host_execution import NativeHostLaunch
|
|
|
|
if type(host) is not NativeHostLaunch or host.native_observation is not guard:
|
|
raise BuilderExecutionPolicyError("native_resume_owner_mismatch")
|
|
host._require_live()
|
|
guard.require_healthy()
|
|
context = host.lifetime.context
|
|
if user_id is not None and user_id != context.actor_id:
|
|
raise BuilderExecutionPolicyError("native_resume_identity_mismatch")
|
|
if entity is not None or queue is not None:
|
|
generation = host.generation_bindings
|
|
if (
|
|
entity is not generation.entity
|
|
or queue is not generation.queue
|
|
or queue.native_host is not host
|
|
or queue.native_observation is not guard
|
|
):
|
|
raise BuilderExecutionPolicyError("native_resume_identity_mismatch")
|
|
if context.generation > 0:
|
|
restored = host.resume_bindings
|
|
host.require_resume_projection_bindings(
|
|
entity=restored.entity, runtime_state=restored.runtime_state, response_filter=restored.response_filter
|
|
)
|
|
if task_id is not None and task_id != restored.entity.task_id:
|
|
raise BuilderExecutionPolicyError("native_resume_identity_mismatch")
|
|
|
|
|
|
def require_resume_projection(*, host, guard, queue, entity=None, workflow_run_id=None):
|
|
with guard.persistence_scope():
|
|
validate_projection_host(host=host, guard=guard)
|
|
host.require_resume_projection(guard=guard, queue=queue, entity=entity, workflow_run_id=workflow_run_id)
|
|
|
|
|
|
def load_resume_history(*, host, workflow_repository, node_repository):
|
|
from core.repositories.sqlalchemy_workflow_execution_repository import SQLAlchemyWorkflowExecutionRepository
|
|
from core.repositories.sqlalchemy_workflow_node_execution_repository import (
|
|
SQLAlchemyWorkflowNodeExecutionRepository,
|
|
)
|
|
from graphon.enums import WorkflowExecutionStatus
|
|
|
|
validate_projection_host(host=host, guard=host.native_observation)
|
|
restored = host.resume_bindings
|
|
host.require_resume_bindings(
|
|
entity=restored.entity, runtime_state=restored.runtime_state, response_filter=restored.response_filter
|
|
)
|
|
if (
|
|
type(workflow_repository) is not SQLAlchemyWorkflowExecutionRepository
|
|
or type(node_repository) is not SQLAlchemyWorkflowNodeExecutionRepository
|
|
):
|
|
raise BuilderExecutionPolicyError("native_resume_history_unavailable")
|
|
context, guard = host.lifetime.context, host.native_observation
|
|
bind_native_repository_observation(repository=workflow_repository, context=context, guard=guard)
|
|
bind_native_repository_observation(repository=node_repository, context=context, guard=guard)
|
|
execution = workflow_repository.get_native_observation_run(workflow_run_id=context.native_run_id)
|
|
if execution.id_ != context.native_run_id or execution.status is not WorkflowExecutionStatus.PAUSED:
|
|
raise BuilderExecutionPolicyError("native_resume_history_unavailable")
|
|
nodes = node_repository.get_by_workflow_execution(context.native_run_id)
|
|
if len({node.id for node in nodes}) != len(nodes):
|
|
raise BuilderExecutionPolicyError("native_resume_history_unavailable")
|
|
host.require_resume_bindings(
|
|
entity=restored.entity, runtime_state=restored.runtime_state, response_filter=restored.response_filter
|
|
)
|
|
return workflow_repository, node_repository, execution, nodes
|
|
|
|
|
|
def read_claimed_history(*, host, entity, workflow_repository, node_repository):
|
|
"""Read bound owned content before persistence installs or mutates history."""
|
|
from graphon.enums import WorkflowExecutionStatus
|
|
from services.dify_builder.native_testing.host_execution import NativeHostLaunch
|
|
|
|
if type(host) is not NativeHostLaunch:
|
|
raise BuilderExecutionPolicyError("native_resume_owner_mismatch")
|
|
restored = host.resume_bindings
|
|
generation = host.generation_bindings
|
|
host.require_resume_bindings(
|
|
entity=entity, runtime_state=restored.runtime_state, response_filter=restored.response_filter
|
|
)
|
|
if (
|
|
generation.entity is not entity
|
|
or generation.workflow_repository is not workflow_repository
|
|
or generation.node_repository is not node_repository
|
|
or host.engine_bindings.runtime_state is not restored.runtime_state
|
|
):
|
|
raise BuilderExecutionPolicyError("native_resume_identity_mismatch")
|
|
history = host._resume_history
|
|
if history is None or history[0] is not workflow_repository or history[1] is not node_repository:
|
|
raise BuilderExecutionPolicyError("native_resume_history_unavailable")
|
|
context = host.lifetime.context
|
|
guard = host.native_observation
|
|
# Repository binding is an exact retained association, never a redacted
|
|
# row's self-asserted provenance. These real reads validate all offloads.
|
|
bind_native_repository_observation(repository=workflow_repository, context=context, guard=guard)
|
|
bind_native_repository_observation(repository=node_repository, context=context, guard=guard)
|
|
execution = workflow_repository.get_native_observation_run(workflow_run_id=context.native_run_id)
|
|
if execution.id_ != context.native_run_id or execution.status is not WorkflowExecutionStatus.PAUSED:
|
|
raise BuilderExecutionPolicyError("native_resume_history_unavailable")
|
|
nodes = node_repository.get_by_workflow_execution(context.native_run_id)
|
|
if len({node.id for node in nodes}) != len(nodes):
|
|
raise BuilderExecutionPolicyError("native_resume_history_unavailable")
|
|
host.require_resume_bindings(
|
|
entity=entity, runtime_state=restored.runtime_state, response_filter=restored.response_filter
|
|
)
|
|
return execution, nodes
|
|
|
|
|
|
def resume_native_generator(*, app_model, workflow, user, runtime, native_host, sessions):
|
|
from core.repositories.sqlalchemy_workflow_execution_repository import SQLAlchemyWorkflowExecutionRepository
|
|
from core.repositories.sqlalchemy_workflow_node_execution_repository import (
|
|
SQLAlchemyWorkflowNodeExecutionRepository,
|
|
)
|
|
from models.enums import WorkflowRunTriggeredFrom
|
|
from models.workflow import WorkflowNodeExecutionTriggeredFrom
|
|
from services.dify_builder.native_testing.generation_wiring import (
|
|
HostOwnedNativeGeneratorStream,
|
|
preflight_native_observation,
|
|
validate_native_host,
|
|
)
|
|
from services.workflow_run_agg import WorkflowRunAgg
|
|
from services.workflow_runtime.workflow_generator import WorkflowAppGenerator
|
|
|
|
context = native_host.lifetime.context
|
|
validate_native_host(required=True, ref=context.ref, runtime=runtime, host=native_host)
|
|
bindings = native_host.resume_bindings
|
|
native_host.require_resume_bindings(
|
|
entity=bindings.entity, runtime_state=bindings.runtime_state, response_filter=bindings.response_filter
|
|
)
|
|
preflight_native_observation(host=native_host, entity=bindings.entity, workflow=workflow)
|
|
workflow_repo = SQLAlchemyWorkflowExecutionRepository(
|
|
sessions,
|
|
context.tenant_id,
|
|
user,
|
|
context.app_id,
|
|
WorkflowRunTriggeredFrom.DEBUGGING,
|
|
)
|
|
node_repo = SQLAlchemyWorkflowNodeExecutionRepository(
|
|
sessions,
|
|
context.tenant_id,
|
|
user,
|
|
context.app_id,
|
|
WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN,
|
|
)
|
|
with native_host.observation_scope():
|
|
native_host.bind_resume_history(workflow_repository=workflow_repo, node_repository=node_repo)
|
|
source = WorkflowAppGenerator(execution_driver=WorkflowRunAgg.run).resume(
|
|
app_model=app_model,
|
|
workflow=workflow,
|
|
user=user,
|
|
application_generate_entity=bindings.entity,
|
|
graph_runtime_state=bindings.runtime_state,
|
|
response_stream_filter=bindings.response_filter,
|
|
workflow_execution_repository=workflow_repo,
|
|
workflow_node_execution_repository=node_repo,
|
|
native_runtime=runtime,
|
|
native_host=native_host,
|
|
)
|
|
from collections.abc import Generator
|
|
|
|
if not isinstance(source, Generator):
|
|
raise BuilderExecutionPolicyError("native_stream_unavailable")
|
|
return HostOwnedNativeGeneratorStream(source=source, host=native_host)
|
|
|
|
|
|
class DifyNativeCancellationFence:
|
|
def __init__(self, *, lifecycle, delivery):
|
|
self.lifecycle, self.delivery = lifecycle, delivery
|
|
|
|
def poll(self):
|
|
from services.dify_builder.native_testing.state_store import invocation_state
|
|
|
|
with self.lifecycle.loader.session_factory() as session:
|
|
owned, scope = self.lifecycle.loader._load(session, ref=self.delivery.ref, lock=False, settlement=True)
|
|
state = invocation_state(session, self.delivery.ref, owned.tenant_id)
|
|
dispatch = session.scalar(
|
|
owner_query(NativeDispatch, self.delivery.ref, owned.tenant_id).where(
|
|
NativeDispatch.id == self.delivery.dispatch_id,
|
|
NativeDispatch.generation == self.delivery.generation,
|
|
)
|
|
)
|
|
if dispatch is None or state.generation != self.delivery.generation:
|
|
raise BuilderExecutionPolicyError("native_dispatch_conflict")
|
|
return scope.cancelled or state.cancellation_key is not None
|