@@ -53,7 +53,7 @@ def test_defaults(self):
5353 cfg = LocalManagedConfig ()
5454 assert cfg .name == "Agent"
5555 assert cfg .model == "gpt-4o"
56- assert cfg .sandbox_type == "subprocess"
56+ assert cfg .host_packages_ok is False
5757 assert cfg .max_turns == 25
5858 assert "execute_command" in cfg .tools
5959
@@ -471,3 +471,293 @@ def test_provision_execute_shutdown_local(self):
471471
472472 asyncio .run (agent .shutdown_compute ())
473473 assert agent ._compute_instance_id is None
474+
475+
476+ # ====================================================================== #
477+ # Security tests - package installation and sandbox behavior
478+ # ====================================================================== #
479+
480+ class TestManagedSandboxSafety :
481+ def test_install_packages_without_compute_raises (self ):
482+ """Test that package installation without compute provider raises ManagedSandboxRequired."""
483+ import pytest
484+ from praisonai .integrations .managed_local import LocalManagedAgent , LocalManagedConfig
485+ from praisonai .integrations .managed_agents import ManagedSandboxRequired
486+
487+ cfg = LocalManagedConfig (packages = {"pip" : ["requests" ]})
488+ agent = LocalManagedAgent (config = cfg )
489+
490+ with pytest .raises (ManagedSandboxRequired , match = "Package installation requires compute provider" ):
491+ agent ._install_packages ()
492+
493+ def test_install_packages_with_host_packages_ok_works (self ):
494+ """Test that package installation with explicit opt-out works."""
495+ from unittest .mock import patch , MagicMock
496+ from praisonai .integrations .managed_local import LocalManagedAgent , LocalManagedConfig
497+
498+ cfg = LocalManagedConfig (packages = {"pip" : ["requests" ]}, host_packages_ok = True )
499+ agent = LocalManagedAgent (config = cfg )
500+
501+ with patch ('subprocess.run' ) as mock_run :
502+ mock_run .return_value = MagicMock ()
503+ agent ._install_packages ()
504+ mock_run .assert_called_once ()
505+ args = mock_run .call_args [0 ][0 ]
506+ assert "pip" in args
507+ assert "install" in args
508+ assert "requests" in args
509+
510+ def test_install_packages_with_compute_runs_in_sandbox (self ):
511+ """Test that packages install in compute sandbox when compute provider attached."""
512+ import asyncio
513+ from unittest .mock import patch , MagicMock , AsyncMock
514+ from praisonai .integrations .managed_local import LocalManagedAgent , LocalManagedConfig
515+
516+ mock_compute = MagicMock ()
517+ mock_compute .execute = AsyncMock (return_value = {"exit_code" : 0 , "stdout" : "success" , "stderr" : "" })
518+
519+ cfg = LocalManagedConfig (packages = {"pip" : ["requests" ]})
520+ agent = LocalManagedAgent (config = cfg , compute = mock_compute )
521+ agent ._compute_instance_id = "test_instance"
522+
523+ with patch ('subprocess.run' ) as mock_subprocess :
524+ agent ._install_packages ()
525+ # subprocess.run should NOT be called when compute is attached
526+ mock_subprocess .assert_not_called ()
527+ # compute.execute should be called instead
528+ mock_compute .execute .assert_called_once ()
529+
530+ def test_no_packages_skips_installation (self ):
531+ """Test that no packages specified skips installation entirely."""
532+ from unittest .mock import patch
533+ from praisonai .integrations .managed_local import LocalManagedAgent , LocalManagedConfig
534+
535+ cfg = LocalManagedConfig ()
536+ agent = LocalManagedAgent (config = cfg )
537+
538+ with patch ('subprocess.run' ) as mock_run :
539+ agent ._install_packages ()
540+ mock_run .assert_not_called ()
541+
542+ def test_empty_pip_packages_skips_installation (self ):
543+ """Test that empty pip packages list skips installation."""
544+ from unittest .mock import patch
545+ from praisonai .integrations .managed_local import LocalManagedAgent , LocalManagedConfig
546+
547+ cfg = LocalManagedConfig (packages = {"pip" : []})
548+ agent = LocalManagedAgent (config = cfg )
549+
550+ with patch ('subprocess.run' ) as mock_run :
551+ agent ._install_packages ()
552+ mock_run .assert_not_called ()
553+
554+ def test_exception_message_includes_remediation (self ):
555+ """Test that ManagedSandboxRequired exception includes actionable remediation."""
556+ import pytest
557+ from praisonai .integrations .managed_local import LocalManagedAgent , LocalManagedConfig
558+ from praisonai .integrations .managed_agents import ManagedSandboxRequired
559+
560+ cfg = LocalManagedConfig (packages = {"pip" : ["dangerous-package" ]})
561+ agent = LocalManagedAgent (config = cfg )
562+
563+ with pytest .raises (ManagedSandboxRequired ) as exc_info :
564+ agent ._install_packages ()
565+
566+ error_msg = str (exc_info .value )
567+ assert "dangerous-package" in error_msg
568+ assert "compute='docker'" in error_msg
569+ assert "host_packages_ok=True" in error_msg
570+
571+ def test_managed_sandbox_required_exception_creation (self ):
572+ """Test ManagedSandboxRequired exception can be created and has correct default message."""
573+ from praisonai .integrations .managed_agents import ManagedSandboxRequired
574+
575+ exc = ManagedSandboxRequired ()
576+ assert "Package installation requires compute provider for security" in str (exc )
577+
578+ custom_exc = ManagedSandboxRequired ("Custom message" )
579+ assert str (custom_exc ) == "Custom message"
580+
581+
582+ class TestComputeToolBridge :
583+ """Test compute-bridged tool execution routing."""
584+
585+ def test_tools_use_compute_bridge_when_compute_attached (self ):
586+ """Test that shell tools use compute bridge when compute provider attached."""
587+ from unittest .mock import MagicMock
588+ from praisonai .integrations .managed_local import LocalManagedAgent , LocalManagedConfig
589+
590+ mock_compute = MagicMock ()
591+ cfg = LocalManagedConfig (tools = ["execute_command" , "read_file" , "write_file" , "list_files" ])
592+ agent = LocalManagedAgent (config = cfg , compute = mock_compute )
593+
594+ tools = agent ._resolve_tools ()
595+
596+ # Should have 4 compute-bridged tools
597+ shell_tools = [t for t in tools if hasattr (t , '__name__' ) and
598+ t .__name__ in {"execute_command" , "read_file" , "write_file" , "list_files" }]
599+ assert len (shell_tools ) == 4
600+
601+ def test_tools_use_host_when_no_compute (self ):
602+ """Test that tools use host versions when no compute provider."""
603+ from unittest .mock import patch , MagicMock
604+ from praisonai .integrations .managed_local import LocalManagedAgent , LocalManagedConfig
605+
606+ cfg = LocalManagedConfig (tools = ["execute_command" ])
607+ agent = LocalManagedAgent (config = cfg )
608+
609+ with patch ('praisonaiagents.tools.execute_command' ) as mock_tool :
610+ mock_tool .__name__ = "execute_command"
611+ tools = agent ._resolve_tools ()
612+ # Should use host tool, not compute bridge
613+ assert mock_tool in tools
614+
615+ def test_compute_execute_command_bridge (self ):
616+ """Test compute-bridged execute_command works correctly."""
617+ import asyncio
618+ from unittest .mock import MagicMock , AsyncMock
619+ from praisonai .integrations .managed_local import LocalManagedAgent , LocalManagedConfig
620+
621+ mock_compute = MagicMock ()
622+ mock_compute .execute = AsyncMock (return_value = {
623+ "stdout" : "hello world" ,
624+ "stderr" : "" ,
625+ "exit_code" : 0
626+ })
627+
628+ agent = LocalManagedAgent (compute = mock_compute )
629+ agent ._compute_instance_id = "test_instance"
630+
631+ execute_command = agent ._create_compute_execute_command ()
632+ result = execute_command ("echo hello world" )
633+
634+ assert result == "hello world"
635+ mock_compute .execute .assert_called_once_with ("test_instance" , "echo hello world" , timeout = 300 )
636+
637+ def test_compute_execute_command_with_stderr (self ):
638+ """Test compute-bridged execute_command handles stderr correctly."""
639+ import asyncio
640+ from unittest .mock import MagicMock , AsyncMock
641+ from praisonai .integrations .managed_local import LocalManagedAgent
642+
643+ mock_compute = MagicMock ()
644+ mock_compute .execute = AsyncMock (return_value = {
645+ "stdout" : "output" ,
646+ "stderr" : "warning" ,
647+ "exit_code" : 1
648+ })
649+
650+ agent = LocalManagedAgent (compute = mock_compute )
651+ agent ._compute_instance_id = "test_instance"
652+
653+ execute_command = agent ._create_compute_execute_command ()
654+ result = execute_command ("failing_command" )
655+
656+ assert "output" in result
657+ assert "STDERR: warning" in result
658+ assert "Exit code: 1" in result
659+
660+ def test_compute_read_file_bridge (self ):
661+ """Test compute-bridged read_file works correctly."""
662+ from unittest .mock import MagicMock , AsyncMock
663+ from praisonai .integrations .managed_local import LocalManagedAgent
664+
665+ mock_compute = MagicMock ()
666+ mock_compute .execute = AsyncMock (return_value = {
667+ "stdout" : "file contents" ,
668+ "stderr" : "" ,
669+ "exit_code" : 0
670+ })
671+
672+ agent = LocalManagedAgent (compute = mock_compute )
673+ agent ._compute_instance_id = "test_instance"
674+
675+ read_file = agent ._create_compute_read_file ()
676+ result = read_file ("/path/to/file.txt" )
677+
678+ assert result == "file contents"
679+ mock_compute .execute .assert_called_once_with ("test_instance" , "cat /path/to/file.txt" , timeout = 60 )
680+
681+ def test_compute_write_file_bridge (self ):
682+ """Test compute-bridged write_file works correctly."""
683+ from unittest .mock import MagicMock , AsyncMock
684+ from praisonai .integrations .managed_local import LocalManagedAgent
685+
686+ mock_compute = MagicMock ()
687+ mock_compute .execute = AsyncMock (return_value = {
688+ "stdout" : "" ,
689+ "stderr" : "" ,
690+ "exit_code" : 0
691+ })
692+
693+ agent = LocalManagedAgent (compute = mock_compute )
694+ agent ._compute_instance_id = "test_instance"
695+
696+ write_file = agent ._create_compute_write_file ()
697+ result = write_file ("/path/to/file.txt" , "file content" )
698+
699+ assert "File written successfully" in result
700+ # Check that the command was properly escaped
701+ mock_compute .execute .assert_called_once ()
702+ call_args = mock_compute .execute .call_args
703+ assert "echo" in call_args [0 ][1 ]
704+ assert "/path/to/file.txt" in call_args [0 ][1 ]
705+
706+ def test_compute_list_files_bridge (self ):
707+ """Test compute-bridged list_files works correctly."""
708+ from unittest .mock import MagicMock , AsyncMock
709+ from praisonai .integrations .managed_local import LocalManagedAgent
710+
711+ mock_compute = MagicMock ()
712+ mock_compute .execute = AsyncMock (return_value = {
713+ "stdout" : "file1.txt\n file2.txt\n " ,
714+ "stderr" : "" ,
715+ "exit_code" : 0
716+ })
717+
718+ agent = LocalManagedAgent (compute = mock_compute )
719+ agent ._compute_instance_id = "test_instance"
720+
721+ list_files = agent ._create_compute_list_files ()
722+ result = list_files ("/some/dir" )
723+
724+ assert "file1.txt" in result
725+ assert "file2.txt" in result
726+ mock_compute .execute .assert_called_once_with ("test_instance" , "ls -la /some/dir" , timeout = 60 )
727+
728+ def test_compute_tools_require_provisioned_instance (self ):
729+ """Test that compute tools raise error when no instance is provisioned."""
730+ import pytest
731+ from praisonai .integrations .managed_local import LocalManagedAgent
732+
733+ mock_compute = MagicMock ()
734+ agent = LocalManagedAgent (compute = mock_compute )
735+ # Don't set _compute_instance_id
736+
737+ execute_command = agent ._create_compute_execute_command ()
738+ with pytest .raises (RuntimeError , match = "No compute provider provisioned" ):
739+ execute_command ("echo test" )
740+
741+ def test_auto_provision_compute_in_ensure_agent (self ):
742+ """Test that _ensure_agent auto-provisions compute when needed."""
743+ import asyncio
744+ from unittest .mock import patch , MagicMock , AsyncMock
745+ from praisonai .integrations .managed_local import LocalManagedAgent , LocalManagedConfig
746+
747+ mock_compute = MagicMock ()
748+ mock_info = MagicMock ()
749+ mock_info .instance_id = "auto_provisioned_instance"
750+
751+ cfg = LocalManagedConfig ()
752+ agent = LocalManagedAgent (config = cfg , compute = mock_compute )
753+
754+ with patch .object (agent , 'provision_compute' , new_callable = AsyncMock ) as mock_provision :
755+ mock_provision .return_value = mock_info
756+ with patch ('praisonaiagents.Agent' ) as mock_agent_class :
757+ mock_agent_class .return_value = MagicMock ()
758+
759+ inner_agent = agent ._ensure_agent ()
760+
761+ # Should have auto-provisioned compute
762+ mock_provision .assert_called_once ()
763+ assert agent ._compute_instance_id == "auto_provisioned_instance"
0 commit comments