test_computer_policy.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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. def test_workspace_memory_uses_agent_controls_without_mutating_another_agent(self):
  91. stopped = []
  92. other = SimpleNamespace(_memory_enabled=True)
  93. self.agent._memory_store = object()
  94. self.agent._memory_manager = SimpleNamespace(get_all_tool_names=lambda: {'provider_remember'}, shutdown_all=lambda: stopped.append(True))
  95. self.agent._memory_enabled = self.agent._user_profile_enabled = True
  96. self.agent.skip_background_review = False
  97. self.agent.disabled_toolsets = ['terminal']
  98. invalidated = []
  99. self.agent._invalidate_system_prompt = lambda: invalidated.append(True)
  100. self.binding.workspace_memory = True
  101. self.session['_yaoyao_workspace_memory'] = True
  102. bridge._isolate_profile_memory(self.agent, self.binding)
  103. self.assertIsNone(self.agent._memory_store)
  104. self.assertIsNone(self.agent._memory_manager)
  105. self.assertFalse(self.agent._memory_enabled)
  106. self.assertFalse(self.agent._user_profile_enabled)
  107. self.assertTrue(self.agent.skip_background_review)
  108. self.assertEqual(self.agent.disabled_toolsets, ['terminal', 'memory'])
  109. self.assertEqual(stopped, [True]); self.assertEqual(invalidated, [True])
  110. self.assertTrue(other._memory_enabled)
  111. for tool in ('memory', 'provider_remember'):
  112. self.assertEqual(bridge.computer_directive(session_id='stored', tool_name=tool, args={})['action'], 'block')
  113. self.assertIsNone(bridge.computer_directive(session_id='stored', tool_name='skill_view', args={}))
  114. def context_agent(self, session_id='stored'):
  115. agent = SimpleNamespace(session_id=session_id, enabled_toolsets=['skills'], disabled_toolsets=[],
  116. skip_context_files=False, load_soul_identity=True,
  117. _cached_system_prompt='previous-profile-context')
  118. agent._invalidate_system_prompt = lambda: setattr(agent, '_cached_system_prompt', None)
  119. return agent
  120. def test_isolated_bind_and_resumed_agent_clear_context_cache_without_changing_history(self):
  121. self.session.update(running=False, history=[{'role': 'user', 'content': 'keep-history'}])
  122. original = self.context_agent('ordinary-session')
  123. body = {'session_id': 'runtime', 'stored_session_id': 'stored', 'profile': 'default',
  124. 'bridge_url': self.binding.bridge_url, 'token': self.binding.token,
  125. 'expires_at': time.time() * 1000 + 60000, 'computer_policy': self.binding.computer_policy}
  126. with patch.object(bridge, '_agent_has_tools', return_value=True):
  127. for generation in ['first-turn', 'resumed-turn']:
  128. agent = self.context_agent()
  129. self.session['agent'] = agent
  130. bridge.bind(self.binding.owner, {**body, 'generation': generation})
  131. self.assertTrue(agent.skip_context_files)
  132. self.assertFalse(agent.load_soul_identity)
  133. self.assertIsNone(agent._cached_system_prompt)
  134. self.assertEqual(agent.enabled_toolsets, ['skills'])
  135. self.assertEqual(self.session['history'], [{'role': 'user', 'content': 'keep-history'}])
  136. bridge.unbind(self.binding.owner, {'session_id': 'runtime', 'generation': generation})
  137. self.assertTrue(agent.skip_context_files)
  138. self.assertFalse(original.skip_context_files)
  139. self.assertTrue(original.load_soul_identity)
  140. def test_isolated_child_inherits_context_policy_and_keeps_skill_tools(self):
  141. child = self.context_agent('child-session')
  142. self.agent._active_children = [child]
  143. tools = [{'function': {'name': name}} for name in ['skills_list', 'skill_view', 'skill_manage']]
  144. model_tools = SimpleNamespace(get_tool_definitions=lambda **kw: tools, get_toolset_for_tool=lambda name: 'skills')
  145. with patch.dict(sys.modules, {'model_tools': model_tools}):
  146. bridge.inherit_child(parent_session_id='stored', child_session_id='child-session')
  147. self.assertTrue(child.skip_context_files)
  148. self.assertFalse(child.load_soul_identity)
  149. self.assertIsNone(child._cached_system_prompt)
  150. self.assertEqual(child.valid_tool_names, {'skills_list', 'skill_view', 'skill_manage'})
  151. def test_context_isolation_preserves_profile_mode_and_requires_supported_agent(self):
  152. for policy in [None, {'mode': 'profile', 'hostAccess': True}]:
  153. agent = self.context_agent()
  154. self.binding.computer_policy = policy
  155. bridge._isolate_profile_context(agent, self.binding)
  156. self.assertFalse(agent.skip_context_files)
  157. self.assertTrue(agent.load_soul_identity)
  158. self.assertEqual(agent._cached_system_prompt, 'previous-profile-context')
  159. self.binding.computer_policy = {'mode': 'isolated', 'hostAccess': False}
  160. with self.assertRaises(bridge.BridgeError) as error:
  161. bridge._isolate_profile_context(SimpleNamespace(), self.binding)
  162. self.assertEqual(error.exception.code, 'context_isolation_unavailable')
  163. if __name__ == '__main__':
  164. unittest.main()