enterprise-workshop.test.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. """Observable enterprise boundaries, installation idempotency and credential preservation."""
  2. import importlib.util
  3. import json
  4. import os
  5. from pathlib import Path
  6. import shutil
  7. import subprocess
  8. import sys
  9. import tempfile
  10. import unittest
  11. sys.dont_write_bytecode = True
  12. ROOT = Path(__file__).resolve().parents[1]
  13. sys.path.insert(0, str(ROOT / 'deploy/enterprise-workshop'))
  14. import bootstrap as core
  15. import gogs_setup
  16. import host_bootstrap
  17. import readiness
  18. import registry
  19. spec = importlib.util.spec_from_file_location('enterprise_packager', ROOT / 'scripts/package-enterprise-workshop.py')
  20. packager = importlib.util.module_from_spec(spec)
  21. spec.loader.exec_module(packager)
  22. class EnterpriseTests(unittest.TestCase):
  23. @classmethod
  24. def setUpClass(cls):
  25. cls.payload = packager.content()
  26. def setUp(self):
  27. self.temp = tempfile.TemporaryDirectory(prefix='ksf-enterprise-test-')
  28. self.addCleanup(self.temp.cleanup)
  29. root = Path(self.temp.name)
  30. self.package, self.data = root / 'package', root / 'data'
  31. for name, raw in self.payload.items():
  32. p = self.package / name
  33. p.parent.mkdir(parents=True, exist_ok=True)
  34. p.write_bytes(raw)
  35. self.current = self.data / '.fmode/config/enterprise/current.json'
  36. self.cfg = json.loads((self.package / 'enterprise.json').read_bytes())
  37. self.save_current()
  38. def save_current(self, **changes):
  39. value = {k: self.cfg[k] for k in ('companyObjectId', 'appId', 'serverURL', 'functionsURL')}
  40. value.update(provisionStatus='success', masterKey='PRIVATE-MASTER-DO-NOT-COPY')
  41. value.update(changes)
  42. self.current.parent.mkdir(parents=True, exist_ok=True)
  43. self.current.write_bytes(core.encode(value))
  44. self.current.chmod(0o600)
  45. def install(self):
  46. p = core.plan(self.package, self.data)
  47. return core.apply(self.package, self.data, p)
  48. def test_install_and_same_plan_replay_are_idempotent(self):
  49. unrelated = self.data / 'skills/existing-skill/SKILL.md'
  50. core.atomic_create(unrelated, b'EXISTING SKILL')
  51. p = core.plan(self.package, self.data)
  52. self.assertTrue(p['changes'])
  53. first = core.apply(self.package, self.data, p)
  54. second = core.apply(self.package, self.data, p)
  55. self.assertTrue(first['ok'])
  56. self.assertFalse(second['applied'])
  57. self.assertEqual(second['installedFiles'], 0)
  58. self.assertEqual(unrelated.read_bytes(), b'EXISTING SKILL')
  59. self.assertFalse((self.data / 'skills/ksf-voc-library/SKILL.md').exists())
  60. self.assertFalse(first['agentLoaded'])
  61. self.assertFalse(first['businessAuthorized'])
  62. self.assertFalse(first['automaticLifecycleHookVerified'])
  63. self.assertNotIn(b'PRIVATE-MASTER', b''.join(p.read_bytes() for p in
  64. (self.data / 'ksf-enterprise-workshop').rglob('*') if p.is_file()))
  65. def test_existing_private_files_survive_verify(self):
  66. self.install()
  67. key = self.data / 'skills/ksf-workshop-runtime/credential-binding.json'
  68. core.atomic_create(key, b'PRIVATE-HOST-BINDING')
  69. before = key.read_bytes(), key.stat().st_mtime_ns
  70. result = core.plan(self.package, self.data)
  71. self.assertEqual(result['changes'], [])
  72. core.verify(self.package, self.data)
  73. self.assertEqual((key.read_bytes(), key.stat().st_mtime_ns), before)
  74. def test_wrong_tenant_and_underscore_mismatch_stop_before_writes(self):
  75. for fields in ({'companyObjectId': 'OTHER'}, {'appId': 'FKieRbIoGtmain'}):
  76. self.save_current(**fields)
  77. with self.assertRaisesRegex(core.Stopped, 'ENTERPRISE_MISMATCH'):
  78. core.plan(self.package, self.data)
  79. self.assertFalse((self.data / 'skills').exists())
  80. def test_wrong_endpoint_and_unprovisioned_stop(self):
  81. for fields in ({'functionsURL': 'https://other.fmode.cn/api/functions'}, {'provisionStatus': 'pending'}):
  82. self.save_current(**fields)
  83. with self.assertRaises(core.Stopped):
  84. core.plan(self.package, self.data)
  85. self.assertFalse((self.data / 'skills').exists())
  86. def test_payload_tampering_and_unlisted_files_stop(self):
  87. p = self.package / 'payload/skills/ksf-workshop-runtime/SKILL.md'
  88. before = p.read_bytes()
  89. p.write_bytes(b'UNTRUSTED')
  90. with self.assertRaisesRegex(core.Stopped, 'PACKAGE_HASH_MISMATCH'):
  91. core.plan(self.package, self.data)
  92. p.write_bytes(before)
  93. (self.package / 'unlisted.py').write_bytes(b'UNLISTED')
  94. with self.assertRaisesRegex(core.Stopped, 'UNDECLARED_PACKAGE_FILE'):
  95. core.plan(self.package, self.data)
  96. def test_unknown_existing_component_not_overwritten(self):
  97. p = self.data / 'skills/ksf-workshop-runtime/SKILL.md'
  98. core.atomic_create(p, b'LOCAL-UNKNOWN-VERSION')
  99. with self.assertRaisesRegex(core.Stopped, 'EXISTING_COMPONENT'):
  100. core.plan(self.package, self.data)
  101. self.assertEqual(p.read_bytes(), b'LOCAL-UNKNOWN-VERSION')
  102. def test_package_staged_inside_data_dir_can_install(self):
  103. staged = self.data / 'ksf-enterprise-packages/package-r1'
  104. shutil.copytree(self.package, staged)
  105. p = core.plan(staged, self.data)
  106. self.assertTrue(core.apply(staged, self.data, p)['ok'])
  107. def test_changed_plan_is_rejected_without_install(self):
  108. p = core.plan(self.package, self.data)
  109. core.atomic_create(self.data / 'skills/ksf-workshop-runtime/other-local-file', b'CONCURRENT')
  110. with self.assertRaises(core.Stopped):
  111. core.apply(self.package, self.data, p)
  112. self.assertFalse((self.data / 'skills/ksf-voc-library').exists())
  113. def test_research_skill_second_entry_is_rejected(self):
  114. core.atomic_create(self.data / 'skills/ksf-voc-library/SKILL.md', b'OLD ENTRY')
  115. with self.assertRaisesRegex(core.Stopped, 'RESEARCH_SKILL_ENTRYPOINT_STILL_ACTIVE'):
  116. core.plan(self.package, self.data)
  117. def test_environment_activation_preserves_secret_bytes_and_backup(self):
  118. env = self.data / '.env'
  119. original = b'OTHER=keep\r\nPRIVATE_TOKEN="do-not-copy-to-repository"'
  120. core.atomic_create(env, original)
  121. self.assertTrue(readiness.activate_research(self.data))
  122. expected = original + b'\r\nKSF_LIBRARY_CHANNEL=' + str(
  123. self.data / 'ksf-library-channel/channel.json').encode() + b'\r\n'
  124. self.assertEqual(env.read_bytes(), expected)
  125. self.assertFalse(readiness.activate_research(self.data))
  126. backup = list((self.data / 'ksf-enterprise-workshop').glob('env-before-*'))
  127. self.assertEqual(len(backup), 1)
  128. self.assertEqual(backup[0].read_bytes(), original)
  129. def test_environment_binding_conflict_does_not_change_file(self):
  130. env = self.data / '.env'
  131. for raw in (b'KSF_LIBRARY_CHANNEL=/other/private.json\n',
  132. b'KSF_LIBRARY_CHANNEL=x\nKSF_LIBRARY_CHANNEL=x\n'):
  133. env.write_bytes(raw)
  134. with self.assertRaisesRegex(core.Stopped, 'EXISTING_RESEARCH_BINDING_DIFFERS'):
  135. readiness.activate_research(self.data)
  136. self.assertEqual(env.read_bytes(), raw)
  137. def test_gogs_uses_designated_administrator_before_mutation(self):
  138. class WrongUser:
  139. calls = []
  140. def call(inner, method, route, body=None, missing=False):
  141. inner.calls.append((method, route))
  142. return {'username': 'someone-else'}
  143. api = WrongUser()
  144. with self.assertRaisesRegex(core.Stopped, 'GOGS_ADMINISTRATOR_MISMATCH'):
  145. gogs_setup.setup(api, self.package, True)
  146. self.assertEqual(api.calls, [('GET', '/api/v1/user')])
  147. def test_existing_organization_owner_mismatch_stops(self):
  148. class WrongOwner:
  149. calls = []
  150. def call(inner, method, route, body=None, missing=False):
  151. inner.calls.append(method)
  152. if route.endswith('/user'):
  153. return {'username': '18307996893'}
  154. if route.endswith('/teams'):
  155. return [{'id': 1, 'name': 'Owners'}]
  156. if route.endswith('/members'):
  157. return [{'username': 'someone-else'}]
  158. return {'id': 5}
  159. api = WrongOwner()
  160. with self.assertRaisesRegex(core.Stopped, 'EXISTING_ORGANIZATION_OWNER_MISMATCH'):
  161. gogs_setup.setup(api, self.package, True)
  162. self.assertEqual(set(api.calls), {'GET'})
  163. def fake_host(self, current, commit='a' * 40):
  164. calls = []
  165. def runner(args, **kwargs):
  166. calls.append(args)
  167. if 'rev-parse' in args:
  168. output = commit.encode()
  169. elif 'status' in args:
  170. output = b''
  171. elif 'id' in args:
  172. output = b'10000\n'
  173. elif 'cat' in args:
  174. output = core.encode(current)
  175. else:
  176. raise AssertionError('Unexpected mutating command: ' + repr(args))
  177. return subprocess.CompletedProcess(args, 0, output, b'')
  178. return calls, runner
  179. def test_host_checks_enterprise_before_docker_copy(self):
  180. current = json.loads(self.current.read_bytes())
  181. current['companyObjectId'] = 'OTHER'
  182. calls, runner = self.fake_host(current)
  183. with self.assertRaisesRegex(core.Stopped, 'ENTERPRISE_MISMATCH'):
  184. host_bootstrap.initialize(self.package, 'a' * 40, 'agent-node-kangshifu', True, runner)
  185. self.assertFalse(any('cp' in x for x in calls))
  186. def test_host_does_not_install_changed_checkout(self):
  187. calls, runner = self.fake_host(json.loads(self.current.read_bytes()), 'b' * 40)
  188. with self.assertRaisesRegex(core.Stopped, 'CONFIGURATION_COMMIT_MISMATCH'):
  189. host_bootstrap.initialize(self.package, 'a' * 40, 'agent-node-kangshifu', True, runner)
  190. self.assertFalse(any(x[0] == 'docker' for x in calls))
  191. def test_unmapped_enterprises_are_not_changed(self):
  192. file = self.data / 'registry.json'
  193. core.atomic_create(file, core.encode({'schema': 'ksf.enterprise-registry.v1', 'companies': {}}))
  194. calls, runner = self.fake_host({'companyObjectId': 'OTHER'})
  195. result = registry.after_twin(file, 'other-agent', True, runner)
  196. self.assertTrue(result['skipped'])
  197. self.assertEqual(len(calls), 1)
  198. def test_registry_binding_preserves_other_enterprises_and_is_idempotent(self):
  199. file = self.data / 'enterprise-registry.json'
  200. other = {'repositoryURL': 'https://git.fmode.cn/other/config.git', 'commit': 'b' * 40}
  201. original = core.encode({'schema': 'ksf.enterprise-registry.v1', 'companies': {'OTHER': other}})
  202. core.atomic_create(file, original)
  203. def runner(args, **kwargs):
  204. if 'rev-parse' in args:
  205. raw = b'a' * 40
  206. elif 'remote' in args:
  207. raw = b'https://git.fmode.cn/kangshifu/workshop-config.git'
  208. elif 'status' in args:
  209. raw = b''
  210. else:
  211. raise AssertionError(args)
  212. return subprocess.CompletedProcess(args, 0, raw, b'')
  213. self.assertTrue(registry.register(file, self.package, 'a' * 40, True, runner)['mappingChanged'])
  214. value = json.loads(file.read_bytes())
  215. self.assertEqual(value['companies']['OTHER'], other)
  216. self.assertEqual(value['companies']['FKieRbIoGt']['commit'], 'a' * 40)
  217. self.assertFalse(registry.register(file, self.package, 'a' * 40, True, runner)['mappingChanged'])
  218. backups = list(self.data.glob('enterprise-registry.json.before-*'))
  219. self.assertEqual(len(backups), 1)
  220. self.assertEqual(backups[0].read_bytes(), original)
  221. def test_registry_rejects_other_repository_without_writing(self):
  222. file = self.data / 'enterprise-registry.json'
  223. def runner(args, **kwargs):
  224. raw = b'a' * 40 if 'rev-parse' in args else b'https://git.fmode.cn/unrelated/config.git'
  225. return subprocess.CompletedProcess(args, 0, raw, b'')
  226. with self.assertRaisesRegex(core.Stopped, 'CONFIGURATION_REPOSITORY_MISMATCH'):
  227. registry.register(file, self.package, 'a' * 40, True, runner)
  228. self.assertFalse(file.exists())
  229. def test_research_runtime_missing_channel_fails_without_snapshot_fallback(self):
  230. script = self.package / 'payload/skills/ksf-voc-library/scripts/query_voc.py'
  231. env = {**os.environ, 'KSF_LIBRARY_CHANNEL': str(self.data / 'missing-channel.json')}
  232. result = subprocess.run([sys.executable, '-B', str(script), 'contract'], env=env,
  233. capture_output=True, timeout=20)
  234. self.assertEqual(result.returncode, 1)
  235. value = json.loads(result.stderr)
  236. self.assertFalse(value['ok'])
  237. self.assertEqual(value['error']['code'], 'CHANNEL_FILE_NOT_FOUND_OR_REDIRECTED')
  238. if __name__ == '__main__':
  239. unittest.main(verbosity=2)