create-workframe 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/LICENSE +201 -201
  2. package/NOTICE +12 -12
  3. package/README.md +8 -92
  4. package/SECURITY.md +40 -40
  5. package/bin/workframe.js +329 -329
  6. package/docs/workspace-instructions/WORKFRAME_ONBOARDING.md +1 -1
  7. package/docs/workspace-instructions/WORKFRAME_ROUTING.md +8 -8
  8. package/package.json +3 -6
  9. package/profiles/architect/AGENTS.md +29 -29
  10. package/profiles/architect/SOUL.md +2 -2
  11. package/profiles/architect/skills/devops/kanban-worker/SKILL.md +27 -27
  12. package/profiles/designer/AGENTS.md +26 -26
  13. package/profiles/designer/skills/devops/kanban-worker/SKILL.md +27 -27
  14. package/profiles/dev/AGENTS.md +28 -28
  15. package/profiles/dev/skills/devops/kanban-worker/SKILL.md +27 -27
  16. package/profiles/docs/AGENTS.md +27 -27
  17. package/profiles/docs/skills/devops/kanban-worker/SKILL.md +27 -27
  18. package/profiles/research/AGENTS.md +26 -26
  19. package/profiles/research/skills/devops/kanban-worker/SKILL.md +27 -27
  20. package/profiles/visionary/AGENTS.md +25 -25
  21. package/profiles/visionary/skills/devops/kanban-worker/SKILL.md +27 -27
  22. package/profiles/workframe-agent/AGENTS.md +37 -37
  23. package/profiles/workframe-agent/skills/devops/botfather/SKILL.md +85 -85
  24. package/profiles/workframe-agent/skills/devops/kanban-handoff-pattern/SKILL.md +58 -58
  25. package/profiles/workframe-agent/skills/devops/workframe-cohort/SKILL.md +54 -54
  26. package/rules/workspace-README.md +5 -5
  27. package/scripts/apply-update-hermes.sh +17 -17
  28. package/scripts/apply-update-workframe.sh +77 -77
  29. package/scripts/bootstrap-workspace-link.sh +8 -8
  30. package/scripts/bundle-workframe-ui.mjs +3 -3
  31. package/scripts/compose-docker-host.sh +37 -37
  32. package/scripts/ensure-compose-host-paths.mjs +51 -51
  33. package/scripts/fix-zk-encryption-key.sh +35 -35
  34. package/scripts/lib/install-identity.mjs +212 -212
  35. package/scripts/restart-gateway-hermes.sh +12 -12
  36. package/scripts/set-compose-public-url.mjs +92 -92
  37. package/scripts/setup-stack-secrets.sh +50 -50
  38. package/scripts/sync-canonical-to-package.mjs +8 -7
  39. package/scripts/verify-public-deploy.sh +105 -105
  40. package/shared/WORKFRAME_AGENT_LIBRARY.md +17 -17
  41. package/shared/WORKFRAME_AGENT_OPERATIONS.md +15 -15
  42. package/shared/WORKFRAME_AGENT_PACKS.json +18 -18
  43. package/shared/WORKFRAME_AGENT_PACKS.yaml +8 -8
  44. package/shared/WORKFRAME_SKILL_CURATION.md +4 -4
  45. package/workframe-api/README.md +28 -28
  46. package/workframe-api/action_proxy.py +131 -131
  47. package/workframe-api/auth_rate_limit.py +49 -49
  48. package/workframe-api/credential_vault.py +445 -445
  49. package/workframe-api/data/avatar-catalog.json +41 -41
  50. package/workframe-api/email_sender.py +220 -220
  51. package/workframe-api/google_auth.py +90 -90
  52. package/workframe-api/install_api.py +359 -359
  53. package/workframe-api/internal_proxy_auth.py +150 -150
  54. package/workframe-api/llm_proxy.py +277 -277
  55. package/workframe-api/oidc_jwt.py +108 -108
  56. package/workframe-api/package.json +12 -13
  57. package/workframe-api/public/assets/index-DPXu_lGn.css +1 -1
  58. package/workframe-api/public/assets/index-DYnLrCZZ.js +8 -8
  59. package/workframe-api/requirements.txt +2 -2
  60. package/workframe-api/site_meta.py +271 -271
  61. package/workframe-api/stack_config.py +427 -427
  62. package/workframe-api/time-bind-chat.py +99 -99
  63. package/workframe-api/turn_credentials.py +226 -226
  64. package/workframe-api/updates.py +417 -417
  65. package/workframe-api/vault_kek.py +159 -159
  66. package/workframe-api/zk_auth.py +633 -633
  67. package/workframe-supervisor/Dockerfile +11 -11
  68. package/workframe-supervisor/server.py +787 -787
  69. package/workframe-ui/docker/nginx.conf +85 -85
  70. package/workframe-ui/public/assets/{arc-CBDYvkAF.js → arc-COAT3laO.js} +1 -1
  71. package/workframe-ui/public/assets/architecture-7EHR7CIX-DUyH3hWG.js +1 -0
  72. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-XnBRKeW0.js → architectureDiagram-3BPJPVTR-BFjWV24l.js} +1 -1
  73. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-VYHUfVhd.js → blockDiagram-GPEHLZMM-DSQLPfrj.js} +1 -1
  74. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BTjUcJpm.js → c4Diagram-AAUBKEIU-DKEHv1t2.js} +1 -1
  75. package/workframe-ui/public/assets/channel-g7r_RGaY.js +1 -0
  76. package/workframe-ui/public/assets/{chunk-2J33WTMH-w7uu7R-b.js → chunk-2J33WTMH-DHZg-DUi.js} +1 -1
  77. package/workframe-ui/public/assets/{chunk-3OPIFGDE-Cb9LtnDX.js → chunk-3OPIFGDE-BB-OYTfp.js} +1 -1
  78. package/workframe-ui/public/assets/{chunk-4BX2VUAB-DiQ-qCwH.js → chunk-4BX2VUAB-C93q0YIm.js} +1 -1
  79. package/workframe-ui/public/assets/{chunk-55IACEB6-C-mLFr7z.js → chunk-55IACEB6-MAYniqik.js} +1 -1
  80. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-DOesfiCI.js → chunk-5ZQYHXKU-ChgN6YJs.js} +1 -1
  81. package/workframe-ui/public/assets/{chunk-727SXJPM-BJ3oBZuz.js → chunk-727SXJPM-B_FYwdAv.js} +1 -1
  82. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-CCA6xpGs.js → chunk-AQP2D5EJ-1_Hw_h1A.js} +1 -1
  83. package/workframe-ui/public/assets/{chunk-BSJP7CBP-a0cMNFb2.js → chunk-BSJP7CBP-CFiDQ1Rv.js} +1 -1
  84. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-kuqN8EIY.js → chunk-CSCIHK7Q-DZ9UMTlB.js} +1 -1
  85. package/workframe-ui/public/assets/{chunk-FMBD7UC4-DyPgYHCg.js → chunk-FMBD7UC4-DlMlyFgw.js} +1 -1
  86. package/workframe-ui/public/assets/{chunk-KSCS5N6A-CdUuvR0V.js → chunk-KSCS5N6A-DHXtQ_Hf.js} +1 -1
  87. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-Dq9NoWmK.js → chunk-L5ZTLDWV-CuQzg-QG.js} +1 -1
  88. package/workframe-ui/public/assets/{chunk-LZXEDZCA-p74rddlO.js → chunk-LZXEDZCA-BHzjzCGg.js} +2 -2
  89. package/workframe-ui/public/assets/{chunk-ND2GUHAM-DBD2u1Gz.js → chunk-ND2GUHAM-DHXx05n2.js} +1 -1
  90. package/workframe-ui/public/assets/{chunk-NZK2D7GU-BeIeYFnd.js → chunk-NZK2D7GU-CV5pmDM_.js} +1 -1
  91. package/workframe-ui/public/assets/{chunk-O5CBEL6O-ClHc56ib.js → chunk-O5CBEL6O-6tkCHxsV.js} +1 -1
  92. package/workframe-ui/public/assets/chunk-QZHKN3VN-C5UQehWY.js +1 -0
  93. package/workframe-ui/public/assets/chunk-WU5MYG2G-DhWllrI8.js +1 -0
  94. package/workframe-ui/public/assets/{chunk-XPW4576I-EFr8R_1p.js → chunk-XPW4576I-BClwIiCp.js} +1 -1
  95. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BBM_8T8E.js +1 -0
  96. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BBM_8T8E.js +1 -0
  97. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-C7aPBODd.js → cose-bilkent-S5V4N54A-DOrGV6DQ.js} +1 -1
  98. package/workframe-ui/public/assets/{dagre-BM42HDAG-BdU1Rv-H.js → dagre-BM42HDAG-DXTPvJkX.js} +1 -1
  99. package/workframe-ui/public/assets/{diagram-2AECGRRQ-DWowSo85.js → diagram-2AECGRRQ-xX_v-pbf.js} +1 -1
  100. package/workframe-ui/public/assets/{diagram-5GNKFQAL-MnxBbceO.js → diagram-5GNKFQAL-Cd2pXbBe.js} +1 -1
  101. package/workframe-ui/public/assets/{diagram-KO2AKTUF-DQaLRXFf.js → diagram-KO2AKTUF-Df3XvUtk.js} +1 -1
  102. package/workframe-ui/public/assets/{diagram-LMA3HP47-CQaBud9k.js → diagram-LMA3HP47-CsijIPaD.js} +1 -1
  103. package/workframe-ui/public/assets/{diagram-OG6HWLK6-D8bAXbY9.js → diagram-OG6HWLK6-aq5fmfHd.js} +1 -1
  104. package/workframe-ui/public/assets/{dist-DGpTLHr_.js → dist-D1c0mkbB.js} +1 -1
  105. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-1E-xSvBK.js → erDiagram-TEJ5UH35-DnFysVRY.js} +1 -1
  106. package/workframe-ui/public/assets/eventmodeling-FCH6USID-Ci8mdb44.js +1 -0
  107. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-CgOVD5hu.js → flowDiagram-I6XJVG4X-C6Ebi3su.js} +1 -1
  108. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-JFYAIauo.js → ganttDiagram-6RSMTGT7-BQXQtUpa.js} +1 -1
  109. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-B9REenIl.js → gitGraph-WXDBUCRP-Dt0zIs_M.js} +1 -1
  110. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-BQ7NcMSn.js → gitGraphDiagram-PVQCEYII-BF8gHzRn.js} +1 -1
  111. package/workframe-ui/public/assets/index-DpoUZAxh.css +1 -0
  112. package/workframe-ui/public/assets/{index-Dnw6vjqb.js → index-lRpzpNPT.js} +2 -2
  113. package/workframe-ui/public/assets/{info-J43DQDTF-CL6-eTjH.js → info-J43DQDTF-CSmszQJT.js} +1 -1
  114. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-LJTODW4W.js → infoDiagram-5YYISTIA-CVTKGW6p.js} +1 -1
  115. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-bchrQVuo.js → ishikawaDiagram-YF4QCWOH-Z8pT09Lv.js} +1 -1
  116. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-DkrvYuxP.js → journeyDiagram-JHISSGLW-r3wD68_T.js} +1 -1
  117. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-DFRbj0IG.js → kanban-definition-UN3LZRKU-Il8VglqN.js} +1 -1
  118. package/workframe-ui/public/assets/{line-Vd48P7-O.js → line-oyjpfz2A.js} +1 -1
  119. package/workframe-ui/public/assets/{linear-Ckizh2G7.js → linear-Cf7p5tVp.js} +1 -1
  120. package/workframe-ui/public/assets/{mermaid-parser.core-Bkimsnqj.js → mermaid-parser.core-YmbZ-AfY.js} +2 -2
  121. package/workframe-ui/public/assets/{mermaid.core-x0TvVuPo.js → mermaid.core-BFdCAqCo.js} +3 -3
  122. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-6ykAFPEz.js → mindmap-definition-RKZ34NQL-Cy2iCtEl.js} +1 -1
  123. package/workframe-ui/public/assets/{packet-YPE3B663-Dw3xgMDt.js → packet-YPE3B663-DwOBZL6K.js} +1 -1
  124. package/workframe-ui/public/assets/{pie-LRSECV5Y-DATysawG.js → pie-LRSECV5Y-04PPhnKK.js} +1 -1
  125. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-SJKD1S0S.js → pieDiagram-4H26LBE5-LxIpgHqi.js} +1 -1
  126. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-BrYDZX8q.js → quadrantDiagram-W4KKPZXB-0nBYfYm4.js} +1 -1
  127. package/workframe-ui/public/assets/{radar-GUYGQ44K-BmWYPCds.js → radar-GUYGQ44K-D2-vBqps.js} +1 -1
  128. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DwL9Mc8e.js → requirementDiagram-4Y6WPE33-DbuU0nlu.js} +1 -1
  129. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DYIFsL8h.js → sankeyDiagram-5OEKKPKP-B2hQ6B2x.js} +1 -1
  130. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-0-FPkFk8.js → sequenceDiagram-3UESZ5HK-BBrU30e1.js} +1 -1
  131. package/workframe-ui/public/assets/{src-B_od6b6h.js → src-BJEDmV70.js} +1 -1
  132. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BQCiBk6u.js → stateDiagram-AJRCARHV-7FGO4kkH.js} +1 -1
  133. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DLTSizMg.js +1 -0
  134. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DS3tFcXj.js → timeline-definition-PNZ67QCA-ptDm4rCN.js} +1 -1
  135. package/workframe-ui/public/assets/{treeView-BLDUP644-DSyUCKLY.js → treeView-BLDUP644-CS6Z-0q8.js} +1 -1
  136. package/workframe-ui/public/assets/{treemap-LRROVOQU-CEZaNh5Y.js → treemap-LRROVOQU-DqV4Y2VA.js} +1 -1
  137. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CD-Vc9NF.js → vennDiagram-CIIHVFJN-C0UrZJYt.js} +1 -1
  138. package/workframe-ui/public/assets/{wardley-L42UT6IY-Drq5w1Mc.js → wardley-L42UT6IY-bNDN3_Sa.js} +1 -1
  139. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DouXDJoF.js → wardleyDiagram-YWT4CUSO-jWiJsefM.js} +1 -1
  140. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DDf_Lol5.js → xychartDiagram-2RQKCTM6-Dsh_fLCy.js} +1 -1
  141. package/workframe-ui/public/favicon.svg +7 -7
  142. package/workframe-ui/public/index.html +50 -50
  143. package/workframe-ui/public/workframe-config.json +3 -3
  144. package/scripts/security_audit.py +0 -156
  145. package/scripts/test-scaffold.mjs +0 -390
  146. package/workframe-api/tests/__init__.py +0 -0
  147. package/workframe-api/tests/db_setup.py +0 -13
  148. package/workframe-api/tests/test_admin_updates_gated.py +0 -30
  149. package/workframe-api/tests/test_agent_dm_bootstrap.py +0 -196
  150. package/workframe-api/tests/test_agent_profile_sync.py +0 -76
  151. package/workframe-api/tests/test_auth_email.py +0 -222
  152. package/workframe-api/tests/test_auth_hole_fix_selfcheck.py +0 -99
  153. package/workframe-api/tests/test_auth_rate_limit.py +0 -19
  154. package/workframe-api/tests/test_avatar_resolve.py +0 -77
  155. package/workframe-api/tests/test_child_soul_template.py +0 -71
  156. package/workframe-api/tests/test_credential_canary.py +0 -135
  157. package/workframe-api/tests/test_credential_isolation.py +0 -448
  158. package/workframe-api/tests/test_credential_resolution.py +0 -206
  159. package/workframe-api/tests/test_device_oauth.py +0 -108
  160. package/workframe-api/tests/test_doctor_repair.py +0 -103
  161. package/workframe-api/tests/test_ensure_profile_api.py +0 -77
  162. package/workframe-api/tests/test_gateway_compose_security.py +0 -136
  163. package/workframe-api/tests/test_install_secure_host.py +0 -39
  164. package/workframe-api/tests/test_internal_proxy_auth.py +0 -125
  165. package/workframe-api/tests/test_invite_runtime_bootstrap.py +0 -72
  166. package/workframe-api/tests/test_kanban_delegation.py +0 -185
  167. package/workframe-api/tests/test_llm_proxy.py +0 -155
  168. package/workframe-api/tests/test_login_access_policy.py +0 -183
  169. package/workframe-api/tests/test_mvp_model_bootstrap.py +0 -75
  170. package/workframe-api/tests/test_onboarding_bootstrap.py +0 -248
  171. package/workframe-api/tests/test_platform_auth.py +0 -47
  172. package/workframe-api/tests/test_profile_config_path.py +0 -56
  173. package/workframe-api/tests/test_profile_config_yaml_repair.py +0 -63
  174. package/workframe-api/tests/test_profile_create.py +0 -72
  175. package/workframe-api/tests/test_profile_identity_overlay.py +0 -61
  176. package/workframe-api/tests/test_profile_install_health.py +0 -45
  177. package/workframe-api/tests/test_profile_secret_policy.py +0 -57
  178. package/workframe-api/tests/test_profile_workspace_cwd.py +0 -34
  179. package/workframe-api/tests/test_provider_bootstrap.py +0 -75
  180. package/workframe-api/tests/test_provider_connect.py +0 -54
  181. package/workframe-api/tests/test_room_crud.py +0 -192
  182. package/workframe-api/tests/test_room_tenancy.py +0 -701
  183. package/workframe-api/tests/test_runtime_identity_backfill.py +0 -34
  184. package/workframe-api/tests/test_site_meta.py +0 -81
  185. package/workframe-api/tests/test_soul_stub.py +0 -42
  186. package/workframe-api/tests/test_space_member_sync.py +0 -99
  187. package/workframe-api/tests/test_stripe_stack_config.py +0 -37
  188. package/workframe-api/tests/test_supervisor_lifecycle.py +0 -52
  189. package/workframe-api/tests/test_turn_credential_vault.py +0 -125
  190. package/workframe-api/tests/test_updates.py +0 -176
  191. package/workframe-api/tests/test_user_cohort.py +0 -113
  192. package/workframe-api/tests/test_vault_envelope.py +0 -110
  193. package/workframe-api/tests/test_workspace_members.py +0 -183
  194. package/workframe-api/tests/test_workspace_messaging_sync.py +0 -125
  195. package/workframe-api/tests/test_workspace_provider_list.py +0 -57
  196. package/workframe-supervisor/tests/test_exec_guard.py +0 -42
  197. package/workframe-supervisor/tests/test_server_import.py +0 -21
  198. package/workframe-ui/public/assets/architecture-7EHR7CIX-CtbQKTuT.js +0 -1
  199. package/workframe-ui/public/assets/channel-Dy4Z4-jn.js +0 -1
  200. package/workframe-ui/public/assets/chunk-QZHKN3VN-CtBEchFK.js +0 -1
  201. package/workframe-ui/public/assets/chunk-WU5MYG2G-B9pBtriN.js +0 -1
  202. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BMAEA8jI.js +0 -1
  203. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BMAEA8jI.js +0 -1
  204. package/workframe-ui/public/assets/eventmodeling-FCH6USID-D75cstNT.js +0 -1
  205. package/workframe-ui/public/assets/index-DpAGxump.css +0 -1
  206. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-B89jAMFF.js +0 -1
@@ -1,185 +0,0 @@
1
- """Kanban assignee validation with delegation grants and workspace board mapping."""
2
-
3
- import tempfile
4
- import unittest
5
- from pathlib import Path
6
- from unittest.mock import patch
7
-
8
- import server
9
- import credential_vault
10
- import vault_kek
11
- from db_setup import ensure_workframe_schemas
12
-
13
-
14
- class KanbanDelegationTests(unittest.TestCase):
15
- def setUp(self) -> None:
16
- self._tmp = tempfile.TemporaryDirectory()
17
- self.addCleanup(self._tmp.cleanup)
18
- self._old_data_dir = server.DATA_DIR
19
- self._old_auth_db_path = server.AUTH_DB_PATH
20
- self._old_hermes_data = server.HERMES_DATA
21
- server.DATA_DIR = Path(self._tmp.name)
22
- server.AUTH_DB_PATH = Path(self._tmp.name) / "auth.db"
23
- server.HERMES_DATA = Path(self._tmp.name) / "hermes"
24
- (server.HERMES_DATA / "profiles").mkdir(parents=True)
25
- credential_vault.DATA_DIR = server.DATA_DIR
26
- credential_vault.VAULT_DB = server.DATA_DIR / "credential_vault.db"
27
- vault_kek.DATA_DIR = server.DATA_DIR
28
- vault_kek.VAULT_KEK_FILE = server.DATA_DIR / ".vault_kek"
29
- credential_vault.ensure_schema()
30
- credential_vault.unseal_for_tests()
31
- ensure_workframe_schemas()
32
- self.workspace_id = "ws-kanban"
33
- self.user_a = "cb6a2db4-ac86-4c49-8247-14a1d68aca72"
34
- self.user_b = "44fb344c-0954-47b6-a19a-ebbcf20e9680"
35
- conn = server._workframe_db()
36
- try:
37
- now = "1"
38
- conn.execute(
39
- "INSERT INTO workspaces (id, slug, display_name, owner_id, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?)",
40
- (self.workspace_id, "dogfood", "Dogfood", self.user_a, "active", now, now),
41
- )
42
- for uid, name in ((self.user_a, "Fab"), (self.user_b, "Alan")):
43
- conn.execute(
44
- "INSERT INTO users (id, display_name, role, status, created_at, updated_at) VALUES (?,?,?,?,?,?)",
45
- (uid, name, "member", "active", now, now),
46
- )
47
- conn.execute(
48
- """
49
- INSERT INTO workspace_memberships (id, workspace_id, user_id, role, status, created_at, updated_at)
50
- VALUES (?, ?, ?, 'member', 'active', ?, ?)
51
- """,
52
- (f"wm-{uid}", self.workspace_id, uid, now, now),
53
- )
54
- for slug, name in (("workframe-agent", "Workframe Agent"), ("architect", "Architect")):
55
- conn.execute(
56
- """
57
- INSERT INTO agent_profiles (id, workspace_id, slug, display_name, is_native, status, created_at, updated_at)
58
- VALUES (?, ?, ?, ?, ?, 'available', ?, ?)
59
- """,
60
- (f"ap-{slug}", self.workspace_id, slug, name, slug == "workframe-agent", now, now),
61
- )
62
- conn.commit()
63
- finally:
64
- conn.close()
65
- self.runtime_a_arch = server._runtime_profile_slug(self.user_a, "architect")
66
- self.runtime_b_arch = server._runtime_profile_slug(self.user_b, "architect")
67
-
68
- def tearDown(self) -> None:
69
- server.DATA_DIR = self._old_data_dir
70
- server.AUTH_DB_PATH = self._old_auth_db_path
71
- server.HERMES_DATA = self._old_hermes_data
72
-
73
- def test_cross_user_assignee_blocked_without_grant(self) -> None:
74
- ok, reason = server.validate_kanban_assignee(
75
- self.runtime_a_arch,
76
- self.user_b,
77
- self.workspace_id,
78
- )
79
- self.assertFalse(ok)
80
- self.assertEqual(reason, "assignee_owner_forbidden")
81
-
82
- def test_delegate_grant_allows_cross_user_assignee(self) -> None:
83
- server.create_delegation_grant(self.workspace_id, self.user_a, self.user_b)
84
- ok, reason = server.validate_kanban_assignee(
85
- self.runtime_a_arch,
86
- self.user_b,
87
- self.workspace_id,
88
- )
89
- self.assertTrue(ok)
90
- self.assertEqual(reason, self.user_a)
91
-
92
- def test_template_assignee_forbidden_by_default(self) -> None:
93
- ok, reason = server.validate_kanban_assignee("architect", self.user_a, self.workspace_id)
94
- self.assertFalse(ok)
95
- self.assertEqual(reason, "template_assignee_forbidden")
96
-
97
- @patch.object(server, "_gateway_exec", return_value=(0, "created t_test"))
98
- @patch.object(server, "resolve_runtime_assignee", side_effect=lambda t, u, w: server._runtime_profile_slug(u, t))
99
- def test_kanban_proxy_create_rejects_cross_user(self, _resolve, _exec) -> None:
100
- with self.assertRaises(PermissionError):
101
- server.kanban_proxy_create_task(
102
- self.workspace_id,
103
- self.user_b,
104
- {"title": "nope", "template_slug": "architect", "assignee_user_id": self.user_a},
105
- )
106
- _exec.assert_not_called()
107
-
108
- @patch.object(server, "_gateway_exec", return_value=(0, "created t_ok"))
109
- @patch.object(server, "resolve_runtime_assignee", side_effect=lambda t, u, w: server._runtime_profile_slug(u, t))
110
- def test_kanban_proxy_create_with_grant(self, _resolve, _exec) -> None:
111
- server.create_delegation_grant(self.workspace_id, self.user_a, self.user_b)
112
- result = server.kanban_proxy_create_task(
113
- self.workspace_id,
114
- self.user_b,
115
- {"title": "ok", "template_slug": "architect", "assignee_user_id": self.user_a},
116
- )
117
- self.assertTrue(result["ok"])
118
- _exec.assert_called_once()
119
- args = _exec.call_args[0][1]
120
- self.assertIn("--board", args)
121
- self.assertIn(self.runtime_a_arch, args)
122
-
123
- def test_ensure_workspace_kanban_board_persists_slug(self) -> None:
124
- with patch.object(server, "_gateway_exec", return_value=(0, "ok")):
125
- slug = server.ensure_workspace_kanban_board(self.workspace_id)
126
- self.assertEqual(slug, "dogfood")
127
- row = server._workspace_kanban_board_row(self.workspace_id)
128
- self.assertIsNotNone(row)
129
- self.assertEqual(str(row["hermes_board_slug"]), "dogfood")
130
-
131
- @patch.object(server, "ensure_workspace_kanban_board", return_value="dogfood")
132
- @patch.object(server, "_gateway_exec", return_value=(0, "created t_cred"))
133
- @patch.object(server, "resolve_runtime_assignee", side_effect=lambda t, u, w: server._runtime_profile_slug(u, t))
134
- def test_delegated_kanban_uses_assignee_owner_credentials(
135
- self,
136
- _resolve: object,
137
- _exec: object,
138
- _board: object,
139
- ) -> None:
140
- """Bob delegates to Alice's runtime; credentials must be Alice's, not Bob's."""
141
- server.create_delegation_grant(self.workspace_id, self.user_a, self.user_b)
142
- prof_dir = server._profile_dir(self.runtime_a_arch)
143
- prof_dir.mkdir(parents=True, exist_ok=True)
144
- (prof_dir / "config.yaml").write_text(
145
- "model:\n default: openrouter/test\n provider: openrouter\n",
146
- encoding="utf-8",
147
- )
148
- server._user_hermes_home(self.user_a).mkdir(parents=True, exist_ok=True)
149
- server._user_hermes_home(self.user_b).mkdir(parents=True, exist_ok=True)
150
- credential_vault.store_secret(
151
- "alice-or", "sk-alice-owner", env_var="OPENROUTER_API_KEY", provider="openrouter", user_id=self.user_a,
152
- )
153
- conn = server._workframe_db()
154
- try:
155
- now = "1"
156
- conn.execute(
157
- """
158
- INSERT INTO credential_bindings (
159
- id, workspace_id, user_id, agent_profile_id, provider,
160
- credential_type, credential_ref, label, is_active,
161
- expires_at, created_by, created_at, updated_at, deleted_at
162
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
163
- """,
164
- (
165
- "alice-or", None, self.user_a, None, "openrouter",
166
- "api_key", credential_vault.vault_ref("alice-or"), "Alice OR", 1, None, self.user_a,
167
- now, now, None,
168
- ),
169
- )
170
- conn.commit()
171
- finally:
172
- conn.close()
173
- server.kanban_proxy_create_task(
174
- self.workspace_id,
175
- self.user_b,
176
- {"title": "delegated", "template_slug": "architect", "assignee_user_id": self.user_a},
177
- )
178
- env_text = (prof_dir / ".env").read_text(encoding="utf-8") if (prof_dir / ".env").is_file() else ""
179
- self.assertNotIn("sk-alice-owner", env_text)
180
- self.assertNotIn("sk-bob-initiator", env_text)
181
- self.assertTrue(server._user_can_use_llm(self.user_a, self.workspace_id))
182
-
183
-
184
- if __name__ == "__main__":
185
- unittest.main()
@@ -1,155 +0,0 @@
1
- """LLM proxy path normalization."""
2
-
3
- import os
4
- import unittest
5
- from unittest.mock import patch
6
-
7
- import internal_proxy_auth
8
- import llm_proxy
9
-
10
-
11
- class LlmProxyPathTests(unittest.TestCase):
12
- def setUp(self) -> None:
13
- internal_proxy_auth.reset_proxy_token_for_tests()
14
- os.environ.pop("WORKFRAME_PROXY_TOKEN", None)
15
-
16
- def tearDown(self) -> None:
17
- internal_proxy_auth.reset_proxy_token_for_tests()
18
- os.environ.pop("WORKFRAME_PROXY_TOKEN", None)
19
- def test_normalize_dedup_v1_prefix(self) -> None:
20
- base = "https://openrouter.ai/api/v1"
21
- self.assertEqual(
22
- llm_proxy.normalize_upstream_path(base, "/v1/chat/completions"),
23
- "/chat/completions",
24
- )
25
- self.assertEqual(llm_proxy.normalize_upstream_path(base, "/v1"), "")
26
-
27
- def test_normalize_keeps_other_paths(self) -> None:
28
- base = "https://api.anthropic.com"
29
- self.assertEqual(
30
- llm_proxy.normalize_upstream_path(base, "/v1/messages"),
31
- "/v1/messages",
32
- )
33
-
34
- def test_handler_streams_success_response_chunks(self) -> None:
35
- class FakeResponse:
36
- status = 200
37
- headers = {"Content-Type": "text/event-stream"}
38
-
39
- def __init__(self) -> None:
40
- self._chunks = [b"data: one\n\n", b"data: two\n\n", b""]
41
-
42
- def __enter__(self):
43
- return self
44
-
45
- def __exit__(self, *_args):
46
- return False
47
-
48
- def read(self, _size: int = -1) -> bytes:
49
- return self._chunks.pop(0)
50
-
51
- def readline(self) -> bytes:
52
- return self._chunks.pop(0)
53
-
54
- class FakeWriter:
55
- def __init__(self) -> None:
56
- self.writes: list[bytes] = []
57
- self.flushes = 0
58
-
59
- def write(self, data: bytes) -> None:
60
- self.writes.append(data)
61
-
62
- def flush(self) -> None:
63
- self.flushes += 1
64
-
65
- class FakeHandler:
66
- client_address = ("172.19.0.2", 1234)
67
- headers = {
68
- "Authorization": "Bearer wf_rt_test",
69
- "X-Workframe-Profile": "u-alice-architect",
70
- }
71
-
72
- def __init__(self) -> None:
73
- self.wfile = FakeWriter()
74
- self.status = 0
75
- self.headers_sent: list[tuple[str, str]] = []
76
-
77
- def send_response(self, status: int) -> None:
78
- self.status = status
79
-
80
- def send_header(self, key: str, value: str) -> None:
81
- self.headers_sent.append((key, value))
82
-
83
- def end_headers(self) -> None:
84
- pass
85
-
86
- handler = FakeHandler()
87
- with patch.object(
88
- llm_proxy.turn_credentials,
89
- "validate_lease",
90
- return_value={
91
- "provider": "openrouter",
92
- "profile_slug": "u-alice-architect",
93
- },
94
- ), patch.object(
95
- llm_proxy.turn_credentials,
96
- "resolve_lease_secret",
97
- return_value=("OPENROUTER_API_KEY", "sk-test"),
98
- ), patch.object(llm_proxy.urllib.request, "urlopen", return_value=FakeResponse()):
99
- handled = llm_proxy.handle_proxy_request(
100
- handler,
101
- "/internal/llm/openrouter/v1/chat/completions",
102
- "POST",
103
- b'{"stream":true}',
104
- resolve_secret=lambda *_args: ("OPENROUTER_API_KEY", "sk-test"),
105
- )
106
-
107
- self.assertTrue(handled)
108
- self.assertEqual(handler.status, 200)
109
- self.assertEqual(handler.wfile.writes, [b"data: one\n\n", b"data: two\n\n"])
110
- self.assertEqual(handler.wfile.flushes, 2)
111
-
112
- def test_lease_rejects_profile_mismatch(self) -> None:
113
- class FakeHandler:
114
- client_address = ("172.19.0.2", 1234)
115
- headers = {
116
- "Authorization": "Bearer wf_rt_test",
117
- "X-Workframe-Profile": "u-bob-architect",
118
- }
119
-
120
- def __init__(self) -> None:
121
- self.wfile = type("W", (), {"write": lambda *_a, **_k: None})()
122
- self.status = 0
123
- self.body = b""
124
-
125
- def send_response(self, status: int) -> None:
126
- self.status = status
127
-
128
- def send_header(self, _key: str, _value: str) -> None:
129
- pass
130
-
131
- def end_headers(self) -> None:
132
- pass
133
-
134
- handler = FakeHandler()
135
- with patch.object(
136
- llm_proxy.turn_credentials,
137
- "validate_lease",
138
- return_value={
139
- "provider": "openrouter",
140
- "profile_slug": "u-alice-architect",
141
- },
142
- ):
143
- handled = llm_proxy.handle_proxy_request(
144
- handler,
145
- "/internal/llm/openrouter/v1/models",
146
- "GET",
147
- None,
148
- resolve_secret=lambda *_args: ("OPENROUTER_API_KEY", "sk-test"),
149
- )
150
- self.assertTrue(handled)
151
- self.assertEqual(handler.status, 403)
152
-
153
-
154
- if __name__ == "__main__":
155
- unittest.main()
@@ -1,183 +0,0 @@
1
- """Invite-only login for multi-user modes post-install."""
2
-
3
- from __future__ import annotations
4
-
5
- import hashlib
6
- import tempfile
7
- import time
8
- import unittest
9
- import uuid
10
- from pathlib import Path
11
- from unittest import mock
12
-
13
- import server
14
- import stack_config
15
- from db_setup import ensure_workframe_schemas
16
-
17
-
18
- class LoginAccessPolicyTests(unittest.TestCase):
19
- def setUp(self) -> None:
20
- self._tmp = tempfile.TemporaryDirectory()
21
- self.addCleanup(self._tmp.cleanup)
22
- self._old_data_dir = server.DATA_DIR
23
- self._old_auth_db_path = server.AUTH_DB_PATH
24
- self._old_mode = server.DEPLOYMENT_MODE
25
- self._old_dev = server.DEV_LOCAL_UNSAFE
26
- server.DATA_DIR = Path(self._tmp.name)
27
- server.AUTH_DB_PATH = Path(self._tmp.name) / "auth.db"
28
- server.DEV_LOCAL_UNSAFE = False
29
- ensure_workframe_schemas()
30
- stack_config.patch_stack_config({"install_complete": True, "deployment_mode": "public_multi_user"})
31
- server.DEPLOYMENT_MODE = "public_multi_user"
32
-
33
- self.workspace_id = "ws-closed"
34
- self.owner_id = "user-owner"
35
- self.owner_email = "owner@biz.test"
36
- conn = server._workframe_db()
37
- try:
38
- now = str(int(time.time()))
39
- conn.execute(
40
- "INSERT INTO users (id, email, display_name, role, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?)",
41
- (self.owner_id, self.owner_email, "Owner", "owner", "active", now, now),
42
- )
43
- conn.execute(
44
- """
45
- INSERT INTO workspaces (id, slug, display_name, owner_id, status, created_at, updated_at)
46
- VALUES (?, ?, ?, ?, ?, ?, ?)
47
- """,
48
- (self.workspace_id, "default", "Acme Corp", self.owner_id, "active", now, now),
49
- )
50
- conn.execute(
51
- """
52
- INSERT INTO workspace_memberships (id, workspace_id, user_id, role, status, created_at, updated_at)
53
- VALUES (?, ?, ?, ?, ?, ?, ?)
54
- """,
55
- ("wm-owner", self.workspace_id, self.owner_id, "owner", "active", now, now),
56
- )
57
- conn.commit()
58
- finally:
59
- conn.close()
60
-
61
- def tearDown(self) -> None:
62
- server.DATA_DIR = self._old_data_dir
63
- server.AUTH_DB_PATH = self._old_auth_db_path
64
- server.DEPLOYMENT_MODE = self._old_mode
65
- server.DEV_LOCAL_UNSAFE = self._old_dev
66
-
67
- def test_stranger_denied_when_invite_only(self) -> None:
68
- self.assertTrue(server._invite_only_login_enforced())
69
- allowed, meta = server._email_allowed_to_authenticate("stranger@evil.test")
70
- self.assertFalse(allowed)
71
- self.assertEqual(meta["error"], "private_workspace")
72
- self.assertIn("Acme Corp", meta["message"])
73
-
74
- def test_owner_allowed(self) -> None:
75
- allowed, _ = server._email_allowed_to_authenticate(self.owner_email)
76
- self.assertTrue(allowed)
77
-
78
- def test_pending_invitee_allowed(self) -> None:
79
- invite_email = "invitee@partner.test"
80
- token = "invite-token-secret"
81
- conn = server._workframe_db()
82
- try:
83
- conn.execute(
84
- """
85
- INSERT INTO workspace_invites
86
- (id, workspace_id, email, role, token_hash, invited_by_user_id, expires_at, created_at)
87
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
88
- """,
89
- (
90
- "inv-1",
91
- self.workspace_id,
92
- invite_email,
93
- "member",
94
- hashlib.sha256(token.encode()).hexdigest(),
95
- self.owner_id,
96
- str(int(time.time()) + 3600),
97
- str(int(time.time())),
98
- ),
99
- )
100
- conn.commit()
101
- finally:
102
- conn.close()
103
- allowed, _ = server._email_allowed_to_authenticate(invite_email)
104
- self.assertTrue(allowed)
105
- self.assertTrue(server._invite_token_allows_email(token, invite_email))
106
-
107
- def test_trusted_team_denies_stranger_post_install(self) -> None:
108
- server.DEPLOYMENT_MODE = "trusted_team"
109
- allowed, meta = server._email_allowed_to_authenticate("stranger@evil.test")
110
- self.assertFalse(allowed)
111
- self.assertEqual(meta["error"], "private_workspace")
112
-
113
- def test_owner_claim_blocked_after_install(self) -> None:
114
- conn = server._workframe_db()
115
- try:
116
- now = str(int(time.time()))
117
- conn.execute(
118
- "INSERT INTO workspaces (id, slug, display_name, owner_id, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?)",
119
- ("ws-unclaimed", "unclaimed", "Unclaimed", "", "active", now, now),
120
- )
121
- conn.commit()
122
- promoted = server._promote_workspace_owner_if_unclaimed(conn, "ws-unclaimed", "user-attacker")
123
- conn.commit()
124
- row = conn.execute(
125
- "SELECT owner_id FROM workspaces WHERE id = ?", ("ws-unclaimed",)
126
- ).fetchone()
127
- finally:
128
- conn.close()
129
- self.assertFalse(promoted)
130
- self.assertEqual(str(row["owner_id"] or ""), "")
131
-
132
- def test_install_stack_get_denied_anonymous_post_install(self) -> None:
133
- from http.server import BaseHTTPRequestHandler
134
-
135
- handler = mock.Mock(spec=BaseHTTPRequestHandler)
136
- handler.command = "GET"
137
- handler.path = "/api/install/stack"
138
- handler.headers = {}
139
- with mock.patch.object(server, "_install_window_open", return_value=False), mock.patch.object(
140
- server, "_session_id_from_request", return_value=""
141
- ):
142
- self.assertFalse(server._auth_check(handler))
143
-
144
- def _handler(self) -> server.Handler:
145
- from io import BytesIO
146
- from unittest.mock import MagicMock
147
-
148
- sock = MagicMock()
149
- sock.makefile.return_value = BytesIO()
150
- return server.Handler(sock, ("127.0.0.1", 0), None)
151
-
152
- def test_ensure_user_does_not_auto_join_when_invite_only(self) -> None:
153
- stranger_id = str(uuid.uuid4())
154
- stranger_email = "stranger@evil.test"
155
- self._handler()._ensure_user(stranger_id, stranger_email, stranger_email)
156
- conn = server._workframe_db()
157
- try:
158
- row = conn.execute(
159
- "SELECT id FROM workspace_memberships WHERE workspace_id = ? AND user_id = ? AND deleted_at IS NULL",
160
- (self.workspace_id, stranger_id),
161
- ).fetchone()
162
- self.assertIsNone(row)
163
- finally:
164
- conn.close()
165
-
166
- @mock.patch.object(server, "_invite_only_login_enforced", return_value=False)
167
- def test_ensure_user_auto_joins_when_not_invite_only(self, _enforced: mock.MagicMock) -> None:
168
- stranger_id = str(uuid.uuid4())
169
- stranger_email = "open@dogfood.test"
170
- self._handler()._ensure_user(stranger_id, stranger_email, stranger_email)
171
- conn = server._workframe_db()
172
- try:
173
- row = conn.execute(
174
- "SELECT id FROM workspace_memberships WHERE workspace_id = ? AND user_id = ? AND deleted_at IS NULL",
175
- (self.workspace_id, stranger_id),
176
- ).fetchone()
177
- self.assertIsNotNone(row)
178
- finally:
179
- conn.close()
180
-
181
-
182
- if __name__ == "__main__":
183
- unittest.main()
@@ -1,75 +0,0 @@
1
- import importlib.util
2
- import tempfile
3
- import unittest
4
- from pathlib import Path
5
- from unittest.mock import patch
6
-
7
- API = Path(__file__).resolve().parents[1] / "server.py"
8
- spec = importlib.util.spec_from_file_location("server", API)
9
- server = importlib.util.module_from_spec(spec)
10
- assert spec and spec.loader
11
- spec.loader.exec_module(server)
12
-
13
-
14
- class MvpModelBootstrapTest(unittest.TestCase):
15
- def test_apply_mvp_openrouter_writes_config_yaml(self) -> None:
16
- with tempfile.TemporaryDirectory() as tmp:
17
- root = Path(tmp)
18
- prof_dir = root / "profiles" / "u-test-user-workframe-agent"
19
- prof_dir.mkdir(parents=True)
20
- (prof_dir / "profile.yaml").write_text(
21
- "model:\n default: openrouter/owl-alpha\n provider: openrouter\n",
22
- encoding="utf-8",
23
- )
24
- with patch.object(server, "HERMES_DATA", root):
25
- ok = server._apply_mvp_model_for_provider("u-test-user-workframe-agent", "openrouter")
26
- self.assertTrue(ok)
27
- cfg = prof_dir / "config.yaml"
28
- self.assertTrue(cfg.is_file())
29
- text = cfg.read_text(encoding="utf-8")
30
- self.assertIn("default: openrouter/owl-alpha", text)
31
- self.assertIn("provider: openrouter", text)
32
- self.assertIn("openrouter/nex-agi", text)
33
- self.assertIn("nemotron-3-ultra", text)
34
-
35
- def test_bootstrap_runtime_sets_model_when_missing(self) -> None:
36
- with tempfile.TemporaryDirectory() as tmp:
37
- root = Path(tmp)
38
- runtime = "u-test-user-workframe-agent"
39
- runtime_dir = root / "profiles" / runtime
40
- runtime_dir.mkdir(parents=True)
41
- (runtime_dir / ".env").write_text("OPENROUTER_API_KEY=sk-test\n", encoding="utf-8")
42
- user_home = root / "profiles" / "user-test"
43
- user_home.mkdir(parents=True)
44
- (user_home / ".env").write_text("OPENROUTER_API_KEY=sk-test\n", encoding="utf-8")
45
- with patch.object(server, "HERMES_DATA", root), patch.object(
46
- server, "NATIVE_PROFILE", "workframe-agent"
47
- ), patch.object(server, "_primary_profile", return_value="workframe-agent"), patch.object(
48
- server, "resolve_hermes_profile", side_effect=lambda p: p
49
- ), patch.object(server, "_prepare_runtime_profile_credentials", return_value=True), patch.object(
50
- server, "_resolve_credential", return_value={"credential_ref": "env:OPENROUTER_API_KEY", "env_var": "OPENROUTER_API_KEY"}
51
- ):
52
- server._bootstrap_profile_providers(runtime, "user-test", "ws-1")
53
- cfg = runtime_dir / "config.yaml"
54
- self.assertTrue(cfg.is_file())
55
- self.assertIn("openrouter/owl-alpha", cfg.read_text(encoding="utf-8"))
56
-
57
- def test_profile_model_reads_nested_default_without_crossing_lines(self) -> None:
58
- with tempfile.TemporaryDirectory() as tmp:
59
- root = Path(tmp)
60
- runtime = "u-test-user-workframe-agent"
61
- runtime_dir = root / "profiles" / runtime
62
- runtime_dir.mkdir(parents=True)
63
- (runtime_dir / "config.yaml").write_text(
64
- "model:\n"
65
- " provider: custom\n"
66
- " default: openrouter/owl-alpha\n"
67
- " base_url: http://workframe-api:8080/internal/llm/openrouter/v1\n",
68
- encoding="utf-8",
69
- )
70
- with patch.object(server, "HERMES_DATA", root):
71
- self.assertEqual(server._profile_model(runtime), "openrouter/owl-alpha")
72
-
73
-
74
- if __name__ == "__main__":
75
- unittest.main()