create-workframe 0.1.11 → 0.1.13

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 (195) hide show
  1. package/README.md +1 -1
  2. package/bin/create-workframe.js +2 -2
  3. package/bin/workframe.js +130 -2
  4. package/docs/security.md +85 -0
  5. package/package.json +1 -1
  6. package/scripts/bundle-workframe-ui.mjs +9 -0
  7. package/scripts/sync-canonical-to-package.mjs +9 -0
  8. package/scripts/verify-public-deploy.sh +121 -3
  9. package/workframe-api/action_proxy.py +23 -17
  10. package/workframe-api/activity_feed.py +798 -0
  11. package/workframe-api/api_meta.py +157 -0
  12. package/workframe-api/auth_gate.py +566 -0
  13. package/workframe-api/avatar_registry.py +343 -0
  14. package/workframe-api/broker_audit.py +164 -0
  15. package/workframe-api/cell_authority.py +231 -0
  16. package/workframe-api/chat_bind.py +319 -0
  17. package/workframe-api/chat_sessions.py +509 -0
  18. package/workframe-api/chat_stream.py +694 -0
  19. package/workframe-api/cleanup_dogfood_smoke.py +252 -0
  20. package/workframe-api/credential_broker.py +162 -0
  21. package/workframe-api/credential_resolve.py +202 -0
  22. package/workframe-api/credential_store.py +245 -0
  23. package/workframe-api/crew_registry.py +250 -0
  24. package/workframe-api/db_schema.py +755 -0
  25. package/workframe-api/docker_gateway.py +166 -0
  26. package/workframe-api/doctor_runtime.py +109 -0
  27. package/workframe-api/domain/__init__.py +47 -0
  28. package/workframe-api/domain/entities.py +252 -0
  29. package/workframe-api/domain/schema/workframe-domain.schema.json +278 -0
  30. package/workframe-api/egress_policy.py +42 -0
  31. package/workframe-api/handler_modules/__init__.py +17 -0
  32. package/workframe-api/handler_modules/handler_admin.py +542 -0
  33. package/workframe-api/handler_modules/handler_auth.py +512 -0
  34. package/workframe-api/handler_modules/handler_chat.py +641 -0
  35. package/workframe-api/handler_modules/handler_install.py +178 -0
  36. package/workframe-api/handler_modules/handler_provider.py +325 -0
  37. package/workframe-api/handler_modules/handler_workspace.py +1420 -0
  38. package/workframe-api/health_monitor.py +80 -0
  39. package/workframe-api/hermes_admin.py +756 -0
  40. package/workframe-api/hermes_profiles.py +1328 -0
  41. package/workframe-api/install_api.py +123 -0
  42. package/workframe-api/kanban_cron.py +473 -0
  43. package/workframe-api/lane_bindings.py +423 -0
  44. package/workframe-api/llm_proxy.py +17 -43
  45. package/workframe-api/mention_helpers.py +180 -0
  46. package/workframe-api/mention_invoke.py +431 -0
  47. package/workframe-api/model_surface.py +1139 -0
  48. package/workframe-api/oauth_pending.py +85 -0
  49. package/workframe-api/oauth_redirect.py +381 -0
  50. package/workframe-api/openrouter_catalog.py +4 -4
  51. package/workframe-api/package.json +2 -2
  52. package/workframe-api/profile_api_lifecycle.py +66 -0
  53. package/workframe-api/profile_config_yaml.py +126 -0
  54. package/workframe-api/profile_gateway.py +436 -0
  55. package/workframe-api/provider_bindings.py +673 -0
  56. package/workframe-api/provider_bootstrap.py +347 -0
  57. package/workframe-api/provider_catalog.py +180 -0
  58. package/workframe-api/rooms.py +1613 -0
  59. package/workframe-api/route_registry.py +470 -0
  60. package/workframe-api/run-typecheck.mjs +26 -0
  61. package/workframe-api/run_authority.py +287 -0
  62. package/workframe-api/run_ledger.py +470 -0
  63. package/workframe-api/run_surface_wiring.py +344 -0
  64. package/workframe-api/runtime_cohort.py +752 -0
  65. package/workframe-api/runtime_tokens.py +127 -0
  66. package/workframe-api/server.py +1376 -19305
  67. package/workframe-api/snapshot_feed.py +49 -0
  68. package/workframe-api/stack_config.py +33 -1
  69. package/workframe-api/supervisor_client.py +154 -0
  70. package/workframe-api/test_api_meta_build_stamp.py +34 -0
  71. package/workframe-api/test_cell_authority.py +79 -0
  72. package/workframe-api/test_chat_bind.py +48 -0
  73. package/workframe-api/test_credential_lease_matrix.py +171 -0
  74. package/workframe-api/test_credential_resolve.py +51 -0
  75. package/workframe-api/test_credential_store.py +27 -0
  76. package/workframe-api/test_dogfood_flows.py +346 -0
  77. package/workframe-api/test_domain_entities.py +152 -0
  78. package/workframe-api/test_exception_hygiene.py +12 -0
  79. package/workframe-api/test_hermes_admin.py +72 -0
  80. package/workframe-api/test_install_wizard_resume.py +78 -0
  81. package/workframe-api/test_local_bootstrap.py +67 -0
  82. package/workframe-api/test_mention_helpers.py +35 -0
  83. package/workframe-api/test_mention_invoke.py +26 -0
  84. package/workframe-api/test_model_surface_consistency.py +117 -0
  85. package/workframe-api/test_oauth_pending.py +41 -0
  86. package/workframe-api/test_profile_model_yaml.py +99 -0
  87. package/workframe-api/test_provider_bindings.py +52 -0
  88. package/workframe-api/test_provider_catalog.py +28 -0
  89. package/workframe-api/test_public_routes.py +41 -0
  90. package/workframe-api/test_route_registry.py +245 -0
  91. package/workframe-api/test_run_authority.py +112 -0
  92. package/workframe-api/test_run_ledger.py +157 -0
  93. package/workframe-api/test_run_surface_wiring.py +130 -0
  94. package/workframe-api/test_runtime_cohort.py +85 -0
  95. package/workframe-api/test_secure_mode_docker_boundary.py +39 -0
  96. package/workframe-api/test_server_reexports.py +99 -0
  97. package/workframe-api/test_stack_config_install_smtp.py +7 -0
  98. package/workframe-api/time-bind-steps.py +75 -0
  99. package/workframe-api/time-perf.py +81 -0
  100. package/workframe-api/time-warm.py +107 -0
  101. package/workframe-api/turn_credentials.py +28 -13
  102. package/workframe-api/turn_overlay.py +715 -0
  103. package/workframe-api/updates.py +16 -1
  104. package/workframe-api/user_prefs.py +387 -0
  105. package/workframe-api/wf032_extract_remaining_handlers.py +417 -0
  106. package/workframe-api/workspace_bootstrap.py +536 -0
  107. package/workframe-api/workspace_files.py +344 -0
  108. package/workframe-api/workspace_messaging.py +206 -0
  109. package/workframe-api/zk_auth.py +3 -2
  110. package/workframe-supervisor/server.py +10 -1
  111. package/workframe-supervisor/test_supervisor_negative.py +75 -0
  112. package/workframe-ui/public/assets/{arc-DjpS9a2b.js → arc-aUUffclm.js} +1 -1
  113. package/workframe-ui/public/assets/architecture-7EHR7CIX-BSUP4S8J.js +1 -0
  114. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-puCKmKHV.js → architectureDiagram-3BPJPVTR-DU6oaqIb.js} +1 -1
  115. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-C2P0g-eH.js → blockDiagram-GPEHLZMM-D2p8BU6-.js} +1 -1
  116. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-DdvYEITH.js → c4Diagram-AAUBKEIU-ouwyPmTJ.js} +1 -1
  117. package/workframe-ui/public/assets/channel-DOunZZ0X.js +1 -0
  118. package/workframe-ui/public/assets/{chunk-2J33WTMH-Cxe8QsEF.js → chunk-2J33WTMH-DsE7U5dj.js} +1 -1
  119. package/workframe-ui/public/assets/{chunk-3OPIFGDE-DPNyTwR3.js → chunk-3OPIFGDE-DM0kkZ9h.js} +1 -1
  120. package/workframe-ui/public/assets/{chunk-4BX2VUAB-XKG9gfRN.js → chunk-4BX2VUAB-CQ8mAyXp.js} +1 -1
  121. package/workframe-ui/public/assets/{chunk-55IACEB6-oPMTulgb.js → chunk-55IACEB6-DN1BDtbH.js} +1 -1
  122. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BWwXvpa_.js → chunk-5ZQYHXKU-BzK2KqOt.js} +1 -1
  123. package/workframe-ui/public/assets/{chunk-727SXJPM-Cml2twO_.js → chunk-727SXJPM-C3AxtOi9.js} +1 -1
  124. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-DzrRBhQu.js → chunk-AQP2D5EJ-4mVwEFuD.js} +1 -1
  125. package/workframe-ui/public/assets/{chunk-BSJP7CBP-EZ7STU6h.js → chunk-BSJP7CBP-23kN2q0A.js} +1 -1
  126. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-BGTQzWYw.js → chunk-CSCIHK7Q-CPBKVW-L.js} +2 -2
  127. package/workframe-ui/public/assets/{chunk-FMBD7UC4-Cm4stfRa.js → chunk-FMBD7UC4-CKqcYq7K.js} +1 -1
  128. package/workframe-ui/public/assets/{chunk-KSCS5N6A-exbMijjy.js → chunk-KSCS5N6A-JRQnhxQE.js} +1 -1
  129. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-C75kbQwN.js → chunk-L5ZTLDWV-BLZrdIwa.js} +1 -1
  130. package/workframe-ui/public/assets/chunk-LZXEDZCA-DisG0XJT.js +2 -0
  131. package/workframe-ui/public/assets/{chunk-ND2GUHAM-D84zyIu2.js → chunk-ND2GUHAM-ClAOxArS.js} +1 -1
  132. package/workframe-ui/public/assets/{chunk-NZK2D7GU-CUNNo1bj.js → chunk-NZK2D7GU-XUE1aNkm.js} +1 -1
  133. package/workframe-ui/public/assets/{chunk-O5CBEL6O-DN6WEIh-.js → chunk-O5CBEL6O-DDN-Ifon.js} +1 -1
  134. package/workframe-ui/public/assets/chunk-QZHKN3VN-EVQ0VMd5.js +1 -0
  135. package/workframe-ui/public/assets/chunk-WU5MYG2G-k_-ZsLOS.js +1 -0
  136. package/workframe-ui/public/assets/{chunk-XPW4576I-Ct5VQv2D.js → chunk-XPW4576I-B4_iYZ6r.js} +1 -1
  137. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-W9WVQsby.js +1 -0
  138. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-W9WVQsby.js +1 -0
  139. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-Bj4W5oMk.js → cose-bilkent-S5V4N54A-Cy1HhS7j.js} +1 -1
  140. package/workframe-ui/public/assets/{dagre-BM42HDAG-DAjHBkAy.js → dagre-BM42HDAG-Bjal81Ie.js} +1 -1
  141. package/workframe-ui/public/assets/{diagram-2AECGRRQ-Ch2gGFPG.js → diagram-2AECGRRQ-v5Gdvzp_.js} +1 -1
  142. package/workframe-ui/public/assets/{diagram-5GNKFQAL-BzIIvgYH.js → diagram-5GNKFQAL-DuyRdfmY.js} +1 -1
  143. package/workframe-ui/public/assets/{diagram-KO2AKTUF-Df0Q8nv6.js → diagram-KO2AKTUF-WwWCGbIW.js} +1 -1
  144. package/workframe-ui/public/assets/{diagram-LMA3HP47-D-G3ekmF.js → diagram-LMA3HP47-DTYwZALT.js} +1 -1
  145. package/workframe-ui/public/assets/{diagram-OG6HWLK6-TBHhT9f5.js → diagram-OG6HWLK6-B46kBIqE.js} +1 -1
  146. package/workframe-ui/public/assets/{dist-CPbiDkEZ.js → dist-mQWmB3z6.js} +1 -1
  147. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-YxkUmLLV.js → erDiagram-TEJ5UH35-__40xO-y.js} +1 -1
  148. package/workframe-ui/public/assets/eventmodeling-FCH6USID-Cm1uRTxU.js +1 -0
  149. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-kfZ7-RSd.js → flowDiagram-I6XJVG4X-Cwj6dVUz.js} +1 -1
  150. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-DmTx3hzR.js → ganttDiagram-6RSMTGT7-D_IOSwAS.js} +1 -1
  151. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BXa0ewp3.js → gitGraph-WXDBUCRP-C1vGecee.js} +1 -1
  152. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-CQr2qMxU.js → gitGraphDiagram-PVQCEYII-DMTxFJxn.js} +1 -1
  153. package/workframe-ui/public/assets/index-BdiM4-lI.js +130 -0
  154. package/workframe-ui/public/assets/index-Fnad47vV.css +1 -0
  155. package/workframe-ui/public/assets/{info-J43DQDTF-t-Qlx8zv.js → info-J43DQDTF-CwjmyPmD.js} +1 -1
  156. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-CzkYofPx.js → infoDiagram-5YYISTIA-BGzh7ZL2.js} +1 -1
  157. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-djK3Oxgb.js → ishikawaDiagram-YF4QCWOH-BLXdnR_5.js} +1 -1
  158. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CWxYXxdF.js → journeyDiagram-JHISSGLW-BK3behMe.js} +1 -1
  159. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-D7leCAD9.js → kanban-definition-UN3LZRKU-Bx172cCl.js} +1 -1
  160. package/workframe-ui/public/assets/{line-BSFj2OC5.js → line-BTCEHb7l.js} +1 -1
  161. package/workframe-ui/public/assets/{linear-pfPwnu-o.js → linear-CkbydpnK.js} +1 -1
  162. package/workframe-ui/public/assets/{mermaid-parser.core-Dm2L9Ib_.js → mermaid-parser.core-DvrtArpk.js} +2 -2
  163. package/workframe-ui/public/assets/{mermaid.core-Bevx9bif.js → mermaid.core-yj_1x_qj.js} +3 -3
  164. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-UkjDssmP.js → mindmap-definition-RKZ34NQL-BXbMltQ0.js} +1 -1
  165. package/workframe-ui/public/assets/{packet-YPE3B663-JO2ezH3_.js → packet-YPE3B663-9pVCBQ7S.js} +1 -1
  166. package/workframe-ui/public/assets/{pie-LRSECV5Y-DdMFsfR8.js → pie-LRSECV5Y-CdqoWesP.js} +1 -1
  167. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-CEPHFBU1.js → pieDiagram-4H26LBE5-kGs-VfXd.js} +1 -1
  168. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-_RF9_86k.js → quadrantDiagram-W4KKPZXB-D9D8gQw5.js} +1 -1
  169. package/workframe-ui/public/assets/{radar-GUYGQ44K-Cb-YfdaA.js → radar-GUYGQ44K-d4zMZpQS.js} +1 -1
  170. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DBVOxmBn.js → requirementDiagram-4Y6WPE33-BI7EQ9zc.js} +1 -1
  171. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-BTpxsGUA.js → sankeyDiagram-5OEKKPKP-Bcz19jQB.js} +1 -1
  172. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-DHP_Bq3l.js → sequenceDiagram-3UESZ5HK-iQiCEfYp.js} +1 -1
  173. package/workframe-ui/public/assets/{src-CY_By-r7.js → src-D6mvlP96.js} +1 -1
  174. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BVtp_LPR.js → stateDiagram-AJRCARHV-CxaTXs02.js} +1 -1
  175. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CXikj9xi.js +1 -0
  176. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-D38HJhop.js → timeline-definition-PNZ67QCA-Bej-EAnf.js} +1 -1
  177. package/workframe-ui/public/assets/{treeView-BLDUP644-CFCG3Sc_.js → treeView-BLDUP644-C7PnnzwX.js} +1 -1
  178. package/workframe-ui/public/assets/{treemap-LRROVOQU-BeiMs_dF.js → treemap-LRROVOQU-CqISLmkO.js} +1 -1
  179. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CKMeT6PW.js → vennDiagram-CIIHVFJN-eJMTTEW-.js} +1 -1
  180. package/workframe-ui/public/assets/{wardley-L42UT6IY-BljDy8ph.js → wardley-L42UT6IY-D4sMroBF.js} +1 -1
  181. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DEE8D94J.js → wardleyDiagram-YWT4CUSO-DC3DCUs7.js} +1 -1
  182. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-BRaAWV_g.js → xychartDiagram-2RQKCTM6-BeMaM6Aq.js} +1 -1
  183. package/workframe-ui/public/index.html +7 -6
  184. package/workframe-ui/public/workframe-build.json +5 -0
  185. package/workframe-ui/public/assets/architecture-7EHR7CIX-CvhbXEvt.js +0 -1
  186. package/workframe-ui/public/assets/channel-D63s_yN9.js +0 -1
  187. package/workframe-ui/public/assets/chunk-LZXEDZCA-CsRdBqUB.js +0 -2
  188. package/workframe-ui/public/assets/chunk-QZHKN3VN-Dj_Yxhgo.js +0 -1
  189. package/workframe-ui/public/assets/chunk-WU5MYG2G-AFchshb6.js +0 -1
  190. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-Opj8t01I.js +0 -1
  191. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-Opj8t01I.js +0 -1
  192. package/workframe-ui/public/assets/eventmodeling-FCH6USID-LQOYL50n.js +0 -1
  193. package/workframe-ui/public/assets/index-B08aShJy.css +0 -1
  194. package/workframe-ui/public/assets/index-DCF3W3G_.js +0 -129
  195. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CvJhS_BJ.js +0 -1
@@ -0,0 +1,67 @@
1
+ """Local bootstrap must require a real owner email — no synthetic placeholders."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+ from unittest.mock import patch
9
+
10
+ API_DIR = Path(__file__).resolve().parent
11
+ if str(API_DIR) not in sys.path:
12
+ sys.path.insert(0, str(API_DIR))
13
+
14
+ os.environ.setdefault("WORKFRAME_API_DATA_DIR", str(API_DIR / ".tmp-test-data"))
15
+ os.environ.setdefault("HERMES_DATA", str(API_DIR / ".tmp-test-hermes"))
16
+ os.environ.setdefault("DEV_LOCAL_UNSAFE", "true")
17
+ os.environ["DEPLOYMENT_MODE"] = "single_user_local"
18
+
19
+ import server # noqa: E402
20
+
21
+
22
+ class _CaptureHandler(server.Handler):
23
+ captured: tuple[int, dict] | None = None
24
+
25
+ def _json(self, status: int, payload: dict, **kwargs) -> None: # type: ignore[override]
26
+ type(self).captured = (status, payload)
27
+
28
+
29
+ def test_local_bootstrap_rejects_missing_email() -> None:
30
+ handler = _CaptureHandler.__new__(_CaptureHandler)
31
+ with patch.object(server, "DEPLOYMENT_MODE", "single_user_local"), patch.object(
32
+ server, "_install_window_open", return_value=True,
33
+ ):
34
+ handler._route_post_auth_local_bootstrap({})
35
+ assert _CaptureHandler.captured is not None
36
+ status, body = _CaptureHandler.captured
37
+ assert status == 400
38
+ assert body.get("error") == "email_required"
39
+
40
+
41
+ def test_local_bootstrap_uses_submitted_email() -> None:
42
+ handler = _CaptureHandler.__new__(_CaptureHandler)
43
+ with patch.object(server, "DEPLOYMENT_MODE", "single_user_local"), patch.object(
44
+ server, "_install_window_open", return_value=True,
45
+ ), patch.object(
46
+ server._zk,
47
+ "create_session_for_email",
48
+ return_value={"user_id": "u-test", "session_id": "s-test", "refresh_token": "r-test"},
49
+ ) as create_session, patch.object(handler, "_ensure_user"), patch.object(
50
+ handler, "_first_owner_bootstrap",
51
+ ), patch.object(server, "_session_profile_payload", return_value={"ok": True}), patch.object(
52
+ server, "_session_cookie_secure", return_value=False,
53
+ ), patch.object(
54
+ server._zk, "session_cookie_value", return_value="cookie=test",
55
+ ):
56
+ handler._route_post_auth_local_bootstrap(
57
+ {"email": "Owner@Example.com", "display_name": "Owner"},
58
+ )
59
+ create_session.assert_called_once_with("owner@example.com")
60
+ assert _CaptureHandler.captured is not None
61
+ assert _CaptureHandler.captured[0] == 200
62
+
63
+
64
+ if __name__ == "__main__":
65
+ test_local_bootstrap_rejects_missing_email()
66
+ test_local_bootstrap_uses_submitted_email()
67
+ print("ok")
@@ -0,0 +1,35 @@
1
+ """WF-032 mention_helpers self-check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
9
+
10
+ import mention_helpers # noqa: E402
11
+
12
+
13
+ def test_mention_handle() -> None:
14
+ assert mention_helpers.mention_handle("Alice Smith") == "alicesmith"
15
+ assert mention_helpers.mention_handle("", "alice@example.com") == "alice"
16
+
17
+
18
+ def test_parse_room_mentions() -> None:
19
+ agents = [{"id": "a1", "slug": "dev", "display_name": "Dev Agent"}]
20
+ result = mention_helpers.parse_room_mentions("@dev please review", agents, [])
21
+ assert len(result["agents"]) == 1
22
+ assert result["agents"][0]["slug"] == "dev"
23
+ assert result["users"] == []
24
+
25
+
26
+ def test_parse_mentions() -> None:
27
+ rows = mention_helpers.parse_mentions("hello @architect and @dev")
28
+ assert [r["slug"] for r in rows] == ["architect", "dev"]
29
+
30
+
31
+ if __name__ == "__main__":
32
+ test_mention_handle()
33
+ test_parse_room_mentions()
34
+ test_parse_mentions()
35
+ print("test_mention_helpers: ok")
@@ -0,0 +1,26 @@
1
+ """WF-032 mention_invoke self-check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
9
+
10
+ import mention_invoke # noqa: E402
11
+
12
+
13
+ def test_invoke_mention_missing_agent() -> None:
14
+ """No-op when agent row lacks slug/id."""
15
+ mention_invoke._invoke_room_agent_mention(
16
+ "room-1",
17
+ "ws-1",
18
+ {"slug": "", "id": ""},
19
+ "",
20
+ "msg-1",
21
+ )
22
+
23
+
24
+ if __name__ == "__main__":
25
+ test_invoke_mention_missing_agent()
26
+ print("test_mention_invoke: ok")
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env python3
2
+ """Regression tests for model save/provider surface (WF-034).
3
+
4
+ Run: python services/workframe-api/test_model_surface_consistency.py
5
+ """
6
+ import importlib.util
7
+ import os
8
+ import sys
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+ ROOT = Path(__file__).resolve().parent
13
+ tmpdir = tempfile.mkdtemp(prefix="wf-model-surface-")
14
+ os.environ["WORKFRAME_AGENTS_DIR"] = tmpdir
15
+ os.environ["WORKFRAME_PROJECT"] = "ModelSurfaceTest"
16
+ os.environ["NATIVE_PROFILE"] = "surface-test-agent"
17
+
18
+ spec = importlib.util.spec_from_file_location("wf_server_model_surface", ROOT / "server.py")
19
+ server = importlib.util.module_from_spec(spec)
20
+ sys.modules["wf_server_model_surface"] = server
21
+ spec.loader.exec_module(server)
22
+ sys.modules["server"] = server # ponytail: extracted modules resolve _srv() to this instance
23
+
24
+ PROF = "surface-test-agent"
25
+ prof_dir = server._profile_dir(PROF)
26
+ prof_dir.mkdir(parents=True, exist_ok=True)
27
+ (prof_dir / "config.yaml").write_text(
28
+ "model:\n default: gpt-5.4-medium\n provider: openai-codex\n",
29
+ encoding="utf-8",
30
+ )
31
+ cfg_path = prof_dir / "config.yaml"
32
+ before = cfg_path.read_bytes()
33
+
34
+ # A: GET models must not mutate config.yaml
35
+ server.hermes_models(PROF, user_id="", workspace_id="")
36
+ assert cfg_path.read_bytes() == before, "hermes_models must not rewrite config on read"
37
+
38
+ # B: reconcile idempotent when routing already matches codex oauth
39
+ assert server._profile_routing_matches_billing(PROF, "codex")
40
+ assert server._reconcile_profile_llm_for_user(PROF, "user-1", "") is False
41
+
42
+ # C: billing resolution honors prefer= when model exists in that catalog
43
+ connected = {"codex", "openrouter"}
44
+ assert server._resolve_billing_provider_for_model(
45
+ "gpt-5.4-medium", connected, prefer="codex",
46
+ ) == "codex"
47
+ assert server._resolve_billing_provider_for_model(
48
+ "openrouter/owl-alpha", connected, prefer="openrouter",
49
+ ) == "openrouter"
50
+ assert server._resolve_billing_provider_for_model(
51
+ "openai-codex/gpt-5.4-mini", {"codex", "openrouter"},
52
+ ) == "codex"
53
+
54
+ # D: set with explicit billing_provider returns billing id in response
55
+ applied = server.hermes_model_set(
56
+ PROF, "gpt-5.4-medium", user_id="", workspace_id="", billing_provider="codex",
57
+ )
58
+ assert applied.get("ok"), applied
59
+ assert applied.get("billing_provider") == "codex"
60
+ assert applied.get("provider") == "codex"
61
+
62
+ # E: reconcile picks model billing over stale openrouter proxy yaml
63
+ (prof_dir / "config.yaml").write_text(
64
+ "model:\n default: openai-codex/gpt-5.4-mini\n provider: custom\n"
65
+ " base_url: http://workframe-api:8080/internal/llm/openrouter/v1\n",
66
+ encoding="utf-8",
67
+ )
68
+ _orig_picker = server._user_llm_providers_for_picker
69
+ _orig_can = server._user_can_use_llm
70
+ _orig_sync = server._sync_oauth_llm_to_profile
71
+ server._user_llm_providers_for_picker = lambda uid: {"codex"} if uid == "user-1" else set()
72
+ server._user_can_use_llm = lambda uid, ws, prov: uid == "user-1" and prov == "codex"
73
+ server._sync_oauth_llm_to_profile = lambda *a, **k: None
74
+ try:
75
+ assert server._reconcile_profile_llm_for_user(PROF, "user-1", "") is True
76
+ block = server._read_model_block(PROF)
77
+ assert block.get("provider") == "openai-codex"
78
+ assert block.get("default") == "gpt-5.4-mini"
79
+ assert not block.get("base_url")
80
+ finally:
81
+ server._user_llm_providers_for_picker = _orig_picker
82
+ server._user_can_use_llm = _orig_can
83
+ server._sync_oauth_llm_to_profile = _orig_sync
84
+
85
+ # F: runtime disk wins over template in _read_model_block
86
+ runtime_prof = "u-test-user-surface-test-agent"
87
+ runtime_dir = server._profile_dir(runtime_prof)
88
+ runtime_dir.mkdir(parents=True, exist_ok=True)
89
+ (runtime_dir / "config.yaml").write_text(
90
+ "model:\n default: gpt-5.4-mini\n provider: openai-codex\n",
91
+ encoding="utf-8",
92
+ )
93
+ block = server._read_model_block(runtime_prof)
94
+ assert block.get("default") == "gpt-5.4-mini", block
95
+ assert block.get("provider") == "openai-codex", block
96
+
97
+ # G: template model save mirrors to runtime (same model id, not stale template re-read)
98
+ runtime_prof = "u-test-user2-surface-test-agent"
99
+ runtime_dir = server._profile_dir(runtime_prof)
100
+ runtime_dir.mkdir(parents=True, exist_ok=True)
101
+ (runtime_dir / "config.yaml").write_text(
102
+ "model:\n default: gpt-5.4-medium\n provider: openai-codex\n",
103
+ encoding="utf-8",
104
+ )
105
+ _orig_restart = server._restart_runtime_profile_gateway
106
+ server._restart_runtime_profile_gateway = lambda *a, **k: None
107
+ try:
108
+ applied = server.hermes_model_set(
109
+ PROF, "gpt-5.4-mini", user_id="test-user2", workspace_id="", billing_provider="codex",
110
+ )
111
+ finally:
112
+ server._restart_runtime_profile_gateway = _orig_restart
113
+ assert applied.get("ok"), applied
114
+ runtime_block = server._parse_model_block_from_disk(runtime_prof)
115
+ assert runtime_block.get("default") == "gpt-5.4-mini", runtime_block
116
+
117
+ print("model surface consistency self-check ok")
@@ -0,0 +1,41 @@
1
+ """WF-032 oauth_pending store/take self-check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ import tempfile
8
+ from pathlib import Path
9
+
10
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
11
+
12
+ import oauth_pending # noqa: E402
13
+ import server # noqa: E402
14
+
15
+
16
+ def test_store_take_round_trip() -> None:
17
+ with tempfile.TemporaryDirectory() as tmp:
18
+ os.environ["WORKFRAME_API_DATA_DIR"] = tmp
19
+ server.DATA_DIR = Path(tmp)
20
+ server.AUTH_DB_PATH = Path(tmp) / "auth.db"
21
+ server._ensure_workframe_db_schema()
22
+ oauth_pending.store_pending("state-1", "user-1", "github", "verifier-abc", "ws-1")
23
+ row = oauth_pending.take_pending("state-1")
24
+ assert row is not None
25
+ assert row["user_id"] == "user-1"
26
+ assert row["provider"] == "github"
27
+ assert row["code_verifier"] == "verifier-abc"
28
+ assert oauth_pending.take_pending("state-1") is None
29
+
30
+
31
+ def test_pkce_pair() -> None:
32
+ verifier, challenge = oauth_pending.pkce_pair()
33
+ assert len(verifier) > 20
34
+ assert len(challenge) > 20
35
+ assert verifier != challenge
36
+
37
+
38
+ if __name__ == "__main__":
39
+ test_pkce_pair()
40
+ test_store_take_round_trip()
41
+ print("test_oauth_pending: ok")
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env python3
2
+ """Coalesce corrupt duplicate model blocks + profile_config_yaml writer (WF-033)."""
3
+ import importlib.util
4
+ import os
5
+ import sys
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ import yaml
10
+
11
+ ROOT = Path(__file__).resolve().parent
12
+ os.environ.setdefault("WORKFRAME_AGENTS_DIR", tempfile.mkdtemp(prefix="wf-yaml-test-"))
13
+ os.environ.setdefault("WORKFRAME_PROJECT", "YamlTest")
14
+ os.environ.setdefault("NATIVE_PROFILE", "yaml-test-agent")
15
+
16
+ spec = importlib.util.spec_from_file_location("wf_server_yaml_test", ROOT / "server.py")
17
+ server = importlib.util.module_from_spec(spec)
18
+ sys.modules["wf_server_yaml_test"] = server
19
+ spec.loader.exec_module(server)
20
+
21
+ import profile_config_yaml # noqa: E402
22
+
23
+ BROKEN = """model:
24
+ default: gpt-5.4-medium
25
+ provider: openai-codex
26
+ model:
27
+ default: gpt-5.4-medium
28
+ - provider: openai-codex
29
+ fallback_providers:
30
+ - provider: openai-codex
31
+ model: gpt-5.4-mini
32
+ toolsets:
33
+ """
34
+
35
+ prof = "yaml-test-agent"
36
+ prof_dir = server._profile_dir(prof)
37
+ prof_dir.mkdir(parents=True, exist_ok=True)
38
+ cfg_path = prof_dir / "config.yaml"
39
+ cfg_path.write_text(BROKEN, encoding="utf-8")
40
+
41
+ server._normalize_profile_config_yaml(prof)
42
+ fixed = cfg_path.read_text(encoding="utf-8")
43
+ assert fixed.count("\nmodel:\n") + fixed.count("model:\n") <= 2, fixed
44
+ assert " - provider:" not in fixed.split("fallback_providers:")[0], fixed
45
+ parsed = yaml.safe_load(fixed)
46
+ assert isinstance(parsed.get("model"), dict), parsed
47
+ assert parsed["model"]["default"] == "gpt-5.4-medium"
48
+ assert parsed["model"]["provider"] == "openai-codex"
49
+ assert len(parsed.get("fallback_providers") or []) == 1
50
+ assert "toolsets" in parsed
51
+
52
+ # Round-trip via update_model_surface preserves non-model keys
53
+ round_prof = "yaml-round-trip"
54
+ round_dir = server._profile_dir(round_prof)
55
+ round_dir.mkdir(parents=True, exist_ok=True)
56
+ round_path = round_dir / "config.yaml"
57
+ round_path.write_text(
58
+ "# picker note\nmodel:\n default: old-model\n provider: openrouter\ncron: []\n",
59
+ encoding="utf-8",
60
+ )
61
+ profile_config_yaml.update_model_surface(
62
+ round_path, default="new-model", provider="custom", base_url="http://proxy/v1",
63
+ )
64
+ rt = yaml.safe_load(round_path.read_text(encoding="utf-8"))
65
+ assert rt["model"]["default"] == "new-model"
66
+ assert rt["model"]["provider"] == "custom"
67
+ assert rt["model"]["base_url"] == "http://proxy/v1"
68
+ assert rt.get("cron") == []
69
+ assert "# picker note" not in round_path.read_text(encoding="utf-8") # comments not preserved
70
+
71
+ # Missing file: writer creates parent dirs + config
72
+ missing_prof = "yaml-missing"
73
+ missing_dir = server._profile_dir(missing_prof)
74
+ missing_path = missing_dir / "config.yaml"
75
+ if missing_path.is_file():
76
+ missing_path.unlink()
77
+ profile_config_yaml.update_model_surface(
78
+ missing_path, default="gpt-5.4-mini", provider="openai-codex",
79
+ )
80
+ assert missing_path.is_file()
81
+ loaded = yaml.safe_load(missing_path.read_text(encoding="utf-8"))
82
+ assert loaded["model"]["default"] == "gpt-5.4-mini"
83
+
84
+ # Coalesce flag drops duplicate blocks
85
+ dup_path = round_dir / "dup.yaml"
86
+ dup_path.write_text(BROKEN, encoding="utf-8")
87
+ profile_config_yaml.update_model_surface(
88
+ dup_path,
89
+ default="gpt-5.4-medium",
90
+ provider="openai-codex",
91
+ fallback_chain=[{"provider": "openai-codex", "model": "gpt-5.4-mini"}],
92
+ coalesce=True,
93
+ )
94
+ dup_text = dup_path.read_text(encoding="utf-8")
95
+ assert dup_text.count("\nmodel:\n") + dup_text.count("model:\n") <= 2, dup_text
96
+ dup_parsed = yaml.safe_load(dup_text)
97
+ assert dup_parsed["model"]["default"] == "gpt-5.4-medium"
98
+
99
+ print("profile model yaml coalesce self-check ok")
@@ -0,0 +1,52 @@
1
+ """WF-032 provider_bindings pure helpers self-check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
9
+
10
+ import provider_bindings # noqa: E402
11
+
12
+
13
+ def test_hermes_oauth_auth_keys() -> None:
14
+ keys = provider_bindings._hermes_oauth_auth_keys("openai-codex")
15
+ assert "openai-codex" in keys
16
+ assert "codex" in keys
17
+ assert "openai_codex" in keys
18
+
19
+
20
+ def test_auth_json_has_oauth_material() -> None:
21
+ pool_only = {
22
+ "version": 1,
23
+ "providers": {},
24
+ "credential_pool": {"openai-codex": [{"id": "x", "auth_type": "oauth"}]},
25
+ }
26
+ assert provider_bindings._auth_json_has_oauth_material(pool_only)
27
+
28
+
29
+ def test_merge_oauth_auth_into_profile() -> None:
30
+ pool_only = {
31
+ "version": 1,
32
+ "providers": {},
33
+ "credential_pool": {"openai-codex": [{"id": "x", "auth_type": "oauth"}]},
34
+ }
35
+ merged: dict = {"version": 1, "providers": {}, "credential_pool": {}}
36
+ assert provider_bindings._merge_oauth_auth_into_profile(merged, pool_only, "openai-codex")
37
+ assert merged["credential_pool"].get("openai-codex")
38
+
39
+
40
+ def test_parse_device_oauth_log() -> None:
41
+ text = "Open this URL\n https://auth.openai.com/device\nEnter this code\n ABCD-1234"
42
+ parsed = provider_bindings._parse_device_oauth_log(text)
43
+ assert parsed["verification_uri"] and "openai.com" in parsed["verification_uri"]
44
+ assert parsed["user_code"] == "ABCD-1234"
45
+
46
+
47
+ if __name__ == "__main__":
48
+ test_hermes_oauth_auth_keys()
49
+ test_auth_json_has_oauth_material()
50
+ test_merge_oauth_auth_into_profile()
51
+ test_parse_device_oauth_log()
52
+ print("test_provider_bindings: ok")
@@ -0,0 +1,28 @@
1
+ """WF-032 provider_catalog self-check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
9
+
10
+ import provider_catalog # noqa: E402
11
+
12
+
13
+ def test_catalog_lookup() -> None:
14
+ gh = provider_catalog.catalog_provider("github")
15
+ assert gh is not None
16
+ assert gh.get("connect_mode") == "oauth"
17
+ assert provider_catalog.catalog_provider_for_llm("openai-codex") is not None
18
+
19
+
20
+ def test_user_only() -> None:
21
+ assert provider_catalog.provider_user_only("github")
22
+ assert not provider_catalog.provider_user_only("openrouter")
23
+
24
+
25
+ if __name__ == "__main__":
26
+ test_catalog_lookup()
27
+ test_user_only()
28
+ print("test_provider_catalog: ok")
@@ -0,0 +1,41 @@
1
+ """ponytail self-check: session-less GET policy (WF-031).
2
+
3
+ Run: python services/workframe-api/test_public_routes.py
4
+ """
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
9
+
10
+ import server # noqa: E402
11
+
12
+ ALWAYS = "/api/health"
13
+ DATA = "/api/snapshot"
14
+ META = "/api/meta"
15
+
16
+
17
+ def allowed(path: str, mode: str, dev: bool = False) -> bool:
18
+ return server._sessionless_get_allowed(path, deployment_mode=mode, dev_local_unsafe=dev)
19
+
20
+
21
+ # Always-public routes work in every deployment mode.
22
+ assert allowed(META, "public_multi_user")
23
+ assert allowed(ALWAYS, "trusted_team")
24
+ assert allowed("/api/public/branding/logo", "public_multi_user")
25
+
26
+ # Data routes: blocked for multi-user modes without DEV_LOCAL_UNSAFE.
27
+ assert not allowed(DATA, "public_multi_user")
28
+ assert not allowed(DATA, "trusted_team")
29
+ assert not allowed("/api/chat/messages", "public_multi_user")
30
+ assert not allowed("/api/files/list", "trusted_team")
31
+
32
+ # Data routes: allowed for single_user_local or dev unsafe.
33
+ assert allowed(DATA, "single_user_local")
34
+ assert allowed(DATA, "public_multi_user", dev=True)
35
+ assert allowed("/api/board", "single_user_local")
36
+
37
+ # _is_public_get composes static/UI paths.
38
+ assert server._is_public_get("/")
39
+ assert server._is_public_get("/assets/app.js")
40
+
41
+ print("public routes self-check ok")