mirror of
https://github.com/langgenius/dify.git
synced 2026-06-08 00:41:55 +08:00
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
221 lines
8.0 KiB
Python
221 lines
8.0 KiB
Python
"""Resolve and proxy read-only access to an Agent App conversation's sandbox.
|
|
|
|
The Agent App's shell layer runs bash in a per-conversation sandbox workspace on
|
|
the agent backend. The workspace identity (``session_id``) is generated inside
|
|
the shell layer and rides the conversation's ``session_snapshot``. This service
|
|
extracts that id and proxies list/preview/download to the agent backend's
|
|
read-only workspace endpoints, so the console can show a "sandbox file system"
|
|
inspector without the API ever touching shellctl directly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
|
|
from agenton.compositor import CompositorSessionSnapshot
|
|
from sqlalchemy import select
|
|
|
|
from clients.agent_backend.request_builder import DIFY_SHELL_LAYER_ID
|
|
from clients.agent_backend.workspace_files_client import (
|
|
WorkspaceDownloadResult,
|
|
WorkspaceFilesBackendClient,
|
|
WorkspaceListResult,
|
|
WorkspacePreviewResult,
|
|
)
|
|
from configs import dify_config
|
|
from core.app.apps.agent_app.session_store import AgentAppRuntimeSessionStore
|
|
from core.db.session_factory import session_factory
|
|
from models.agent import (
|
|
AgentRuntimeSessionOwnerType,
|
|
WorkflowAgentRuntimeSession,
|
|
WorkflowAgentRuntimeSessionStatus,
|
|
)
|
|
|
|
|
|
class AgentWorkspaceInspectorError(Exception):
|
|
"""A workspace inspection failure mapped to an HTTP status by the controller."""
|
|
|
|
code: str
|
|
message: str
|
|
status_code: int
|
|
|
|
def __init__(self, code: str, message: str, *, status_code: int = 400) -> None:
|
|
super().__init__(message)
|
|
self.code = code
|
|
self.message = message
|
|
self.status_code = status_code
|
|
|
|
|
|
class AgentAppWorkspaceService:
|
|
"""List/preview/download files in an Agent App conversation's sandbox workspace."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
session_store: AgentAppRuntimeSessionStore | None = None,
|
|
client_factory: Callable[[], WorkspaceFilesBackendClient] | None = None,
|
|
) -> None:
|
|
self._session_store = session_store or AgentAppRuntimeSessionStore()
|
|
self._client_factory = client_factory or _default_client_factory
|
|
|
|
def list_files(self, *, tenant_id: str, app_id: str, conversation_id: str, path: str) -> WorkspaceListResult:
|
|
session_id = self._resolve_session_id(tenant_id=tenant_id, app_id=app_id, conversation_id=conversation_id)
|
|
return self._client_factory().list_files(session_id, path)
|
|
|
|
def preview(self, *, tenant_id: str, app_id: str, conversation_id: str, path: str) -> WorkspacePreviewResult:
|
|
session_id = self._resolve_session_id(tenant_id=tenant_id, app_id=app_id, conversation_id=conversation_id)
|
|
return self._client_factory().preview(session_id, path)
|
|
|
|
def download(self, *, tenant_id: str, app_id: str, conversation_id: str, path: str) -> WorkspaceDownloadResult:
|
|
session_id = self._resolve_session_id(tenant_id=tenant_id, app_id=app_id, conversation_id=conversation_id)
|
|
return self._client_factory().download(session_id, path)
|
|
|
|
def _resolve_session_id(self, *, tenant_id: str, app_id: str, conversation_id: str) -> str:
|
|
snapshot = self._session_store.load_active_snapshot_for_conversation(
|
|
tenant_id=tenant_id, app_id=app_id, conversation_id=conversation_id
|
|
)
|
|
if snapshot is None:
|
|
raise AgentWorkspaceInspectorError(
|
|
"no_active_session",
|
|
"this conversation has no active sandbox session yet",
|
|
status_code=404,
|
|
)
|
|
session_id = _shell_session_id(snapshot)
|
|
if not session_id:
|
|
raise AgentWorkspaceInspectorError(
|
|
"no_sandbox",
|
|
"this conversation's agent has no sandbox workspace",
|
|
status_code=404,
|
|
)
|
|
return session_id
|
|
|
|
|
|
class WorkflowAgentWorkspaceService:
|
|
"""List/preview/download files in a Workflow Agent node sandbox workspace."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
client_factory: Callable[[], WorkspaceFilesBackendClient] | None = None,
|
|
) -> None:
|
|
self._client_factory = client_factory or _default_client_factory
|
|
|
|
def list_files(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
app_id: str,
|
|
workflow_run_id: str,
|
|
node_id: str,
|
|
node_execution_id: str | None,
|
|
path: str,
|
|
) -> WorkspaceListResult:
|
|
session_id = self._resolve_session_id(
|
|
tenant_id=tenant_id,
|
|
app_id=app_id,
|
|
workflow_run_id=workflow_run_id,
|
|
node_id=node_id,
|
|
node_execution_id=node_execution_id,
|
|
)
|
|
return self._client_factory().list_files(session_id, path)
|
|
|
|
def preview(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
app_id: str,
|
|
workflow_run_id: str,
|
|
node_id: str,
|
|
node_execution_id: str | None,
|
|
path: str,
|
|
) -> WorkspacePreviewResult:
|
|
session_id = self._resolve_session_id(
|
|
tenant_id=tenant_id,
|
|
app_id=app_id,
|
|
workflow_run_id=workflow_run_id,
|
|
node_id=node_id,
|
|
node_execution_id=node_execution_id,
|
|
)
|
|
return self._client_factory().preview(session_id, path)
|
|
|
|
def download(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
app_id: str,
|
|
workflow_run_id: str,
|
|
node_id: str,
|
|
node_execution_id: str | None,
|
|
path: str,
|
|
) -> WorkspaceDownloadResult:
|
|
session_id = self._resolve_session_id(
|
|
tenant_id=tenant_id,
|
|
app_id=app_id,
|
|
workflow_run_id=workflow_run_id,
|
|
node_id=node_id,
|
|
node_execution_id=node_execution_id,
|
|
)
|
|
return self._client_factory().download(session_id, path)
|
|
|
|
def _resolve_session_id(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
app_id: str,
|
|
workflow_run_id: str,
|
|
node_id: str,
|
|
node_execution_id: str | None,
|
|
) -> str:
|
|
stmt = select(WorkflowAgentRuntimeSession).where(
|
|
WorkflowAgentRuntimeSession.owner_type == AgentRuntimeSessionOwnerType.WORKFLOW_RUN,
|
|
WorkflowAgentRuntimeSession.tenant_id == tenant_id,
|
|
WorkflowAgentRuntimeSession.app_id == app_id,
|
|
WorkflowAgentRuntimeSession.workflow_run_id == workflow_run_id,
|
|
WorkflowAgentRuntimeSession.node_id == node_id,
|
|
WorkflowAgentRuntimeSession.status == WorkflowAgentRuntimeSessionStatus.ACTIVE,
|
|
)
|
|
if node_execution_id:
|
|
stmt = stmt.where(WorkflowAgentRuntimeSession.node_execution_id == node_execution_id)
|
|
stmt = stmt.order_by(WorkflowAgentRuntimeSession.updated_at.desc()).limit(1)
|
|
|
|
with session_factory.create_session() as session:
|
|
row = session.scalar(stmt)
|
|
|
|
if row is None:
|
|
raise AgentWorkspaceInspectorError(
|
|
"no_active_session",
|
|
"this workflow Agent node has no active sandbox session yet",
|
|
status_code=404,
|
|
)
|
|
snapshot = CompositorSessionSnapshot.model_validate_json(row.session_snapshot)
|
|
session_id = _shell_session_id(snapshot)
|
|
if not session_id:
|
|
raise AgentWorkspaceInspectorError(
|
|
"no_sandbox",
|
|
"this workflow Agent node has no sandbox workspace",
|
|
status_code=404,
|
|
)
|
|
return session_id
|
|
|
|
|
|
def _shell_session_id(snapshot: CompositorSessionSnapshot) -> str | None:
|
|
for layer in snapshot.layers:
|
|
if layer.name == DIFY_SHELL_LAYER_ID:
|
|
session_id = layer.runtime_state.get("session_id")
|
|
return session_id if isinstance(session_id, str) and session_id else None
|
|
return None
|
|
|
|
|
|
def _default_client_factory() -> WorkspaceFilesBackendClient:
|
|
base_url = dify_config.AGENT_BACKEND_BASE_URL
|
|
if not base_url:
|
|
raise AgentWorkspaceInspectorError(
|
|
"inspector_unavailable",
|
|
"the sandbox file inspector is not available (agent backend not configured)",
|
|
status_code=503,
|
|
)
|
|
return WorkspaceFilesBackendClient(base_url)
|
|
|
|
|
|
__all__ = ["AgentAppWorkspaceService", "AgentWorkspaceInspectorError", "WorkflowAgentWorkspaceService"]
|