feat(api): add sandbox info endpoint for Agent App conversations (#38390)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
林玮 (Jade Lin)
2026-07-03 09:43:57 +00:00
committed by GitHub
co-authored by autofix-ci[bot]
parent d3ed533716
commit 5e17af4530
8 changed files with 223 additions and 34 deletions
@@ -9,13 +9,17 @@ from dify_agent.protocol import SandboxListResponse, SandboxReadResponse, Sandbo
from controllers.console import agent_app_sandbox as module
from models.model import App, AppMode, IconType
from services.agent_app_sandbox_service import AgentSandboxInspectorError
from services.agent_app_sandbox_service import AgentSandboxInfo, AgentSandboxInspectorError
class _AgentAppService:
def __init__(self) -> None:
self.calls: list[tuple[str, str, str, str, str]] = []
def get_info(self, *, tenant_id: str, app_id: str, conversation_id: str) -> AgentSandboxInfo:
self.calls.append(("info", tenant_id, app_id, conversation_id, ""))
return AgentSandboxInfo(session_id="abc1234", workspace_cwd="~/workspace/abc1234")
def list_files(self, *, tenant_id: str, app_id: str, conversation_id: str, path: str) -> SandboxListResponse:
self.calls.append(("list", tenant_id, app_id, conversation_id, path))
return SandboxListResponse(path=path, entries=[], truncated=False)
@@ -131,14 +135,17 @@ def test_agent_app_sandbox_resources_proxy_service(monkeypatch: pytest.MonkeyPat
SimpleNamespace(get_json=lambda silent=True: {"conversation_id": "conv-1", "path": "report.txt"}),
)
info = unwrap(module.AgentAppSandboxInfoResource.get)(object(), "tenant-1", "agent-1")
listing = unwrap(module.AgentAppSandboxListResource.get)(object(), "tenant-1", "agent-1")
preview = unwrap(module.AgentAppSandboxReadResource.get)(object(), "tenant-1", "agent-1")
upload = unwrap(module.AgentAppSandboxUploadResource.post)(object(), "tenant-1", "agent-1")
assert info == {"session_id": "abc1234", "workspace_cwd": "~/workspace/abc1234"}
assert listing["path"] == "sub/report.txt"
assert preview["text"] == "hello"
assert upload["file"]["reference"] == "dify-file-ref:file-1"
assert service.calls == [
("info", "tenant-1", "app-1", "conv-1", ""),
("list", "tenant-1", "app-1", "conv-1", "sub/report.txt"),
("read", "tenant-1", "app-1", "conv-1", "sub/report.txt"),
("upload", "tenant-1", "app-1", "conv-1", "report.txt"),
@@ -147,6 +154,9 @@ def test_agent_app_sandbox_resources_proxy_service(monkeypatch: pytest.MonkeyPat
def test_agent_app_sandbox_resource_returns_normalized_errors(monkeypatch: pytest.MonkeyPatch) -> None:
class FailingService:
def get_info(self, **kwargs):
raise AgentSandboxInspectorError("no_active_session", "no active session", status_code=404)
def list_files(self, **kwargs):
raise AgentSandboxInspectorError("no_active_session", "no active session", status_code=404)
@@ -156,6 +166,10 @@ def test_agent_app_sandbox_resource_returns_normalized_errors(monkeypatch: pytes
module, "query_params_from_request", lambda model: SimpleNamespace(conversation_id="conv-1", path=".")
)
assert unwrap(module.AgentAppSandboxInfoResource.get)(object(), "tenant-1", "agent-1") == (
{"code": "no_active_session", "message": "no active session"},
404,
)
assert unwrap(module.AgentAppSandboxListResource.get)(object(), "tenant-1", "agent-1") == (
{"code": "no_active_session", "message": "no active session"},
404,
@@ -23,14 +23,23 @@ from services.agent_app_sandbox_service import (
)
def _snapshot(*, session_id: str = "abc1234") -> CompositorSessionSnapshot:
def _snapshot(
*,
session_id: str = "abc1234",
shell_runtime_state: dict[str, str] | None = None,
) -> CompositorSessionSnapshot:
runtime_state = (
{"session_id": session_id, "workspace_cwd": f"~/workspace/{session_id}"}
if shell_runtime_state is None
else shell_runtime_state
)
return CompositorSessionSnapshot(
layers=[
LayerSessionSnapshot(name="execution_context", lifecycle_state=LifecycleState.SUSPENDED, runtime_state={}),
LayerSessionSnapshot(
name="shell",
lifecycle_state=LifecycleState.SUSPENDED,
runtime_state={"session_id": session_id, "workspace_cwd": f"~/workspace/{session_id}"},
runtime_state=runtime_state,
),
]
)
@@ -77,7 +86,10 @@ class FakeClient:
)
def _stored_session() -> StoredAgentAppSession:
def _stored_session(
*,
session_snapshot: CompositorSessionSnapshot | None = None,
) -> StoredAgentAppSession:
return StoredAgentAppSession(
scope=AgentAppSessionScope(
tenant_id="tenant-1",
@@ -86,12 +98,25 @@ def _stored_session() -> StoredAgentAppSession:
agent_id="agent-1",
agent_config_snapshot_id="snapshot-1",
),
session_snapshot=_snapshot(),
session_snapshot=_snapshot() if session_snapshot is None else session_snapshot,
backend_run_id="run-1",
runtime_layer_specs=_runtime_layer_specs(),
)
def test_agent_app_sandbox_service_get_info_returns_metadata() -> None:
store = FakeStore(_stored_session())
client = FakeClient()
service = AgentAppSandboxService(session_store=store, client_factory=lambda: client) # type: ignore[arg-type]
result = service.get_info(tenant_id="tenant-1", app_id="app-1", conversation_id="conv-1")
assert result.session_id == "abc1234"
assert result.workspace_cwd == "~/workspace/abc1234"
assert client.calls == []
assert store.scope == ("tenant-1", "app-1", "conv-1")
def test_agent_app_sandbox_service_builds_locator_and_proxies() -> None:
store = FakeStore(_stored_session())
client = FakeClient()
@@ -108,31 +133,21 @@ def test_agent_app_sandbox_service_raises_when_no_active_session() -> None:
service = AgentAppSandboxService(session_store=FakeStore(None), client_factory=lambda: FakeClient()) # type: ignore[arg-type]
with pytest.raises(AgentSandboxInspectorError) as exc_info:
service.read_file(tenant_id="tenant-1", app_id="app-1", conversation_id="conv-1", path="note.txt")
service.get_info(tenant_id="tenant-1", app_id="app-1", conversation_id="conv-1")
assert exc_info.value.code == "no_active_session"
assert exc_info.value.status_code == 404
def test_agent_app_sandbox_service_raises_when_runtime_specs_cannot_build_locator() -> None:
broken_session = StoredAgentAppSession(
scope=AgentAppSessionScope(
tenant_id="tenant-1",
app_id="app-1",
conversation_id="conv-1",
agent_id="agent-1",
agent_config_snapshot_id="snapshot-1",
),
session_snapshot=_snapshot(),
backend_run_id="run-1",
runtime_layer_specs=[],
)
def test_agent_app_sandbox_service_raises_when_shell_workspace_metadata_missing() -> None:
broken_session = _stored_session(session_snapshot=_snapshot(shell_runtime_state={"session_id": "abc1234"}))
service = AgentAppSandboxService(session_store=FakeStore(broken_session), client_factory=lambda: FakeClient()) # type: ignore[arg-type]
with pytest.raises(AgentSandboxInspectorError) as exc_info:
service.list_files(tenant_id="tenant-1", app_id="app-1", conversation_id="conv-1", path=".")
service.get_info(tenant_id="tenant-1", app_id="app-1", conversation_id="conv-1")
assert exc_info.value.code == "no_sandbox"
assert exc_info.value.status_code == 404
def test_default_client_factory_requires_agent_backend_base_url(monkeypatch: pytest.MonkeyPatch) -> None: