"""Code-only trusted qualification and current native resource binding. No production qualification issuer is installed. A required server owner must validate actual immutable empty-snapshot and deployment facts; DTOs, SQL flags, environment probes and copied digests never establish those facts. Its validate method is a local currentness check, including when called under C02's writer; provisioning, deployment measurement and RPC belong outside that transaction. """ from __future__ import annotations import hashlib import json from dataclasses import asdict, dataclass from importlib.metadata import version from inspect import getsource from types import MappingProxyType from typing import Annotated, NoReturn, Protocol, cast, runtime_checkable from urllib.parse import urlsplit from uuid import NAMESPACE_URL, uuid5 from pydantic import Field, JsonValue, SecretStr, TypeAdapter from shellctl.shared.schemas import NativeScopeLimits, NativeScopeSpec from sqlalchemy import select from configs import dify_config from core.dify_builder.models import Actor from core.helper.code_executor.code_executor import CodeExecutor from graphon.nodes.code.code_node import CodeNode from graphon.nodes.code.entities import CodeLanguage, CodeNodeData from graphon.nodes.code.limits import CodeNodeLimits from graphon.nodes.template_transform.entities import TemplateTransformNodeData from graphon.nodes.template_transform.template_transform_node import ( TemplateTransformNode, ) from models.dify_builder import DifyBuilderExecutionRequest from models.dify_builder_native_tests import ( NativeEffectOperation, NativeInvocationState, NativePendingAction, NativeTestInvocation, ) from runtime_contracts.builder.execution import ( BuilderExecutionPolicyError, Digest, canonical_digest, canonical_json, ) from runtime_contracts.builder.native.approval_contracts import ( NativeApprovalProposal, NativeApprovalTerms, ) from runtime_contracts.builder.native.contracts import ( ID, EffectIntent, FiniteJSON, FrozenDTO, operation_digest, ) from services.code_execution.native_scope_transport import ( NativeCodeBounds, NativeCodeProfile, ) from services.dify_builder.execution_policy_service import check_execution_permission from services.dify_builder.native_testing.context_binding import NativeRuntimeProfile from services.dify_builder.native_testing.control import marked_native_node_class from services.dify_builder.native_testing.http_binding import _redacted_shape from services.dify_builder.native_testing.private_reads import ( validate_invocation_workflow, validate_private_read, ) from services.dify_builder.native_testing.public_contracts import ( PendingActionDisclosure, ) from services.dify_builder.native_testing.repository import ( NativeBinding, native_workflow_revision, ) from services.dify_builder.native_testing.runtime_bridge import NativeContextUnavailable from services.dify_builder.native_testing.sandbox.contracts import ( CodeScopeSpec, SnapshotID, ) from services.dify_builder.native_testing.transitions import require_ancestor_authority PROFILE_ID = "builder-native-code-no-network-v1" BOUNDS = MappingProxyType( { "cpu_quota": 100000, "memory_bytes": 256 << 20, "pids": 32, "fds": 128, "disk_bytes": 64 << 20, "wall_seconds": 30, "input_bytes": 1 << 20, "output_bytes": 1 << 20, "max_pages": 256, "create_seconds": 5, "execution_seconds": 10, "close_seconds": 5, "destroy_seconds": 5, "join_seconds": 1, "result_bytes": 8 << 20, } ) def refuse() -> NoReturn: raise BuilderExecutionPolicyError("native_code_qualification_unavailable") from None class NativeCodeCapability(FrozenDTO): """Complete server snapshot, not an attestation or ready flag.""" bounds: tuple[tuple[str, int], ...] deployment_ref: ID image_digest: Digest environment_digest: Digest python_digest: Digest node_digest: Digest empty_snapshot_ref: SnapshotID empty_snapshot_digest: Digest endpoint: Annotated[str, Field(strict=True, min_length=1, max_length=2048)] auth_token: SecretStr python_executable: Annotated[str, Field(strict=True, pattern=r"^/[A-Za-z0-9_./-]+$")] node_executable: Annotated[str, Field(strict=True, pattern=r"^/[A-Za-z0-9_./-]+$")] @runtime_checkable class NativeCodeQualificationOwner(Protocol): def validate(self, *, capability: NativeCodeCapability) -> None: """Revalidate actual complete qualification; local only, no IO/RPC. The real trusted issuer/provisioner is an unresolved delivery gate. Implementations must verify the exact fixed BOUNDS as deployment facts, authenticated endpoint, content-bound secret-free empty snapshot and image/interpreter/environment identities. Return None or refuse. """ ... class NativeCodeQualification: def __init__(self, *, capability: NativeCodeCapability, owner: NativeCodeQualificationOwner): try: if ( type(capability) is not NativeCodeCapability or not isinstance(owner, NativeCodeQualificationOwner) or not callable(owner.validate) ): refuse() cap = NativeCodeCapability.model_validate(capability.model_dump(mode="python"), strict=True) if cap.bounds != tuple(sorted(BOUNDS.items())): refuse() endpoint = urlsplit(cap.endpoint) if ( endpoint.scheme not in ("http", "https") or not endpoint.hostname or endpoint.path not in ("", "/") or endpoint.username or endpoint.password or endpoint.query or endpoint.fragment or not cap.auth_token.get_secret_value() ): refuse() if any( part in (".", "..", "") for executable in (cap.python_executable, cap.node_executable) for part in executable.split("/")[1:] ): refuse() self.capability, self.owner = cap, owner self._identity = self._fingerprint() self.check() except Exception: refuse() def _fingerprint(self): cap = self.capability material = cap.model_dump(mode="json") material["auth_token"] = cap.auth_token.get_secret_value() return canonical_digest({"capability": material, "bounds": dict(BOUNDS)}) def check(self) -> None: try: if self._fingerprint() != self._identity: refuse() if self.owner.validate(capability=self.capability) is not None or self._fingerprint() != self._identity: refuse() except Exception: refuse() def descriptor(self) -> dict: self.check() cap = self.capability return { "deployment_ref": cap.deployment_ref, "image_digest": cap.image_digest, "environment_digest": cap.environment_digest, "python_digest": cap.python_digest, "node_digest": cap.node_digest, "empty_snapshot_ref": cap.empty_snapshot_ref, "empty_snapshot_digest": cap.empty_snapshot_digest, "endpoint_digest": canonical_digest(cap.endpoint), "python_executable": cap.python_executable, "node_executable": cap.node_executable, "bounds": dict(BOUNDS), } def profile(self) -> NativeRuntimeProfile: return NativeRuntimeProfile( profile_id=PROFILE_ID, implementation_revision=canonical_digest(self.descriptor()), limits={"calls": 16, "dispatches": 16, "cost_units": 16}, ) def validate_profile(self, profile: NativeRuntimeProfile) -> None: if type(profile) is not NativeRuntimeProfile or profile != self.profile(): refuse() # Current graph/config metadata uses the same Dify owners as the HTTP lane. _JSON = TypeAdapter(FiniteJSON) def code_limits(): return CodeNodeLimits( max_string_length=min(dify_config.CODE_MAX_STRING_LENGTH, BOUNDS["output_bytes"]), max_number=dify_config.CODE_MAX_NUMBER, min_number=dify_config.CODE_MIN_NUMBER, max_precision=dify_config.CODE_MAX_PRECISION, max_depth=dify_config.CODE_MAX_DEPTH, max_number_array_length=dify_config.CODE_MAX_NUMBER_ARRAY_LENGTH, max_string_array_length=dify_config.CODE_MAX_STRING_ARRAY_LENGTH, max_object_array_length=dify_config.CODE_MAX_OBJECT_ARRAY_LENGTH, ) def template_limit(): return min(dify_config.TEMPLATE_TRANSFORM_MAX_LENGTH, BOUNDS["output_bytes"]) def supported(raw): try: if type(raw) is not dict or raw.get("version", "1") != "1" or version("graphon") != "0.8.0": refuse() kind = raw.get("type") if kind == "code": node = CodeNodeData.model_validate(raw) cls = CodeNode if node.dependencies: refuse() elif kind == "template-transform": node, cls = ( TemplateTransformNodeData.model_validate(raw), TemplateTransformNode, ) else: refuse() if cls.get_node_type_classes_mapping().get(kind, {}).get("1") is not cls: refuse() if len({x.variable for x in node.variables}) != len(node.variables) or any( len(x.value_selector) < 2 or x.value_selector[0] in {"env", "sys", "conversation"} for x in node.variables ): refuse() return node, cls except Exception: refuse() def describe_code_resource(*, qualification, node_id, raw, workflow_revision, app_id, workflow_id): if type(qualification) is not NativeCodeQualification: refuse() node, cls = supported(raw) return { "kind": "code", "node_id": node_id, "app_id": app_id, "workflow_id": workflow_id, "workflow_revision": workflow_revision, "node_config_digest": canonical_digest(raw), "native_type": node.type, "native_version": cls.version(), "native_class": cls.__module__ + "." + cls.__name__, "graphon_version": version("graphon"), "implementation_revision": qualification.profile().implementation_revision, "qualification": qualification.descriptor(), "code_limits": asdict(code_limits()), "template_limit": template_limit(), "transformer_digest": canonical_digest( { name.value: { "runner": transformer.get_runner_script(), "preload": transformer.get_preload_script(), "class": transformer.__module__ + "." + transformer.__name__, "sources": [ hashlib.sha256(getsource(base).encode()).hexdigest() for base in (CodeExecutor, *transformer.__mro__[:-1]) ], } for name, transformer in CodeExecutor.code_template_transformers.items() } ), "policy": { "isolated": True, "egress": False, "new_data": False, "destructive": False, "approval_scope": "operation", "cost_units": 1, "additional_budget": {}, }, } def _node(graph, node_id): nodes = [n["data"] for n in graph.get("nodes", []) if n.get("id") == node_id] if len(nodes) != 1: refuse() return nodes[0] def _entries(*, qualification, graph, revision, app_id, workflow_id): # Other effect families are unavailable in this profile. Actual scalar # schemas/classes continue to be qualified by the existing native host. if any(n.get("data", {}).get("type") in {"http-request", "agent"} for n in graph.get("nodes", [])): refuse() entries = tuple( describe_code_resource( qualification=qualification, node_id=n["id"], raw=n["data"], workflow_revision=revision, app_id=app_id, workflow_id=workflow_id, ) for n in graph.get("nodes", []) if n.get("data", {}).get("type") in {"code", "template-transform"} ) if len({x["node_id"] for x in entries}) != len(entries): refuse() return entries class NativeCodeResourceSnapshot: def __init__(self, *, session_factory, qualification): self._sessions, self.qualification = session_factory, qualification def __call__( self, *, actor: Actor, app_id: str, workflow_id: str, graph: dict[str, JsonValue], profile: NativeRuntimeProfile, ) -> tuple[dict[str, JsonValue], ...]: self.qualification.validate_profile(profile) check_execution_permission(actor, app_id, self._sessions) with self._sessions() as session: workflow = validate_invocation_workflow( session, tenant_id=actor.tenant_id, app_id=app_id, workflow_id=workflow_id, ) if json.loads(workflow.graph) != graph or workflow._environment_variables not in (None, "", "[]", "{}"): refuse() return cast( tuple[dict[str, JsonValue], ...], _entries( qualification=self.qualification, graph=graph, revision=native_workflow_revision(workflow), app_id=app_id, workflow_id=workflow_id, ), ) def current_metadata(*, session, owned, node_id, qualification): qualification.check() # required trusted local-only currentness; no RPC under writer state = session.scalar( select(NativeInvocationState).where( NativeInvocationState.tenant_id == owned.tenant_id, NativeInvocationState.request_id == owned.ref.request_id, NativeInvocationState.invocation_id == owned.ref.invocation_id, ) ) if state is None or not state.app_id or not state.workflow_id: refuse() require_ancestor_authority(session, owned.ref, owned.tenant_id) workflow = validate_invocation_workflow( session, tenant_id=owned.tenant_id, app_id=state.app_id, workflow_id=state.workflow_id, ) request = session.get(DifyBuilderExecutionRequest, owned.ref.request_id) if request is None or workflow._environment_variables not in (None, "", "[]", "{}"): refuse() qualification.validate_profile(NativeRuntimeProfile.model_validate(request.context.get("profile"))) graph, revision = json.loads(workflow.graph), native_workflow_revision(workflow) entries = _entries( qualification=qualification, graph=graph, revision=revision, app_id=workflow.app_id, workflow_id=workflow.id, ) digest = canonical_digest(list(entries)) if digest != request.context.get("resource_manifest_digest"): refuse() raw = _node(graph, node_id) descriptor = describe_code_resource( qualification=qualification, node_id=node_id, raw=raw, workflow_revision=revision, app_id=workflow.app_id, workflow_id=workflow.id, ) if descriptor not in entries: refuse() return raw, descriptor, digest def classified_binding(*, intent_digest, execution_id, descriptor, manifest_digest): return NativeBinding( intent_digest=intent_digest, kind="code", resource_ref=descriptor["node_id"], implementation_revision=descriptor["implementation_revision"], account_ref=None, capability_digest=manifest_digest, resource_version=descriptor["workflow_revision"], credential_version=None, isolated=True, egress=False, new_data=False, destructive=False, approval_scope="operation", cost_units=1, budget={}, execution_id=execution_id, invocation_digest=intent_digest, ) class NativeCodeBindingResolver: def __init__(self, *, qualification): self.qualification = qualification def resolve_binding(self, *, session, owned, key): if key.invocation_id != owned.ref.invocation_id: refuse() frozen = NativeBinding.model_validate(owned.bindings.get(operation_digest(key))) if frozen.kind != "code" or key.occurrence_path != (frozen.execution_id,): refuse() _, descriptor, digest = current_metadata( session=session, owned=owned, node_id=frozen.resource_ref, qualification=self.qualification, ) return classified_binding( intent_digest=frozen.intent_digest, execution_id=frozen.execution_id, descriptor=descriptor, manifest_digest=digest, ) def resolve_pending(self, **kwargs): refuse() @dataclass(frozen=True) class PreparedCodeCall: intent: EffectIntent spec: CodeScopeSpec profile: NativeCodeProfile bounds: NativeCodeBounds language: CodeLanguage code: str inputs: dict runner: str preload: str class NativeCodeBindingProducer: def __init__(self, *, association, loader, state_store, journal, qualification): self.association, self.loader, self.store, self.journal = ( association, loader, state_store, journal, ) self.qualification = qualification self._context = self._cursor = None def _snapshot(self, context): raw = _node(context.graph, self.association.node_id) expected, cls = supported(raw) node = self.association.bound_node if type(node) is not marked_native_node_class(cls, marked=True) or canonical_digest( node.node_data.model_dump(mode="json") ) != canonical_digest(expected.model_dump(mode="json")): refuse() descriptor = describe_code_resource( qualification=self.qualification, node_id=self.association.node_id, raw=raw, workflow_revision=context.graph_revision, app_id=context.app_id, workflow_id=context.workflow_id, ) if context.resource_manifest.count(descriptor) != 1: refuse() return expected, descriptor def _current_inputs(self, context, node): expected = {} graph_ids = {n["id"] for n in context.graph["nodes"]} for variable in node.variables: if variable.value_selector[0] not in graph_ids: refuse() value = self.association.runtime_state.variable_pool.get(variable.value_selector) if value is not None and str(value.value_type) in { "secret", "file", "array[file]", }: refuse() expected[variable.variable] = value.to_object() if value else None return _JSON.validate_python(expected, strict=True) def validate_prepared(self, prepared): try: context = self.association.resolve() node, descriptor = self._snapshot(context) material = prepared.intent.parameters if ( canonical_json(self._current_inputs(context, node)) != canonical_json(prepared.inputs) or canonical_digest(list(context.resource_manifest)) != prepared.intent.capability_digest or any( descriptor[key] != material[key] for key in ( "node_config_digest", "qualification", "transformer_digest", "code_limits", "template_limit", ) ) ): refuse() return context except (BuilderExecutionPolicyError, NativeContextUnavailable): raise except Exception: refuse() def prepare(self, *, language, code, inputs): try: return self._prepare(language=language, code=code, inputs=inputs) except (BuilderExecutionPolicyError, NativeContextUnavailable): raise except Exception: refuse() def _prepare(self, *, language, code, inputs): context = self.association.resolve() # actual attachment/public UUID first self.qualification.check() node, descriptor = self._snapshot(context) expected_language = node.code_language if isinstance(node, CodeNodeData) else CodeLanguage.JINJA2 expected_code = node.code if isinstance(node, CodeNodeData) else node.template if type(language) is not CodeLanguage or language != expected_language or code != expected_code: refuse() expected = self._current_inputs(context, node) checked = _JSON.validate_python(inputs, strict=True) if type(checked) is not dict or canonical_json(checked) != canonical_json( _JSON.validate_python(expected, strict=True) ): refuse() owned = self.loader.load(ref=context.ref) authority = validate_private_read(self.loader, context.ref) if authority.generation != context.generation: refuse() transformer = CodeExecutor.code_template_transformers[language] runner, preload = transformer.transform_caller(code, checked) payload = ( json.dumps( {"preload": preload, "code": runner}, ensure_ascii=True, separators=(",", ":"), ) + "\n" ).encode() if len(payload) > BOUNDS["input_bytes"]: refuse() material = { "language": language.value, "code": code, "inputs": checked, "node_config_digest": descriptor["node_config_digest"], "qualification": descriptor["qualification"], "transformer_digest": descriptor["transformer_digest"], "runner_digest": hashlib.sha256(runner.encode()).hexdigest(), "preload_digest": hashlib.sha256(preload.encode()).hexdigest(), "code_limits": descriptor["code_limits"], "template_limit": descriptor["template_limit"], } input_ref = self.store.store_node_inputs( ref=context.ref, generation=context.generation, native_execution_id=self.association.execution_id, inputs=material, ) if canonical_json(self.store.load_result(ref=context.ref, result_ref=input_ref)) != canonical_json(material): refuse() cursor = self.journal.open(ref=context.ref, occurrence_path=self.association.occurrence_path) cursor.bind_input(input_result_ref=input_ref, inputs=material) key = cursor.next_key() cap = self.qualification.capability running = CodeExecutor.code_language_to_running_language[language] profile = NativeCodeProfile( language=running, executable=cap.node_executable if running == "nodejs" else cap.python_executable, environment_digest=cap.environment_digest, ) # A deterministic scope identity belongs to this exact logical effect, # never to a new retry. Durable reservation still permits exactly one POST. scope_id = ( "code_" + uuid5( NAMESPACE_URL, canonical_digest( { "ref": context.ref.model_dump(mode="json"), "key": key.model_dump(mode="json"), } ), ).hex ) limits = NativeScopeLimits( **{ k: BOUNDS[k] for k in ( "cpu_quota", "memory_bytes", "pids", "fds", "disk_bytes", "wall_seconds", "input_bytes", "output_bytes", ) } ) spec = CodeScopeSpec( native=NativeScopeSpec( scope_id=scope_id, binding_ref=scope_id, generation=1, base_snapshot_ref=cap.empty_snapshot_ref, input_file_refs=(), network_targets=(), capability_digest=canonical_digest(list(context.resource_manifest)), deadline=owned.expires_at, limits=limits, ), environment_digest=cap.environment_digest, base_snapshot_digest=cap.empty_snapshot_digest, ) profile_digest = hashlib.sha256(json.dumps(asdict(profile), sort_keys=True).encode()).hexdigest() intent = EffectIntent( key=key, kind="code", resource_ref=self.association.node_id, implementation_revision=descriptor["implementation_revision"], account_ref=None, target={ "code_physical": { "scope_spec_digest": canonical_digest(spec.native.model_dump(mode="json")), "environment_digest": cap.environment_digest, "base_snapshot_ref": cap.empty_snapshot_ref, "base_snapshot_digest": cap.empty_snapshot_digest, "payload_digest": hashlib.sha256(payload).hexdigest(), "profile_digest": profile_digest, } }, parameters=material, input_digest=canonical_digest(material), capability_digest=canonical_digest(list(context.resource_manifest)), ) self.loader.load(ref=context.ref) if validate_private_read(self.loader, context.ref) != authority or self.association.resolve() != context: refuse() self._install(context=context, authority=authority, descriptor=descriptor, intent=intent) self._context, self._cursor = context, cursor return PreparedCodeCall( intent, spec, profile, NativeCodeBounds( timeout_seconds=10, output_bytes=BOUNDS["output_bytes"], max_pages=256, input_bytes=BOUNDS["input_bytes"], close_seconds=5, ), language, code, checked, runner, preload, ) def _install(self, *, context, authority, descriptor, intent): from services.dify_builder.native_testing.state_store import ( NativeExecutionStateStore, ) with self.loader.locked(ref=context.ref) as (session, owned, scope): NativeExecutionStateStore._validate_node_inputs_owner( session, owned, scope, context, authority, context.generation ) _, current, digest = current_metadata( session=session, owned=owned, node_id=self.association.node_id, qualification=self.qualification, ) if ( current != descriptor or digest != intent.capability_digest or self.association.execution_id != intent.key.occurrence_path[0] ): refuse() binding = classified_binding( intent_digest=canonical_digest(intent.model_dump(mode="json")), execution_id=self.association.execution_id, descriptor=current, manifest_digest=digest, ) invocation = session.scalar( select(NativeTestInvocation).where( NativeTestInvocation.tenant_id == owned.tenant_id, NativeTestInvocation.request_id == context.ref.request_id, NativeTestInvocation.invocation_id == context.ref.invocation_id, NativeTestInvocation.active.is_(True), ) ) if invocation is None: refuse() identity = operation_digest(intent.key) original = invocation.bindings.get(identity) if original is not None and NativeBinding.model_validate(original) != binding: refuse() if original is None: invocation.bindings = { **invocation.bindings, identity: binding.model_dump(mode="json"), } def approval_proposals(self): if self._context is None or self._cursor is None or self.association.resolve() != self._context: refuse() owned = self.loader.load(ref=self._context.ref) return tuple( NativeApprovalProposal( key=intent.key, terms=NativeApprovalTerms( max_calls=1, max_dispatches=1, max_cost_units=1, expires_at=owned.expires_at.isoformat(), ), ) for intent in self._cursor.snapshot().intents ) def finish(self): if self._cursor is None: refuse() self._cursor.finish() def code_disclosure_summary(*, intent, descriptor): if type(intent.parameters) is not dict or type(intent.target) is not dict: refuse() return ( {"language": intent.parameters["language"], "kind": "isolated_code"}, { "input_digest": intent.input_digest, "typed_shape": _redacted_shape(intent.parameters.get("inputs")), "source_workflow_revision": descriptor["workflow_revision"], "network_scope": "none", "redacted_fields": ["code", "template", "inputs", "outputs"], "max_result_bytes": BOUNDS["result_bytes"], }, ) class NativeCodeDisclosure: """Actual protected pending intent, current classification and bounded public notice.""" def __init__(self, *, loader, state_store, qualification): self.qualification = qualification from services.dify_builder.native_testing.pending_intents import ( ProtectedPendingIntentReader, ) self.loader = loader self.reader = ProtectedPendingIntentReader(loader=loader, state_store=state_store) def describe(self, *, actor: Actor, resolved) -> PendingActionDisclosure: if actor != self.loader.actor or resolved.intent.kind != "code": raise BuilderExecutionPolicyError("native_owner_mismatch") proposed = self.reader.load_terms(resolved=resolved, for_projection=True) # Actual protected proposal. owned = self.loader.load(ref=resolved.ref) terms = NativeApprovalTerms( max_calls=1, max_dispatches=1, max_cost_units=1, expires_at=owned.expires_at.isoformat(), ) if proposed != terms: raise BuilderExecutionPolicyError("native_approval_proposal_unavailable") with self.loader.session_factory() as session: binding = NativeCodeBindingResolver(qualification=self.qualification).resolve_binding( session=session, owned=owned, key=resolved.intent.key ) if ( binding.intent_digest != canonical_digest(resolved.intent.model_dump(mode="json")) or binding.destructive or binding != NativeBinding.model_validate(owned.bindings.get(operation_digest(resolved.intent.key))) ): raise BuilderExecutionPolicyError("native_binding_drift") _, descriptor, _ = current_metadata( session=session, owned=owned, node_id=binding.resource_ref, qualification=self.qualification, ) pending = session.scalar( select(NativePendingAction) .join( NativeEffectOperation, NativeEffectOperation.id == NativePendingAction.operation_id, ) .where( NativeEffectOperation.tenant_id == owned.tenant_id, NativeEffectOperation.request_id == resolved.ref.request_id, NativeEffectOperation.invocation_id == resolved.ref.invocation_id, NativeEffectOperation.operation_digest == operation_digest(resolved.intent.key), NativeEffectOperation.intent_digest == binding.intent_digest, NativePendingAction.tenant_id == owned.tenant_id, NativePendingAction.request_id == resolved.ref.request_id, NativePendingAction.invocation_id == resolved.ref.invocation_id, NativePendingAction.category == "approval", NativePendingAction.owner_version == self._scope(resolved.intent, binding), ) ) if pending is None or pending.generation != resolved.generation: raise BuilderExecutionPolicyError("native_pending_conflict") pending_id, scope_digest = pending.id, pending.owner_version target, summary = code_disclosure_summary(intent=resolved.intent, descriptor=descriptor) return PendingActionDisclosure( pending_operation_id=pending_id, wait_reason="approval", version=resolved.generation + 1, scope_digest=scope_digest, account_label=None, action="Run one isolated Code or Template operation", target=target, parameters_summary=summary, risk_notice=( "One isolated operation with no network or files; submitted source, inputs and results are redacted." ), required_configuration=(), native_form_ref=None, approval_limits=terms, ) @staticmethod def _scope(intent, binding): # Reuse C02's canonical scope recipe; prose never defines grant authority. from services.dify_builder.native_testing.authorization import _scope_digest return _scope_digest(intent, binding)