@@ -53,9 +53,9 @@ def test_defaults(self):
5353 cfg = LocalManagedConfig ()
5454 assert cfg .name == "Agent"
5555 assert cfg .model == "gpt-4o"
56- assert cfg .sandbox_type == "subprocess"
5756 assert cfg .max_turns == 25
5857 assert "execute_command" in cfg .tools
58+ assert cfg .host_packages_ok is False
5959
6060 def test_custom_config (self ):
6161 from praisonai .integrations .managed_local import LocalManagedConfig
@@ -353,6 +353,214 @@ def test_packages_in_config(self):
353353 cfg = LocalManagedConfig (packages = {"pip" : ["pandas" , "numpy" ]})
354354 assert cfg .packages == {"pip" : ["pandas" , "numpy" ]}
355355
356+ def test_host_packages_ok_default_false (self ):
357+ from praisonai .integrations .managed_local import LocalManagedConfig
358+ cfg = LocalManagedConfig ()
359+ assert cfg .host_packages_ok is False
360+
361+ def test_host_packages_ok_explicit_true (self ):
362+ from praisonai .integrations .managed_local import LocalManagedConfig
363+ cfg = LocalManagedConfig (host_packages_ok = True )
364+ assert cfg .host_packages_ok is True
365+
366+
367+ class TestManagedSandboxSafety :
368+ """Test security features for managed agents package installation."""
369+
370+ def test_install_packages_without_compute_raises (self ):
371+ """Test that package installation without compute provider raises ManagedSandboxRequired."""
372+ import pytest
373+ from praisonai .integrations .managed_local import LocalManagedAgent , LocalManagedConfig
374+ from praisonai .integrations .managed_agents import ManagedSandboxRequired
375+
376+ cfg = LocalManagedConfig (packages = {"pip" : ["requests" ]})
377+ agent = LocalManagedAgent (config = cfg )
378+
379+ with pytest .raises (ManagedSandboxRequired ) as exc_info :
380+ agent ._install_packages ()
381+
382+ assert "Package installation requested" in str (exc_info .value )
383+ assert "security risk" in str (exc_info .value )
384+ assert "compute='docker'" in str (exc_info .value )
385+ assert "host_packages_ok=True" in str (exc_info .value )
386+
387+ def test_install_packages_with_host_packages_ok_succeeds (self ):
388+ """Test that package installation with host_packages_ok=True succeeds."""
389+ from unittest .mock import patch
390+ from praisonai .integrations .managed_local import LocalManagedAgent , LocalManagedConfig
391+
392+ cfg = LocalManagedConfig (packages = {"pip" : ["requests" ]}, host_packages_ok = True )
393+ agent = LocalManagedAgent (config = cfg )
394+
395+ with patch ('praisonai.integrations.managed_local.subprocess.run' ) as mock_run :
396+ mock_run .return_value = None
397+ agent ._install_packages () # Should not raise
398+ mock_run .assert_called_once ()
399+
400+ def test_install_packages_with_compute_uses_sandbox (self ):
401+ """Test that package installation with compute provider uses sandbox."""
402+ from unittest .mock import AsyncMock , patch
403+ from praisonai .integrations .managed_local import LocalManagedAgent , LocalManagedConfig
404+
405+ cfg = LocalManagedConfig (packages = {"pip" : ["requests" ]})
406+ agent = LocalManagedAgent (config = cfg , compute = "local" )
407+
408+ # Mock the compute execution
409+ with patch .object (agent , 'provision_compute' ) as mock_provision , \
410+ patch .object (agent ._compute , 'execute' ) as mock_execute , \
411+ patch ('asyncio.run' ) as mock_asyncio_run , \
412+ patch ('asyncio.get_event_loop' ) as mock_get_loop :
413+
414+ mock_provision .return_value = None
415+ mock_execute .return_value = {"exit_code" : 0 , "stdout" : "installed" }
416+ agent ._compute_instance_id = "test_instance"
417+ mock_asyncio_run .return_value = {"exit_code" : 0 , "stdout" : "installed" }
418+
419+ agent ._install_packages ()
420+
421+ # Verify subprocess.run was NOT called (no host installation)
422+ with patch ('praisonai.integrations.managed_local.subprocess.run' ) as mock_run :
423+ agent ._install_packages ()
424+ mock_run .assert_not_called ()
425+
426+ def test_no_packages_no_error (self ):
427+ """Test that agents without packages work normally."""
428+ from praisonai .integrations .managed_local import LocalManagedAgent
429+ agent = LocalManagedAgent ()
430+ agent ._install_packages () # Should not raise
431+
432+ def test_empty_packages_no_error (self ):
433+ """Test that empty packages dict works normally."""
434+ from praisonai .integrations .managed_local import LocalManagedConfig , LocalManagedAgent
435+ cfg = LocalManagedConfig (packages = {"pip" : []})
436+ agent = LocalManagedAgent (config = cfg )
437+ agent ._install_packages () # Should not raise
438+
439+
440+ class TestComputeToolBridge :
441+ """Test compute-based tool execution routing."""
442+
443+ def test_bridged_tools_created_when_compute_attached (self ):
444+ """Test that shell-based tools are bridged when compute is attached."""
445+ from praisonai .integrations .managed_local import LocalManagedAgent
446+ agent = LocalManagedAgent (compute = "local" )
447+ tools = agent ._resolve_tools ()
448+
449+ # Should have tools but they should be wrapped/bridged versions
450+ tool_names = [getattr (t , '__name__' , str (t )) for t in tools if callable (t )]
451+ assert "execute_command" in tool_names
452+
453+ def test_non_bridged_tools_use_original_when_no_compute (self ):
454+ """Test that tools use original implementation when no compute."""
455+ from praisonai .integrations .managed_local import LocalManagedAgent
456+ agent = LocalManagedAgent ()
457+ tools = agent ._resolve_tools ()
458+
459+ # Should have original tools
460+ tool_names = [getattr (t , '__name__' , str (t )) for t in tools if callable (t )]
461+ assert "execute_command" in tool_names
462+
463+ def test_compute_bridge_tool_execute_command (self ):
464+ """Test that execute_command is properly bridged to compute."""
465+ from unittest .mock import AsyncMock , patch
466+ from praisonai .integrations .managed_local import LocalManagedAgent
467+
468+ agent = LocalManagedAgent (compute = "local" )
469+ agent ._compute_instance_id = "test_instance"
470+
471+ # Create a bridge tool for execute_command
472+ original_func = lambda command : "original result"
473+ bridge_tool = agent ._create_compute_bridge_tool ("execute_command" , original_func )
474+
475+ with patch .object (agent ._compute , 'execute' ) as mock_execute , \
476+ patch ('asyncio.run' ) as mock_asyncio_run :
477+
478+ mock_asyncio_run .return_value = {"exit_code" : 0 , "stdout" : "compute result" }
479+
480+ result = bridge_tool ("echo hello" )
481+ assert result == "compute result"
482+
483+ # Verify it attempted to run in compute, not locally
484+ mock_asyncio_run .assert_called ()
485+
486+ def test_compute_bridge_tool_read_file (self ):
487+ """Test that read_file is properly bridged to compute."""
488+ from unittest .mock import patch
489+ from praisonai .integrations .managed_local import LocalManagedAgent
490+
491+ agent = LocalManagedAgent (compute = "local" )
492+ agent ._compute_instance_id = "test_instance"
493+
494+ original_func = lambda filepath : "original content"
495+ bridge_tool = agent ._create_compute_bridge_tool ("read_file" , original_func )
496+
497+ with patch .object (agent , '_bridge_file_tool' ) as mock_bridge :
498+ mock_bridge .return_value = "file content from compute"
499+
500+ result = bridge_tool ("/path/to/file" )
501+ assert result == "file content from compute"
502+ mock_bridge .assert_called_once_with ("read_file" , "/path/to/file" )
503+
504+ def test_compute_bridge_tool_write_file (self ):
505+ """Test that write_file is properly bridged to compute."""
506+ from unittest .mock import patch
507+ from praisonai .integrations .managed_local import LocalManagedAgent
508+
509+ agent = LocalManagedAgent (compute = "local" )
510+ agent ._compute_instance_id = "test_instance"
511+
512+ original_func = lambda filepath , content : "written locally"
513+ bridge_tool = agent ._create_compute_bridge_tool ("write_file" , original_func )
514+
515+ with patch .object (agent , '_bridge_file_tool' ) as mock_bridge :
516+ mock_bridge .return_value = "written to compute"
517+
518+ result = bridge_tool ("/path/to/file" , "content" )
519+ assert result == "written to compute"
520+ mock_bridge .assert_called_once_with ("write_file" , "/path/to/file" , "content" )
521+
522+ def test_bridge_file_tool_read (self ):
523+ """Test _bridge_file_tool for read operations."""
524+ from unittest .mock import patch
525+ from praisonai .integrations .managed_local import LocalManagedAgent
526+
527+ agent = LocalManagedAgent (compute = "local" )
528+ agent ._compute_instance_id = "test_instance"
529+
530+ with patch ('asyncio.run' ) as mock_asyncio_run :
531+ mock_asyncio_run .return_value = {"exit_code" : 0 , "stdout" : "file contents" }
532+
533+ result = agent ._bridge_file_tool ("read_file" , "/test/file" )
534+ assert result == "file contents"
535+
536+ def test_bridge_file_tool_write (self ):
537+ """Test _bridge_file_tool for write operations."""
538+ from unittest .mock import patch
539+ from praisonai .integrations .managed_local import LocalManagedAgent
540+
541+ agent = LocalManagedAgent (compute = "local" )
542+ agent ._compute_instance_id = "test_instance"
543+
544+ with patch ('asyncio.run' ) as mock_asyncio_run :
545+ mock_asyncio_run .return_value = {"exit_code" : 0 , "stdout" : "" }
546+
547+ result = agent ._bridge_file_tool ("write_file" , "/test/file" , "content" )
548+ assert result == ""
549+
550+ def test_bridge_file_tool_list (self ):
551+ """Test _bridge_file_tool for list operations."""
552+ from unittest .mock import patch
553+ from praisonai .integrations .managed_local import LocalManagedAgent
554+
555+ agent = LocalManagedAgent (compute = "local" )
556+ agent ._compute_instance_id = "test_instance"
557+
558+ with patch ('asyncio.run' ) as mock_asyncio_run :
559+ mock_asyncio_run .return_value = {"exit_code" : 0 , "stdout" : "file1\n file2\n " }
560+
561+ result = agent ._bridge_file_tool ("list_files" , "/test/dir" )
562+ assert result == "file1\n file2\n "
563+
356564
357565class TestUpdateAgentKeepsSession :
358566 def test_update_preserves_session (self ):
0 commit comments