mirror of
https://github.com/langgenius/dify.git
synced 2026-10-16 15:35:32 +08:00
Keep native admission disabled until authenticated lineage, workspace and remaining physical effect owners are composed.
540 lines
23 KiB
Python
540 lines
23 KiB
Python
"""Agent wire adapter to the existing authorization and credential owners.
|
|
|
|
The private wire has no actor or tenant authority. Server composition is mandatory
|
|
and reconstructs the owner from the persisted request before granting access.
|
|
Validation does not consume a transport fence and never replaces one.
|
|
"""
|
|
|
|
import json
|
|
import time
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from typing import Literal, Protocol
|
|
|
|
from dify_agent.adapters.native_testing.client import AgentMaterialization, AgentPhysicalDispatchPayload
|
|
from dify_agent.protocol.native_testing import (
|
|
AgentApprovalWait,
|
|
AgentDispatchPermit,
|
|
AgentEffectDecision,
|
|
AgentEffectIntent,
|
|
AgentEffectReceipt,
|
|
AgentNativeTestRef,
|
|
AgentPolicyStop,
|
|
AgentRecordedResult,
|
|
)
|
|
from flask import current_app
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from core.dify_builder.models import Actor
|
|
from extensions.ext_database import db
|
|
from models.dify_builder import DifyBuilderExecutionRequest
|
|
from services.dify_builder.execution_contracts import BuilderExecutionPolicyError, canonical_digest, canonical_json
|
|
from services.dify_builder.native_testing.authorization import EffectAuthorizationService
|
|
from services.dify_builder.native_testing.contracts import (
|
|
ApprovalWait,
|
|
DispatchPermit,
|
|
EffectIntent,
|
|
NativeTestContextRef,
|
|
OperationKey,
|
|
Outcome,
|
|
PolicyStop,
|
|
RecordedResult,
|
|
)
|
|
from services.dify_builder.native_testing.state_store import NativeExecutionStateStore
|
|
from services.tools.builtin_tools_manage_service import BuilderCredentialRuntime, BuiltinToolManageService
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AgentCredentialBinding:
|
|
"""Trusted protected configuration resolution, never constructed from wire authority."""
|
|
|
|
actor: Actor
|
|
tool_ref: str
|
|
provider_id: str
|
|
credential_id: str | None
|
|
verified_plugin: bool
|
|
implementation_revision: str
|
|
requirements_ref: str
|
|
|
|
|
|
class NativeAgentCredentialMaterializer:
|
|
"""Separate OAuth and business phases using the existing native credential owners.
|
|
|
|
Selection is metadata only. Claim, decrypt, OAuth and CAS are sequential and
|
|
no network/crypto call runs in a SQL transaction. A business permit cannot
|
|
refresh; it is consumed at the eventual physical tool dispatcher, not here.
|
|
"""
|
|
|
|
def __init__(self, *, authorization: EffectAuthorizationService, resolve_binding: Callable):
|
|
self.authorization = authorization
|
|
self.resolve_binding = resolve_binding
|
|
|
|
def inspect(
|
|
self, *, ref: NativeTestContextRef, tool_ref: str
|
|
) -> tuple[AgentCredentialBinding, BuilderCredentialRuntime | None]:
|
|
self.authorization.loader.load(ref=ref)
|
|
binding = self.resolve_binding(ref=ref, tool_ref=tool_ref)
|
|
if (
|
|
not isinstance(binding, AgentCredentialBinding)
|
|
or binding.actor != self.authorization.loader.actor
|
|
or binding.tool_ref != tool_ref
|
|
):
|
|
raise BuilderExecutionPolicyError("native_owner_mismatch")
|
|
if binding.credential_id is None:
|
|
return binding, None
|
|
try:
|
|
selection = BuiltinToolManageService.load_builder_credential(
|
|
actor=binding.actor,
|
|
provider_id=binding.provider_id,
|
|
credential_id=binding.credential_id,
|
|
verified_plugin=binding.verified_plugin,
|
|
)
|
|
except BuilderExecutionPolicyError as error:
|
|
if error.reason_code != "needs_setup":
|
|
raise
|
|
return binding, None
|
|
return binding, selection
|
|
|
|
@staticmethod
|
|
def account_ref(selection: BuilderCredentialRuntime) -> str:
|
|
return canonical_digest(selection.snapshot.reference.model_dump(mode="json"))
|
|
|
|
@staticmethod
|
|
def refresh_due(selection: BuilderCredentialRuntime) -> bool:
|
|
reference = selection.snapshot.reference
|
|
return (
|
|
reference.credential_type == "oauth2"
|
|
and reference.expires_at != -1
|
|
and reference.expires_at - 60 < int(time.time())
|
|
)
|
|
|
|
def materialize(
|
|
self,
|
|
*,
|
|
ref: NativeTestContextRef,
|
|
intent: EffectIntent,
|
|
permit: DispatchPermit,
|
|
tool_ref: str,
|
|
phase: Literal["refresh", "business"],
|
|
) -> AgentMaterialization:
|
|
if (
|
|
intent.resource_ref != tool_ref
|
|
or intent.kind != ("oauth" if phase == "refresh" else "tool")
|
|
or phase not in {"refresh", "business"}
|
|
):
|
|
raise BuilderExecutionPolicyError("native_materialization_phase_mismatch")
|
|
self.authorization.validate_dispatch(ref=ref, intent=intent, permit=permit)
|
|
binding, selection = self.inspect(ref=ref, tool_ref=tool_ref)
|
|
if selection is None or binding.credential_id is None:
|
|
raise BuilderExecutionPolicyError("needs_setup")
|
|
if (
|
|
intent.account_ref != self.account_ref(selection)
|
|
or intent.implementation_revision != binding.implementation_revision
|
|
):
|
|
raise BuilderExecutionPolicyError("native_credential_drift")
|
|
due = self.refresh_due(selection)
|
|
if (phase == "business" and due) or (phase == "refresh" and not due):
|
|
raise BuilderExecutionPolicyError("native_materialization_phase_mismatch")
|
|
|
|
from configs import dify_config
|
|
from core.entities import PluginCredentialType
|
|
from core.helper.credential_utils import runtime_check_credential_policy_compliance
|
|
from core.helper.provider_cache import NoOpProviderCredentialCache, ToolProviderCredentialsCache
|
|
from core.plugin.entities.plugin_daemon import CredentialType
|
|
from core.plugin.impl.oauth import OAuthHandler
|
|
from core.tools.tool_manager import ToolManager
|
|
from core.tools.utils.encryption import create_provider_encrypter
|
|
from models.provider_ids import ToolProviderID
|
|
|
|
controller = ToolManager.get_builtin_provider(binding.provider_id, binding.actor.tenant_id)
|
|
if getattr(controller, "plugin_unique_identifier", None) != binding.implementation_revision:
|
|
raise BuilderExecutionPolicyError("installed_tool_unavailable")
|
|
credential_type = CredentialType.of(selection.snapshot.reference.credential_type)
|
|
# Refresh is the sole physical owner of this permit; the claim commits
|
|
# before decrypting any credential or contacting the OAuth endpoint.
|
|
if phase == "refresh":
|
|
self.authorization.claim_dispatch(ref=ref, intent=intent, permit=permit)
|
|
try:
|
|
runtime_check_credential_policy_compliance(
|
|
credential_id=binding.credential_id,
|
|
provider=binding.provider_id,
|
|
credential_type=PluginCredentialType.TOOL,
|
|
check_existence=False,
|
|
)
|
|
encrypter, _ = create_provider_encrypter(
|
|
tenant_id=binding.actor.tenant_id,
|
|
config=[
|
|
x.to_basic_provider_config() for x in controller.get_credentials_schema_by_type(credential_type)
|
|
],
|
|
cache=NoOpProviderCredentialCache(),
|
|
)
|
|
stored = json.loads(selection.snapshot.encrypted_credentials or "{}")
|
|
if not isinstance(stored, dict):
|
|
raise ValueError("credential shape")
|
|
credentials = encrypter.decrypt(stored)
|
|
if phase == "business":
|
|
return AgentMaterialization(
|
|
credential_type=credential_type.value, credentials=dict(credentials), runtime_parameters={}
|
|
)
|
|
provider = ToolProviderID(binding.provider_id)
|
|
client = BuiltinToolManageService.decrypt_builder_oauth_client(
|
|
tenant_id=binding.actor.tenant_id, provider_controller=controller, client=selection.snapshot.client
|
|
)
|
|
refreshed = OAuthHandler().refresh_credentials(
|
|
tenant_id=binding.actor.tenant_id,
|
|
user_id=selection.owner_user_id,
|
|
plugin_id=provider.plugin_id,
|
|
provider=provider.provider_name,
|
|
redirect_uri=f"{dify_config.CONSOLE_API_URL}/console/api/oauth/plugin/{binding.provider_id}/tool/callback",
|
|
system_credentials=client,
|
|
credentials=credentials,
|
|
)
|
|
reference = BuiltinToolManageService.persist_native_agent_refresh(
|
|
actor=binding.actor,
|
|
provider_id=binding.provider_id,
|
|
verified_plugin=binding.verified_plugin,
|
|
selection=selection,
|
|
encrypted_credentials=json.dumps(encrypter.encrypt(refreshed.credentials)),
|
|
expires_at=refreshed.expires_at,
|
|
)
|
|
ToolProviderCredentialsCache(
|
|
tenant_id=binding.actor.tenant_id, provider=binding.provider_id, credential_id=binding.credential_id
|
|
).delete()
|
|
# The refresh result contains only an opaque reference; tokens never
|
|
# enter the durable result owner or the returned refresh envelope.
|
|
return AgentMaterialization(
|
|
credential_type="oauth2",
|
|
credentials={},
|
|
runtime_parameters={"credential_ref": canonical_digest(reference.model_dump(mode="json"))},
|
|
)
|
|
except BuilderExecutionPolicyError:
|
|
raise
|
|
except Exception:
|
|
raise BuilderExecutionPolicyError("native_credential_phase_failed") from None
|
|
|
|
|
|
@dataclass
|
|
class NativeAgentCredentialPhases:
|
|
"""Called by the trusted continuation owner with its frozen child intent.
|
|
|
|
The caller must persist that OAuth child intent before calling refresh and
|
|
keep replaying it after restart, even if the rotated credential is now fresh.
|
|
No business permit is requested until this child has a completed receipt.
|
|
"""
|
|
|
|
authorization: EffectAuthorizationService
|
|
materializer: NativeAgentCredentialMaterializer
|
|
state_store: NativeExecutionStateStore
|
|
|
|
def prepare_business(
|
|
self,
|
|
*,
|
|
ref: NativeTestContextRef,
|
|
key: OperationKey,
|
|
tool_ref: str,
|
|
target,
|
|
parameters,
|
|
input_digest: str,
|
|
capability_digest: str,
|
|
refresh_intent: EffectIntent | None,
|
|
) -> EffectIntent | ApprovalWait | PolicyStop:
|
|
"""Readiness precedes freezing the actual account or reserving business.
|
|
|
|
The continuation owner supplies its durable OAuth child intent when one
|
|
exists, including after a crash following CAS. Absence is never inferred
|
|
from the now-fresh token. A due token with no frozen child fails closed.
|
|
"""
|
|
wait = self.configuration_wait(ref=ref, tool_ref=tool_ref)
|
|
if wait is not None:
|
|
return wait
|
|
if refresh_intent is not None:
|
|
expected_key = key.model_copy(update={"occurrence_path": (*key.occurrence_path, "oauth_refresh")})
|
|
if refresh_intent.key != expected_key or refresh_intent.resource_ref != tool_ref:
|
|
raise BuilderExecutionPolicyError("native_refresh_owner_mismatch")
|
|
wait = self.refresh(ref=ref, intent=refresh_intent)
|
|
if wait is not None:
|
|
return wait
|
|
binding, selection = self.materializer.inspect(ref=ref, tool_ref=tool_ref)
|
|
if selection is None:
|
|
raise BuilderExecutionPolicyError("native_credential_drift")
|
|
if self.materializer.refresh_due(selection):
|
|
return PolicyStop(reason_code="native_refresh_continuation_required")
|
|
return EffectIntent(
|
|
key=key,
|
|
kind="tool",
|
|
resource_ref=tool_ref,
|
|
implementation_revision=binding.implementation_revision,
|
|
account_ref=self.materializer.account_ref(selection),
|
|
target=target,
|
|
parameters=parameters,
|
|
input_digest=input_digest,
|
|
capability_digest=capability_digest,
|
|
)
|
|
|
|
def configuration_wait(self, *, ref: NativeTestContextRef, tool_ref: str) -> ApprovalWait | None:
|
|
from services.dify_builder.native_testing.pending_actions import NativePendingActionService
|
|
|
|
binding, selection = self.materializer.inspect(ref=ref, tool_ref=tool_ref)
|
|
if selection is not None:
|
|
return None
|
|
owned = self.authorization.loader.load(ref=ref)
|
|
return NativePendingActionService(self.authorization.loader).configuration(
|
|
ref=ref,
|
|
requirements_ref=binding.requirements_ref,
|
|
expected_generation=owned.generation,
|
|
)
|
|
|
|
def refresh(self, *, ref: NativeTestContextRef, intent: EffectIntent) -> ApprovalWait | PolicyStop | None:
|
|
if intent.kind != "oauth":
|
|
raise BuilderExecutionPolicyError("native_materialization_phase_mismatch")
|
|
decision = self.authorization.before_call(ref=ref, intent=intent)
|
|
if isinstance(decision, (ApprovalWait, PolicyStop)):
|
|
return decision
|
|
if isinstance(decision, RecordedResult):
|
|
result = self.state_store.load_result(ref=ref, result_ref=decision.result_ref)
|
|
if not isinstance(result, dict) or not isinstance(result.get("credential_ref"), str):
|
|
raise BuilderExecutionPolicyError("native_refresh_result_invalid")
|
|
return None
|
|
try:
|
|
materialized = self.materializer.materialize(
|
|
ref=ref, intent=intent, permit=decision, tool_ref=intent.resource_ref, phase="refresh"
|
|
)
|
|
result_ref = self.state_store.store_result(ref=ref, key=intent.key, result=materialized.runtime_parameters)
|
|
except Exception:
|
|
self.authorization.record_outcome(permit=decision, outcome="unknown", result_ref=None)
|
|
raise
|
|
self.authorization.record_outcome(permit=decision, outcome="completed", result_ref=result_ref)
|
|
return None
|
|
|
|
|
|
def api_ref(ref: AgentNativeTestRef) -> NativeTestContextRef:
|
|
return NativeTestContextRef(
|
|
policy_version=ref.policy_version,
|
|
request_id=ref.request_id,
|
|
invocation_id=ref.invocation_id,
|
|
context_digest=ref.context_digest,
|
|
)
|
|
|
|
|
|
def api_intent(intent: AgentEffectIntent) -> EffectIntent:
|
|
return EffectIntent(
|
|
key=OperationKey(
|
|
invocation_id=intent.key.invocation_id, occurrence_path=intent.key.occurrence_path, cursor=intent.key.cursor
|
|
),
|
|
kind=intent.kind,
|
|
resource_ref=intent.resource_ref,
|
|
implementation_revision=intent.implementation_revision,
|
|
account_ref=intent.account_ref,
|
|
target=intent.target,
|
|
parameters=intent.parameters,
|
|
input_digest=intent.input_digest,
|
|
capability_digest=intent.capability_digest,
|
|
)
|
|
|
|
|
|
def api_permit(permit: AgentDispatchPermit) -> DispatchPermit:
|
|
return DispatchPermit(
|
|
permit_id=permit.permit_id,
|
|
attempt_id=permit.attempt_id,
|
|
operation_id=permit.operation_id,
|
|
expires_at=permit.expires_at,
|
|
)
|
|
|
|
|
|
class AgentReadinessPort(Protocol):
|
|
"""Resolve configuration/form ownership before creating an actual-account effect.
|
|
|
|
Implementations use NativePendingActionService, preserve its durable owner
|
|
reference, and perform only inert metadata reads. They do not refresh tokens.
|
|
"""
|
|
|
|
def prepare(self, *, ref: NativeTestContextRef, intent: EffectIntent) -> ApprovalWait | None: ...
|
|
|
|
|
|
class AgentDispatchFence(Protocol):
|
|
"""NW4 durable at-most-once physical dispatch claim across workers/restart.
|
|
|
|
Must reject repeated or concurrent requests for the same permit, including
|
|
after ambiguous failure. There is no in-memory/default implementation.
|
|
"""
|
|
|
|
def claim_dispatch(
|
|
self,
|
|
*,
|
|
ref: NativeTestContextRef,
|
|
intent: EffectIntent,
|
|
permit: DispatchPermit,
|
|
) -> None: ...
|
|
|
|
|
|
class AgentCredentialMaterializer(Protocol):
|
|
"""Existing credential owner, exact phase, outside SQL. Business never refreshes.
|
|
|
|
Refresh must persist through the existing credential CAS before returning;
|
|
its successful outcome cannot authorize or spend a business permit.
|
|
"""
|
|
|
|
def materialize(
|
|
self,
|
|
*,
|
|
ref: NativeTestContextRef,
|
|
intent: EffectIntent,
|
|
permit: DispatchPermit,
|
|
tool_ref: str,
|
|
phase: Literal["refresh", "business"],
|
|
) -> AgentMaterialization: ...
|
|
|
|
|
|
@dataclass
|
|
class AgentNativeTestingBridge:
|
|
authorization: EffectAuthorizationService
|
|
readiness: AgentReadinessPort | None
|
|
materializer: AgentCredentialMaterializer | None
|
|
fence: AgentDispatchFence | None
|
|
|
|
def before_call(self, *, ref: AgentNativeTestRef, intent: AgentEffectIntent) -> AgentEffectDecision:
|
|
owned_ref, effect = api_ref(ref), api_intent(intent)
|
|
if self.readiness is not None:
|
|
self.authorization.loader.load(ref=owned_ref)
|
|
wait = self.readiness.prepare(ref=owned_ref, intent=effect)
|
|
if wait is not None:
|
|
if wait.wait_reason == "approval":
|
|
raise BuilderExecutionPolicyError("native_readiness_reason_invalid")
|
|
return AgentApprovalWait(pending_operation_id=wait.pending_operation_id, wait_reason=wait.wait_reason)
|
|
decision = self.authorization.before_call(ref=owned_ref, intent=effect)
|
|
if isinstance(decision, ApprovalWait):
|
|
return AgentApprovalWait(
|
|
pending_operation_id=decision.pending_operation_id, wait_reason=decision.wait_reason
|
|
)
|
|
if isinstance(decision, PolicyStop):
|
|
return AgentPolicyStop(reason_code=decision.reason_code)
|
|
if isinstance(decision, RecordedResult):
|
|
return AgentRecordedResult(operation_id=decision.operation_id, result_ref=decision.result_ref)
|
|
return AgentDispatchPermit(
|
|
permit_id=decision.permit_id,
|
|
attempt_id=decision.attempt_id,
|
|
operation_id=decision.operation_id,
|
|
expires_at=decision.expires_at,
|
|
)
|
|
|
|
def record_outcome(
|
|
self, *, ref: AgentNativeTestRef, permit: AgentDispatchPermit, outcome: Outcome, result_ref: str | None
|
|
) -> AgentEffectReceipt:
|
|
native_permit = api_permit(permit)
|
|
# Settlement retains the original request owner even after revocation.
|
|
if self.authorization._permit_ref(native_permit) != api_ref(ref):
|
|
raise BuilderExecutionPolicyError("invalid_dispatch_permit")
|
|
receipt = self.authorization.record_outcome(permit=native_permit, outcome=outcome, result_ref=result_ref)
|
|
return AgentEffectReceipt(
|
|
operation_id=receipt.operation_id,
|
|
attempt_id=receipt.attempt_id,
|
|
outcome=receipt.outcome,
|
|
result_ref=receipt.result_ref,
|
|
)
|
|
|
|
def materialize(
|
|
self,
|
|
*,
|
|
ref: AgentNativeTestRef,
|
|
intent: AgentEffectIntent,
|
|
permit: AgentDispatchPermit,
|
|
tool_ref: str,
|
|
phase: Literal["refresh", "business"],
|
|
) -> AgentMaterialization:
|
|
owned_ref, effect, native_permit = api_ref(ref), api_intent(intent), api_permit(permit)
|
|
if (
|
|
(phase == "refresh" and effect.kind != "oauth")
|
|
or (phase == "business" and effect.kind != "tool")
|
|
or effect.resource_ref != tool_ref
|
|
):
|
|
raise BuilderExecutionPolicyError("native_materialization_phase_mismatch")
|
|
if self.fence is not self.authorization:
|
|
raise BuilderExecutionPolicyError("native_dispatch_fence_unavailable")
|
|
if self.materializer is None:
|
|
raise BuilderExecutionPolicyError("native_materialization_unavailable")
|
|
self.authorization.validate_dispatch(ref=owned_ref, intent=effect, permit=native_permit)
|
|
# The materializer owns refresh claim_dispatch immediately before its
|
|
# physical OAuth boundary. Business consumption belongs to the tool
|
|
# dispatcher. Never consume the same permit again at this HTTP adapter.
|
|
return self.materializer.materialize(
|
|
ref=owned_ref, intent=effect, permit=native_permit, tool_ref=tool_ref, phase=phase
|
|
)
|
|
|
|
|
|
def agent_bridge_for_ref(ref: AgentNativeTestRef) -> AgentNativeTestingBridge:
|
|
"""Service-authenticated route supplies only a ref; actor comes from SQL."""
|
|
with Session(db.engine) as session:
|
|
row = session.scalar(
|
|
select(DifyBuilderExecutionRequest).where(DifyBuilderExecutionRequest.id == ref.request_id)
|
|
)
|
|
if row is None or row.context_digest != ref.context_digest:
|
|
raise BuilderExecutionPolicyError("native_owner_mismatch")
|
|
actor = Actor(account_id=row.actor_id, tenant_id=row.tenant_id)
|
|
factory = current_app.extensions.get("native_agent_bridge_factory")
|
|
if factory is None:
|
|
raise BuilderExecutionPolicyError("native_agent_bridge_unavailable")
|
|
bridge = factory(actor=actor)
|
|
if not isinstance(bridge, AgentNativeTestingBridge) or bridge.authorization.loader.actor != actor:
|
|
raise BuilderExecutionPolicyError("native_agent_bridge_unavailable")
|
|
return bridge
|
|
|
|
|
|
class AgentEndpointLineagePort(Protocol):
|
|
"""A4/A5 resolves authenticated principal + stored run, never an input flag.
|
|
|
|
None means a positively identified ordinary call. Missing/ambiguous native
|
|
identity must raise, not return None. This port must cover omission of an
|
|
envelope as well as the presence of one before native admission is enabled.
|
|
"""
|
|
|
|
def resolve(self, *, boundary: str, caller: dict) -> AgentNativeTestRef | None: ...
|
|
|
|
|
|
def claim_agent_endpoint(
|
|
*,
|
|
boundary: str,
|
|
caller: dict,
|
|
envelope: AgentPhysicalDispatchPayload | None,
|
|
target,
|
|
parameters,
|
|
kind: str,
|
|
physical_owner_ready: bool = True,
|
|
) -> None:
|
|
lineage = current_app.extensions.get("native_agent_endpoint_lineage")
|
|
if lineage is None:
|
|
if envelope is not None:
|
|
raise BuilderExecutionPolicyError("native_endpoint_lineage_unavailable")
|
|
# Compatibility for ordinary deployments only. The native admission
|
|
# capability MUST remain disabled until the trusted lineage port covers
|
|
# every endpoint, including an omitted envelope. This is not native proof.
|
|
return
|
|
required = lineage.resolve(boundary=boundary, caller=caller)
|
|
if required is None:
|
|
if envelope is not None:
|
|
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
|
|
return
|
|
if envelope is None or envelope.ref != required:
|
|
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
|
|
effect = envelope.intent
|
|
if (
|
|
effect.kind != kind
|
|
or canonical_json(effect.target) != canonical_json(target)
|
|
or canonical_json(effect.parameters) != canonical_json(parameters)
|
|
):
|
|
raise BuilderExecutionPolicyError("native_endpoint_arguments_mismatch")
|
|
if not physical_owner_ready:
|
|
raise BuilderExecutionPolicyError("native_endpoint_dependency_unavailable")
|
|
bridge = agent_bridge_for_ref(required)
|
|
owned = bridge.authorization.loader.load(ref=api_ref(required))
|
|
if (
|
|
caller.get("tenant_id") != owned.tenant_id
|
|
or caller.get("app_id") != owned.app_id
|
|
or caller.get("user_id") != owned.actor_id
|
|
or caller.get("user_from") != "account"
|
|
):
|
|
raise BuilderExecutionPolicyError("native_endpoint_owner_mismatch")
|
|
bridge.authorization.claim_dispatch(
|
|
ref=api_ref(required), intent=api_intent(effect), permit=api_permit(envelope.permit)
|
|
)
|