test_computer_policy.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import importlib.util
  2. import json
  3. import sys
  4. import tempfile
  5. import threading
  6. import time
  7. import unittest
  8. from pathlib import Path
  9. from types import SimpleNamespace
  10. from unittest.mock import patch
  11. spec = importlib.util.spec_from_file_location('test_yaoyao_bridge', Path(__file__).parents[1] / 'bridge_runtime.py')
  12. bridge = importlib.util.module_from_spec(spec)
  13. sys.modules[spec.name] = bridge
  14. spec.loader.exec_module(bridge)
  15. class ComputerPolicyTests(unittest.TestCase):
  16. def setUp(self):
  17. self.temp = tempfile.TemporaryDirectory()
  18. self.home = Path(self.temp.name)
  19. self.agent = SimpleNamespace(session_id='stored', enabled_toolsets=['all'])
  20. self.session = {'agent': self.agent, 'session_key': 'stored', 'running': True, 'cwd': str(self.home),
  21. '_yaoyao_computer_policy': {'mode': 'isolated', 'hostAccess': False}}
  22. self.server = SimpleNamespace(_sessions={'runtime': self.session}, _sessions_lock=threading.RLock())
  23. self.binding = bridge.Binding('runtime', 'stored', 'default', self.home, 'generation',
  24. 'http://127.0.0.1:12345', 'a' * 40, time.time() * 1000 + 60000,
  25. ('user', 'password'), self.session, self.agent,
  26. computer_policy=dict(self.session['_yaoyao_computer_policy']))
  27. bridge._bindings.clear(); bridge._generations.clear(); bridge._children.clear()
  28. bridge._bindings['runtime'] = self.binding
  29. self.patches = [patch.dict(sys.modules, {'agent.redact': SimpleNamespace(redact_sensitive_text=lambda text, **kw: text)}),
  30. patch.object(bridge, '_server', return_value=self.server),
  31. patch.object(bridge, '_check_session_mode'),
  32. patch.object(bridge, '_transport_identity', return_value=self.binding.owner),
  33. patch.object(bridge, '_profile_home', return_value=self.home)]
  34. for p in self.patches: p.start()
  35. def tearDown(self):
  36. for p in reversed(self.patches): p.stop()
  37. self.temp.cleanup()
  38. def test_isolated_skills_and_vm_tools_use_native_catalog_but_host_operations_require_approval(self):
  39. for name in ['skills_list', 'skill_view', 'vision_analyze', 'yaoyao_computer_shell_aabbcc']:
  40. self.assertIsNone(bridge.computer_directive(session_id='stored', tool_name=name, args={}))
  41. for name in ['terminal', 'process_manage', 'read_file', 'write_file', 'execute_code', 'delegate_task', 'session_search', 'browser_navigate', 'yaoyao_computer_copy_file_aabbcc']:
  42. result = bridge.computer_directive(session_id='stored', tool_name=name, args={'path': '/authorized.txt'})
  43. self.assertEqual(result['action'], 'approve')
  44. self.assertIn('/authorized.txt', result['message'])
  45. self.binding.computer_policy['hostAccess'] = True
  46. self.assertIsNone(bridge.computer_directive(session_id='stored', tool_name='terminal', args={}))
  47. self.binding.computer_policy.update(mode='profile', hostAccess=False)
  48. self.assertIsNone(bridge.computer_directive(session_id='stored', tool_name='terminal', args={}))
  49. def test_stopped_expired_unbound_and_replaced_sessions_cannot_call_native_tools(self):
  50. for change in ['stop', 'expire', 'unbind', 'replace']:
  51. with self.subTest(change=change):
  52. self.session.update(running=True, agent=self.agent)
  53. self.binding.expires_at = time.time() * 1000 + 60000
  54. bridge._bindings['runtime'] = self.binding
  55. if change == 'stop': self.session['running'] = False
  56. if change == 'expire': self.binding.expires_at = 0
  57. if change == 'unbind': bridge._bindings.clear()
  58. if change == 'replace': self.session['agent'] = SimpleNamespace(session_id='stored')
  59. self.assertEqual(bridge.computer_directive(session_id='runtime', tool_name='skill_view', args={})['action'], 'block')
  60. def test_ordinary_profile_sessions_are_unaffected(self):
  61. self.session.pop('_yaoyao_computer_policy')
  62. self.assertIsNone(bridge.computer_directive(session_id='stored', tool_name='terminal', args={}))
  63. def test_policy_cannot_be_changed_during_renewal(self):
  64. self.session['running'] = False
  65. body = {'session_id': 'runtime', 'stored_session_id': 'stored', 'profile': 'default',
  66. 'generation': 'generation', 'bridge_url': self.binding.bridge_url, 'token': self.binding.token,
  67. 'expires_at': time.time() * 1000 + 60000, 'computer_policy': {'mode': 'profile', 'hostAccess': True}}
  68. with patch.object(bridge, '_agent_has_tools', return_value=True):
  69. with self.assertRaises(bridge.BridgeError) as error:
  70. bridge.bind(self.binding.owner, body)
  71. self.assertEqual(error.exception.code, 'generation_conflict')
  72. def test_transfer_is_owned_by_hermes_and_checks_generation_owner_and_protected_paths(self):
  73. constants = SimpleNamespace(set_hermes_home_override=lambda p: None, reset_hermes_home_override=lambda p: None)
  74. file_tools = SimpleNamespace(_check_sensitive_path=lambda p, t: 'denied' if p.endswith('protected') else None,
  75. _check_protected_instruction_write=lambda p, t: None,
  76. _check_approval_required_write=lambda p, t: None)
  77. safety = SimpleNamespace(get_read_block_error=lambda p: 'denied' if p.endswith('protected') else None)
  78. modules = {'hermes_constants': constants, 'tools.file_tools': file_tools, 'agent.file_safety': safety}
  79. (self.home / 'authorized').write_bytes(b'authorized-input')
  80. body = {'session_id': 'runtime', 'generation': 'generation', 'action': 'read', 'path': 'authorized'}
  81. with patch.dict(sys.modules, modules):
  82. self.assertEqual(bridge.computer_file(self.binding.owner, body)['data'], 'YXV0aG9yaXplZC1pbnB1dA==')
  83. for owner, changed in [(('other', 'password'), body), (self.binding.owner, {**body, 'generation': 'stale'}),
  84. (self.binding.owner, {**body, 'path': 'protected'})]:
  85. with self.assertRaises(bridge.BridgeError): bridge.computer_file(owner, changed)
  86. bridge.computer_file(self.binding.owner, {**body, 'action': 'write', 'path': 'result', 'data': 'cmVzdWx0'})
  87. self.assertEqual((self.home / 'result').read_bytes(), b'result')
  88. self.session['running'] = False
  89. with self.assertRaises(bridge.BridgeError): bridge.computer_file(self.binding.owner, body)
  90. if __name__ == '__main__':
  91. unittest.main()