create-workframe 0.1.0 → 0.1.2

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 (198) hide show
  1. package/LICENSE +201 -201
  2. package/NOTICE +12 -12
  3. package/README.md +8 -92
  4. package/SECURITY.md +38 -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/bundle-workframe-ui.mjs +3 -3
  28. package/scripts/ensure-compose-host-paths.mjs +51 -51
  29. package/scripts/lib/install-identity.mjs +212 -212
  30. package/scripts/set-compose-public-url.mjs +92 -92
  31. package/scripts/sync-canonical-to-package.mjs +27 -9
  32. package/shared/WORKFRAME_AGENT_LIBRARY.md +17 -17
  33. package/shared/WORKFRAME_AGENT_OPERATIONS.md +15 -15
  34. package/shared/WORKFRAME_AGENT_PACKS.json +18 -18
  35. package/shared/WORKFRAME_AGENT_PACKS.yaml +8 -8
  36. package/shared/WORKFRAME_SKILL_CURATION.md +4 -4
  37. package/workframe-api/README.md +26 -28
  38. package/workframe-api/action_proxy.py +131 -131
  39. package/workframe-api/auth_rate_limit.py +49 -49
  40. package/workframe-api/credential_vault.py +445 -445
  41. package/workframe-api/data/avatar-catalog.json +41 -41
  42. package/workframe-api/email_sender.py +220 -220
  43. package/workframe-api/google_auth.py +90 -90
  44. package/workframe-api/install_api.py +359 -359
  45. package/workframe-api/internal_proxy_auth.py +150 -150
  46. package/workframe-api/llm_proxy.py +277 -277
  47. package/workframe-api/oidc_jwt.py +108 -108
  48. package/workframe-api/package.json +12 -13
  49. package/workframe-api/public/assets/index-DPXu_lGn.css +1 -1
  50. package/workframe-api/public/assets/index-DYnLrCZZ.js +8 -8
  51. package/workframe-api/requirements.txt +2 -2
  52. package/workframe-api/site_meta.py +271 -271
  53. package/workframe-api/stack_config.py +427 -427
  54. package/workframe-api/time-bind-chat.py +99 -99
  55. package/workframe-api/turn_credentials.py +226 -226
  56. package/workframe-api/updates.py +417 -417
  57. package/workframe-api/vault_kek.py +159 -159
  58. package/workframe-api/zk_auth.py +633 -633
  59. package/workframe-supervisor/Dockerfile +11 -11
  60. package/workframe-supervisor/server.py +787 -787
  61. package/workframe-ui/docker/nginx.conf +85 -85
  62. package/workframe-ui/public/assets/{arc-CBDYvkAF.js → arc-COAT3laO.js} +1 -1
  63. package/workframe-ui/public/assets/architecture-7EHR7CIX-DUyH3hWG.js +1 -0
  64. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-XnBRKeW0.js → architectureDiagram-3BPJPVTR-BFjWV24l.js} +1 -1
  65. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-VYHUfVhd.js → blockDiagram-GPEHLZMM-DSQLPfrj.js} +1 -1
  66. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BTjUcJpm.js → c4Diagram-AAUBKEIU-DKEHv1t2.js} +1 -1
  67. package/workframe-ui/public/assets/channel-g7r_RGaY.js +1 -0
  68. package/workframe-ui/public/assets/{chunk-2J33WTMH-w7uu7R-b.js → chunk-2J33WTMH-DHZg-DUi.js} +1 -1
  69. package/workframe-ui/public/assets/{chunk-3OPIFGDE-Cb9LtnDX.js → chunk-3OPIFGDE-BB-OYTfp.js} +1 -1
  70. package/workframe-ui/public/assets/{chunk-4BX2VUAB-DiQ-qCwH.js → chunk-4BX2VUAB-C93q0YIm.js} +1 -1
  71. package/workframe-ui/public/assets/{chunk-55IACEB6-C-mLFr7z.js → chunk-55IACEB6-MAYniqik.js} +1 -1
  72. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-DOesfiCI.js → chunk-5ZQYHXKU-ChgN6YJs.js} +1 -1
  73. package/workframe-ui/public/assets/{chunk-727SXJPM-BJ3oBZuz.js → chunk-727SXJPM-B_FYwdAv.js} +1 -1
  74. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-CCA6xpGs.js → chunk-AQP2D5EJ-1_Hw_h1A.js} +1 -1
  75. package/workframe-ui/public/assets/{chunk-BSJP7CBP-a0cMNFb2.js → chunk-BSJP7CBP-CFiDQ1Rv.js} +1 -1
  76. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-kuqN8EIY.js → chunk-CSCIHK7Q-DZ9UMTlB.js} +1 -1
  77. package/workframe-ui/public/assets/{chunk-FMBD7UC4-DyPgYHCg.js → chunk-FMBD7UC4-DlMlyFgw.js} +1 -1
  78. package/workframe-ui/public/assets/{chunk-KSCS5N6A-CdUuvR0V.js → chunk-KSCS5N6A-DHXtQ_Hf.js} +1 -1
  79. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-Dq9NoWmK.js → chunk-L5ZTLDWV-CuQzg-QG.js} +1 -1
  80. package/workframe-ui/public/assets/{chunk-LZXEDZCA-p74rddlO.js → chunk-LZXEDZCA-BHzjzCGg.js} +2 -2
  81. package/workframe-ui/public/assets/{chunk-ND2GUHAM-DBD2u1Gz.js → chunk-ND2GUHAM-DHXx05n2.js} +1 -1
  82. package/workframe-ui/public/assets/{chunk-NZK2D7GU-BeIeYFnd.js → chunk-NZK2D7GU-CV5pmDM_.js} +1 -1
  83. package/workframe-ui/public/assets/{chunk-O5CBEL6O-ClHc56ib.js → chunk-O5CBEL6O-6tkCHxsV.js} +1 -1
  84. package/workframe-ui/public/assets/chunk-QZHKN3VN-C5UQehWY.js +1 -0
  85. package/workframe-ui/public/assets/chunk-WU5MYG2G-DhWllrI8.js +1 -0
  86. package/workframe-ui/public/assets/{chunk-XPW4576I-EFr8R_1p.js → chunk-XPW4576I-BClwIiCp.js} +1 -1
  87. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BBM_8T8E.js +1 -0
  88. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BBM_8T8E.js +1 -0
  89. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-C7aPBODd.js → cose-bilkent-S5V4N54A-DOrGV6DQ.js} +1 -1
  90. package/workframe-ui/public/assets/{dagre-BM42HDAG-BdU1Rv-H.js → dagre-BM42HDAG-DXTPvJkX.js} +1 -1
  91. package/workframe-ui/public/assets/{diagram-2AECGRRQ-DWowSo85.js → diagram-2AECGRRQ-xX_v-pbf.js} +1 -1
  92. package/workframe-ui/public/assets/{diagram-5GNKFQAL-MnxBbceO.js → diagram-5GNKFQAL-Cd2pXbBe.js} +1 -1
  93. package/workframe-ui/public/assets/{diagram-KO2AKTUF-DQaLRXFf.js → diagram-KO2AKTUF-Df3XvUtk.js} +1 -1
  94. package/workframe-ui/public/assets/{diagram-LMA3HP47-CQaBud9k.js → diagram-LMA3HP47-CsijIPaD.js} +1 -1
  95. package/workframe-ui/public/assets/{diagram-OG6HWLK6-D8bAXbY9.js → diagram-OG6HWLK6-aq5fmfHd.js} +1 -1
  96. package/workframe-ui/public/assets/{dist-DGpTLHr_.js → dist-D1c0mkbB.js} +1 -1
  97. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-1E-xSvBK.js → erDiagram-TEJ5UH35-DnFysVRY.js} +1 -1
  98. package/workframe-ui/public/assets/eventmodeling-FCH6USID-Ci8mdb44.js +1 -0
  99. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-CgOVD5hu.js → flowDiagram-I6XJVG4X-C6Ebi3su.js} +1 -1
  100. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-JFYAIauo.js → ganttDiagram-6RSMTGT7-BQXQtUpa.js} +1 -1
  101. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-B9REenIl.js → gitGraph-WXDBUCRP-Dt0zIs_M.js} +1 -1
  102. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-BQ7NcMSn.js → gitGraphDiagram-PVQCEYII-BF8gHzRn.js} +1 -1
  103. package/workframe-ui/public/assets/index-DpoUZAxh.css +1 -0
  104. package/workframe-ui/public/assets/{index-Dnw6vjqb.js → index-lRpzpNPT.js} +2 -2
  105. package/workframe-ui/public/assets/{info-J43DQDTF-CL6-eTjH.js → info-J43DQDTF-CSmszQJT.js} +1 -1
  106. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-LJTODW4W.js → infoDiagram-5YYISTIA-CVTKGW6p.js} +1 -1
  107. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-bchrQVuo.js → ishikawaDiagram-YF4QCWOH-Z8pT09Lv.js} +1 -1
  108. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-DkrvYuxP.js → journeyDiagram-JHISSGLW-r3wD68_T.js} +1 -1
  109. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-DFRbj0IG.js → kanban-definition-UN3LZRKU-Il8VglqN.js} +1 -1
  110. package/workframe-ui/public/assets/{line-Vd48P7-O.js → line-oyjpfz2A.js} +1 -1
  111. package/workframe-ui/public/assets/{linear-Ckizh2G7.js → linear-Cf7p5tVp.js} +1 -1
  112. package/workframe-ui/public/assets/{mermaid-parser.core-Bkimsnqj.js → mermaid-parser.core-YmbZ-AfY.js} +2 -2
  113. package/workframe-ui/public/assets/{mermaid.core-x0TvVuPo.js → mermaid.core-BFdCAqCo.js} +3 -3
  114. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-6ykAFPEz.js → mindmap-definition-RKZ34NQL-Cy2iCtEl.js} +1 -1
  115. package/workframe-ui/public/assets/{packet-YPE3B663-Dw3xgMDt.js → packet-YPE3B663-DwOBZL6K.js} +1 -1
  116. package/workframe-ui/public/assets/{pie-LRSECV5Y-DATysawG.js → pie-LRSECV5Y-04PPhnKK.js} +1 -1
  117. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-SJKD1S0S.js → pieDiagram-4H26LBE5-LxIpgHqi.js} +1 -1
  118. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-BrYDZX8q.js → quadrantDiagram-W4KKPZXB-0nBYfYm4.js} +1 -1
  119. package/workframe-ui/public/assets/{radar-GUYGQ44K-BmWYPCds.js → radar-GUYGQ44K-D2-vBqps.js} +1 -1
  120. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DwL9Mc8e.js → requirementDiagram-4Y6WPE33-DbuU0nlu.js} +1 -1
  121. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DYIFsL8h.js → sankeyDiagram-5OEKKPKP-B2hQ6B2x.js} +1 -1
  122. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-0-FPkFk8.js → sequenceDiagram-3UESZ5HK-BBrU30e1.js} +1 -1
  123. package/workframe-ui/public/assets/{src-B_od6b6h.js → src-BJEDmV70.js} +1 -1
  124. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BQCiBk6u.js → stateDiagram-AJRCARHV-7FGO4kkH.js} +1 -1
  125. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DLTSizMg.js +1 -0
  126. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DS3tFcXj.js → timeline-definition-PNZ67QCA-ptDm4rCN.js} +1 -1
  127. package/workframe-ui/public/assets/{treeView-BLDUP644-DSyUCKLY.js → treeView-BLDUP644-CS6Z-0q8.js} +1 -1
  128. package/workframe-ui/public/assets/{treemap-LRROVOQU-CEZaNh5Y.js → treemap-LRROVOQU-DqV4Y2VA.js} +1 -1
  129. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CD-Vc9NF.js → vennDiagram-CIIHVFJN-C0UrZJYt.js} +1 -1
  130. package/workframe-ui/public/assets/{wardley-L42UT6IY-Drq5w1Mc.js → wardley-L42UT6IY-bNDN3_Sa.js} +1 -1
  131. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DouXDJoF.js → wardleyDiagram-YWT4CUSO-jWiJsefM.js} +1 -1
  132. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DDf_Lol5.js → xychartDiagram-2RQKCTM6-Dsh_fLCy.js} +1 -1
  133. package/workframe-ui/public/favicon.svg +7 -7
  134. package/workframe-ui/public/index.html +50 -50
  135. package/workframe-ui/public/workframe-config.json +3 -3
  136. package/scripts/security_audit.py +0 -156
  137. package/scripts/test-scaffold.mjs +0 -390
  138. package/workframe-api/tests/__init__.py +0 -0
  139. package/workframe-api/tests/db_setup.py +0 -13
  140. package/workframe-api/tests/test_admin_updates_gated.py +0 -30
  141. package/workframe-api/tests/test_agent_dm_bootstrap.py +0 -196
  142. package/workframe-api/tests/test_agent_profile_sync.py +0 -76
  143. package/workframe-api/tests/test_auth_email.py +0 -222
  144. package/workframe-api/tests/test_auth_hole_fix_selfcheck.py +0 -99
  145. package/workframe-api/tests/test_auth_rate_limit.py +0 -19
  146. package/workframe-api/tests/test_avatar_resolve.py +0 -77
  147. package/workframe-api/tests/test_child_soul_template.py +0 -71
  148. package/workframe-api/tests/test_credential_canary.py +0 -135
  149. package/workframe-api/tests/test_credential_isolation.py +0 -448
  150. package/workframe-api/tests/test_credential_resolution.py +0 -206
  151. package/workframe-api/tests/test_device_oauth.py +0 -108
  152. package/workframe-api/tests/test_doctor_repair.py +0 -103
  153. package/workframe-api/tests/test_ensure_profile_api.py +0 -77
  154. package/workframe-api/tests/test_gateway_compose_security.py +0 -136
  155. package/workframe-api/tests/test_install_secure_host.py +0 -39
  156. package/workframe-api/tests/test_internal_proxy_auth.py +0 -125
  157. package/workframe-api/tests/test_invite_runtime_bootstrap.py +0 -72
  158. package/workframe-api/tests/test_kanban_delegation.py +0 -185
  159. package/workframe-api/tests/test_llm_proxy.py +0 -155
  160. package/workframe-api/tests/test_login_access_policy.py +0 -183
  161. package/workframe-api/tests/test_mvp_model_bootstrap.py +0 -75
  162. package/workframe-api/tests/test_onboarding_bootstrap.py +0 -248
  163. package/workframe-api/tests/test_platform_auth.py +0 -47
  164. package/workframe-api/tests/test_profile_config_path.py +0 -56
  165. package/workframe-api/tests/test_profile_config_yaml_repair.py +0 -63
  166. package/workframe-api/tests/test_profile_create.py +0 -72
  167. package/workframe-api/tests/test_profile_identity_overlay.py +0 -61
  168. package/workframe-api/tests/test_profile_install_health.py +0 -45
  169. package/workframe-api/tests/test_profile_secret_policy.py +0 -57
  170. package/workframe-api/tests/test_profile_workspace_cwd.py +0 -34
  171. package/workframe-api/tests/test_provider_bootstrap.py +0 -75
  172. package/workframe-api/tests/test_provider_connect.py +0 -54
  173. package/workframe-api/tests/test_room_crud.py +0 -192
  174. package/workframe-api/tests/test_room_tenancy.py +0 -701
  175. package/workframe-api/tests/test_runtime_identity_backfill.py +0 -34
  176. package/workframe-api/tests/test_site_meta.py +0 -81
  177. package/workframe-api/tests/test_soul_stub.py +0 -42
  178. package/workframe-api/tests/test_space_member_sync.py +0 -99
  179. package/workframe-api/tests/test_stripe_stack_config.py +0 -37
  180. package/workframe-api/tests/test_supervisor_lifecycle.py +0 -52
  181. package/workframe-api/tests/test_turn_credential_vault.py +0 -125
  182. package/workframe-api/tests/test_updates.py +0 -176
  183. package/workframe-api/tests/test_user_cohort.py +0 -113
  184. package/workframe-api/tests/test_vault_envelope.py +0 -110
  185. package/workframe-api/tests/test_workspace_members.py +0 -183
  186. package/workframe-api/tests/test_workspace_messaging_sync.py +0 -125
  187. package/workframe-api/tests/test_workspace_provider_list.py +0 -57
  188. package/workframe-supervisor/tests/test_exec_guard.py +0 -42
  189. package/workframe-supervisor/tests/test_server_import.py +0 -21
  190. package/workframe-ui/public/assets/architecture-7EHR7CIX-CtbQKTuT.js +0 -1
  191. package/workframe-ui/public/assets/channel-Dy4Z4-jn.js +0 -1
  192. package/workframe-ui/public/assets/chunk-QZHKN3VN-CtBEchFK.js +0 -1
  193. package/workframe-ui/public/assets/chunk-WU5MYG2G-B9pBtriN.js +0 -1
  194. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BMAEA8jI.js +0 -1
  195. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BMAEA8jI.js +0 -1
  196. package/workframe-ui/public/assets/eventmodeling-FCH6USID-D75cstNT.js +0 -1
  197. package/workframe-ui/public/assets/index-DpAGxump.css +0 -1
  198. 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()