Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion backend/app/api/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ def _safe_path(agent_id: uuid.UUID, rel_path: str) -> Path:
return full


async def _require_agent_file_delete_access(
db: AsyncSession,
current_user: User,
agent_id: uuid.UUID,
) -> None:
"""Allow destructive workspace file operations only for managers/admins."""
_agent, access_level = await check_agent_access(db, current_user, agent_id)
if access_level == "manage" or current_user.role in ("platform_admin", "org_admin", "super_admin"):
return
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only agent managers or admins can delete files",
)


@router.get("/", response_model=list[FileInfo])
async def list_files(
agent_id: uuid.UUID,
Expand Down Expand Up @@ -177,7 +192,7 @@ async def delete_file(
db: AsyncSession = Depends(get_db),
):
"""Delete a file."""
await check_agent_access(db, current_user, agent_id)
await _require_agent_file_delete_access(db, current_user, agent_id)
target = _safe_path(agent_id, path)

if not target.exists():
Expand Down
84 changes: 84 additions & 0 deletions backend/tests/test_files_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import uuid

import pytest
from fastapi import HTTPException

from app.api import files as files_api
from app.models.agent import Agent
from app.models.user import User


def make_user(**overrides):
values = {
"id": uuid.uuid4(),
"display_name": "Alice",
"role": "member",
"tenant_id": uuid.uuid4(),
"is_active": True,
}
values.update(overrides)
return User(**values)


def make_agent(creator_id: uuid.UUID, **overrides):
values = {
"id": uuid.uuid4(),
"name": "Ops Bot",
"role_description": "assistant",
"creator_id": creator_id,
"status": "idle",
"agent_type": "native",
}
values.update(overrides)
return Agent(**values)


@pytest.mark.asyncio
async def test_use_access_cannot_delete_agent_workspace_file(monkeypatch, tmp_path):
user = make_user()
agent = make_agent(uuid.uuid4(), tenant_id=user.tenant_id)
workspace_file = tmp_path / str(agent.id) / "workspace" / "important.md"
workspace_file.parent.mkdir(parents=True)
workspace_file.write_text("do not delete", encoding="utf-8")

async def fake_check_agent_access(_db, _current_user, _agent_id):
return agent, "use"

monkeypatch.setattr(files_api.settings, "AGENT_DATA_DIR", str(tmp_path))
monkeypatch.setattr(files_api, "check_agent_access", fake_check_agent_access)

with pytest.raises(HTTPException) as exc:
await files_api.delete_file(
agent_id=agent.id,
path="workspace/important.md",
current_user=user,
db=object(),
)

assert exc.value.status_code == 403
assert workspace_file.exists()


@pytest.mark.asyncio
async def test_manage_access_can_delete_agent_workspace_file(monkeypatch, tmp_path):
user = make_user()
agent = make_agent(user.id, tenant_id=user.tenant_id)
workspace_file = tmp_path / str(agent.id) / "workspace" / "obsolete.md"
workspace_file.parent.mkdir(parents=True)
workspace_file.write_text("delete me", encoding="utf-8")

async def fake_check_agent_access(_db, _current_user, _agent_id):
return agent, "manage"

monkeypatch.setattr(files_api.settings, "AGENT_DATA_DIR", str(tmp_path))
monkeypatch.setattr(files_api, "check_agent_access", fake_check_agent_access)

result = await files_api.delete_file(
agent_id=agent.id,
path="workspace/obsolete.md",
current_user=user,
db=object(),
)

assert result == {"status": "ok", "path": "workspace/obsolete.md"}
assert not workspace_file.exists()
4 changes: 2 additions & 2 deletions frontend/src/pages/AgentDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3839,7 +3839,7 @@ function AgentDetailInner() {
• <code>skills/my-skill/SKILL.md</code> — {t('agent.skills.folderFormat', 'Each skill is a folder with a SKILL.md file and optional auxiliary files (scripts/, examples/)')}
</div>
</div>
<FileBrowser api={adapter} rootPath="skills" features={{ newFile: true, edit: true, delete: true, newFolder: true, upload: true, directoryNavigation: true }} title={t('agent.skills.skillFiles')} />
<FileBrowser api={adapter} rootPath="skills" features={{ newFile: true, edit: true, delete: canManage, newFolder: true, upload: true, directoryNavigation: true }} title={t('agent.skills.skillFiles')} />

{/* Browse ClawHub Modal */}
{showAgentClawhub && (
Expand Down Expand Up @@ -4054,7 +4054,7 @@ function AgentDetailInner() {
upload: (file, path, onProgress) => fileApi.upload(id!, file, path + '/', onProgress),
downloadUrl: (p) => fileApi.downloadUrl(id!, p),
};
return <FileBrowser api={adapter} rootPath="workspace" features={{ upload: true, newFile: true, newFolder: true, edit: true, delete: true, directoryNavigation: true }} />;
return <FileBrowser api={adapter} rootPath="workspace" features={{ upload: true, newFile: true, newFolder: true, edit: true, delete: canManage, directoryNavigation: true }} />;
})()
}

Expand Down