From f3fe455aa3b8408b6e9f0fefa1dfc4377a97c8ea Mon Sep 17 00:00:00 2001 From: DeryFerd Date: Sat, 16 May 2026 17:36:44 +0700 Subject: [PATCH] fix(agents): enforce sandbox disabled on remote and cloud workplaces Co-authored-by: Cursor --- routes/agents.py | 23 +++---- ...test_agent_sandbox_workplace_validation.py | 64 +++++++++++++++++++ 2 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 unit_tests/test_agent_sandbox_workplace_validation.py diff --git a/routes/agents.py b/routes/agents.py index a7f02d9..97a0b58 100644 --- a/routes/agents.py +++ b/routes/agents.py @@ -7,7 +7,7 @@ import json import uuid import queue -from typing import Dict, Any, List +from typing import Dict, Any, List, Optional from flask import Blueprint, render_template, jsonify, request, Response, stream_with_context from models.db import db from models.chatlog import chatlog_manager, _DISPLAY_TYPES @@ -30,6 +30,15 @@ def _sanitize_agents(agents: List[Dict[str, Any]]) -> List[Dict[str, Any]]: _sanitize_agent(a) return agents + +def _apply_sandbox_workplace_policy(agent_data: dict, workplace_id: Optional[str]) -> None: + """Docker sandbox is only supported on local workplaces.""" + if not workplace_id: + return + workplace = db.get_workplace(workplace_id) + if workplace and workplace.get('type') in ('remote', 'cloud'): + agent_data['sandbox_enabled'] = 0 + BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) AGENTS_DIR = os.path.join(BASE_DIR, 'agents') WORKSPACE_DIR = os.path.join(BASE_DIR, 'shared', 'agents') @@ -177,11 +186,7 @@ def api_create_agent(): return jsonify({'error': 'Description too long (max 2000 characters).'}), 400 if len(data.get('system_prompt', '')) > 102400: return jsonify({'error': 'System prompt too long (max 100 KB).'}), 400 - # Docker Sandbox only available for local workplace mode - if data.get('sandbox_enabled') and data.get('workplace_id'): - workplace = db.get_workplace(data['workplace_id']) - if workplace and workplace.get('type') in ('remote', 'cloud'): - data['sandbox_enabled'] = 0 + _apply_sandbox_workplace_policy(data, data.get('workplace_id')) try: _ensure_kb_dir(agent_id) # Set default workspace for regular agents to shared/agents/[agent-id] @@ -207,12 +212,8 @@ def api_update_agent(agent_id): # Super agent cannot be disabled if existing.get('is_super') and data.get('enabled') is False: return jsonify({'error': 'Super agent cannot be disabled.'}), 403 - # Docker Sandbox only available for local workplace mode target_workplace_id = data.get('workplace_id', existing.get('workplace_id')) - if data.get('sandbox_enabled') and target_workplace_id: - workplace = db.get_workplace(target_workplace_id) - if workplace and workplace.get('type') in ('remote', 'cloud'): - del data['sandbox_enabled'] # Do not overwrite existing value in database + _apply_sandbox_workplace_policy(data, target_workplace_id) if 'system_prompt' in data: _write_system_prompt(agent_id, data['system_prompt']) db.update_agent(agent_id, data) diff --git a/unit_tests/test_agent_sandbox_workplace_validation.py b/unit_tests/test_agent_sandbox_workplace_validation.py new file mode 100644 index 0000000..a896530 --- /dev/null +++ b/unit_tests/test_agent_sandbox_workplace_validation.py @@ -0,0 +1,64 @@ +""" +Tests for sandbox_enabled enforcement against workplace type (issue #35). +""" + +import unittest +from unittest.mock import patch + +from routes.agents import _apply_sandbox_workplace_policy + + +class TestAgentSandboxWorkplaceValidation(unittest.TestCase): + def _mock_get_workplace(self, workplace_id): + workplaces = { + 'local-wp': {'id': 'local-wp', 'type': 'local'}, + 'remote-wp': {'id': 'remote-wp', 'type': 'remote'}, + 'cloud-wp': {'id': 'cloud-wp', 'type': 'cloud'}, + } + return workplaces.get(workplace_id) + + @patch('routes.agents.db.get_workplace') + def test_local_workplace_allows_sandbox_on(self, mock_get): + mock_get.side_effect = self._mock_get_workplace + data = {'sandbox_enabled': 1} + _apply_sandbox_workplace_policy(data, 'local-wp') + self.assertEqual(data['sandbox_enabled'], 1) + + @patch('routes.agents.db.get_workplace') + def test_local_workplace_allows_sandbox_off(self, mock_get): + mock_get.side_effect = self._mock_get_workplace + data = {'sandbox_enabled': 0} + _apply_sandbox_workplace_policy(data, 'local-wp') + self.assertEqual(data['sandbox_enabled'], 0) + + @patch('routes.agents.db.get_workplace') + def test_remote_workplace_forces_sandbox_off_when_enabling(self, mock_get): + mock_get.side_effect = self._mock_get_workplace + data = {'sandbox_enabled': 1} + _apply_sandbox_workplace_policy(data, 'remote-wp') + self.assertEqual(data['sandbox_enabled'], 0) + + @patch('routes.agents.db.get_workplace') + def test_remote_workplace_forces_sandbox_off_when_already_disabled(self, mock_get): + mock_get.side_effect = self._mock_get_workplace + data = {'sandbox_enabled': 0} + _apply_sandbox_workplace_policy(data, 'remote-wp') + self.assertEqual(data['sandbox_enabled'], 0) + + @patch('routes.agents.db.get_workplace') + def test_cloud_workplace_forces_sandbox_off(self, mock_get): + mock_get.side_effect = self._mock_get_workplace + data = {'sandbox_enabled': 1} + _apply_sandbox_workplace_policy(data, 'cloud-wp') + self.assertEqual(data['sandbox_enabled'], 0) + + @patch('routes.agents.db.get_workplace') + def test_no_workplace_id_leaves_data_unchanged(self, mock_get): + data = {'sandbox_enabled': 1} + _apply_sandbox_workplace_policy(data, None) + self.assertEqual(data['sandbox_enabled'], 1) + mock_get.assert_not_called() + + +if __name__ == '__main__': + unittest.main()