create-workframe 0.1.12 → 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 (188) 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 +2 -2
  5. package/package.json +1 -1
  6. package/scripts/bundle-workframe-ui.mjs +9 -0
  7. package/scripts/sync-canonical-to-package.mjs +1 -0
  8. package/scripts/verify-public-deploy.sh +65 -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/package.json +2 -2
  51. package/workframe-api/profile_api_lifecycle.py +66 -0
  52. package/workframe-api/profile_gateway.py +436 -0
  53. package/workframe-api/provider_bindings.py +673 -0
  54. package/workframe-api/provider_bootstrap.py +347 -0
  55. package/workframe-api/provider_catalog.py +180 -0
  56. package/workframe-api/rooms.py +1613 -0
  57. package/workframe-api/route_registry.py +331 -191
  58. package/workframe-api/run-typecheck.mjs +2 -1
  59. package/workframe-api/run_authority.py +287 -0
  60. package/workframe-api/run_ledger.py +470 -0
  61. package/workframe-api/run_surface_wiring.py +344 -0
  62. package/workframe-api/runtime_cohort.py +752 -0
  63. package/workframe-api/runtime_tokens.py +127 -0
  64. package/workframe-api/server.py +1453 -19513
  65. package/workframe-api/snapshot_feed.py +49 -0
  66. package/workframe-api/stack_config.py +33 -1
  67. package/workframe-api/supervisor_client.py +154 -0
  68. package/workframe-api/test_api_meta_build_stamp.py +34 -0
  69. package/workframe-api/test_cell_authority.py +79 -0
  70. package/workframe-api/test_chat_bind.py +48 -0
  71. package/workframe-api/test_credential_lease_matrix.py +171 -0
  72. package/workframe-api/test_credential_resolve.py +51 -0
  73. package/workframe-api/test_credential_store.py +27 -0
  74. package/workframe-api/test_dogfood_flows.py +346 -0
  75. package/workframe-api/test_domain_entities.py +152 -0
  76. package/workframe-api/test_exception_hygiene.py +12 -0
  77. package/workframe-api/test_hermes_admin.py +72 -0
  78. package/workframe-api/test_install_wizard_resume.py +78 -0
  79. package/workframe-api/test_local_bootstrap.py +67 -0
  80. package/workframe-api/test_mention_helpers.py +35 -0
  81. package/workframe-api/test_mention_invoke.py +26 -0
  82. package/workframe-api/test_model_surface_consistency.py +1 -0
  83. package/workframe-api/test_oauth_pending.py +41 -0
  84. package/workframe-api/test_provider_bindings.py +52 -0
  85. package/workframe-api/test_provider_catalog.py +28 -0
  86. package/workframe-api/test_route_registry.py +171 -35
  87. package/workframe-api/test_run_authority.py +112 -0
  88. package/workframe-api/test_run_ledger.py +157 -0
  89. package/workframe-api/test_run_surface_wiring.py +130 -0
  90. package/workframe-api/test_runtime_cohort.py +85 -0
  91. package/workframe-api/test_secure_mode_docker_boundary.py +39 -0
  92. package/workframe-api/test_server_reexports.py +99 -0
  93. package/workframe-api/test_stack_config_install_smtp.py +7 -0
  94. package/workframe-api/turn_credentials.py +28 -13
  95. package/workframe-api/turn_overlay.py +715 -0
  96. package/workframe-api/updates.py +16 -1
  97. package/workframe-api/user_prefs.py +387 -0
  98. package/workframe-api/wf032_extract_remaining_handlers.py +417 -0
  99. package/workframe-api/workspace_bootstrap.py +536 -0
  100. package/workframe-api/workspace_files.py +344 -0
  101. package/workframe-api/workspace_messaging.py +206 -0
  102. package/workframe-api/zk_auth.py +3 -2
  103. package/workframe-supervisor/server.py +10 -1
  104. package/workframe-supervisor/test_supervisor_negative.py +75 -0
  105. package/workframe-ui/public/assets/{arc-B0OFRGmJ.js → arc-aUUffclm.js} +1 -1
  106. package/workframe-ui/public/assets/architecture-7EHR7CIX-BSUP4S8J.js +1 -0
  107. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-DeBNltHS.js → architectureDiagram-3BPJPVTR-DU6oaqIb.js} +1 -1
  108. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-BDhCHu7I.js → blockDiagram-GPEHLZMM-D2p8BU6-.js} +1 -1
  109. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-olcDYvtI.js → c4Diagram-AAUBKEIU-ouwyPmTJ.js} +1 -1
  110. package/workframe-ui/public/assets/channel-DOunZZ0X.js +1 -0
  111. package/workframe-ui/public/assets/{chunk-2J33WTMH-D0obCBn_.js → chunk-2J33WTMH-DsE7U5dj.js} +1 -1
  112. package/workframe-ui/public/assets/{chunk-3OPIFGDE-DX93f-ZZ.js → chunk-3OPIFGDE-DM0kkZ9h.js} +1 -1
  113. package/workframe-ui/public/assets/{chunk-4BX2VUAB-BX9B5wUs.js → chunk-4BX2VUAB-CQ8mAyXp.js} +1 -1
  114. package/workframe-ui/public/assets/{chunk-55IACEB6-C4DGc6RO.js → chunk-55IACEB6-DN1BDtbH.js} +1 -1
  115. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-Crlja9Lf.js → chunk-5ZQYHXKU-BzK2KqOt.js} +1 -1
  116. package/workframe-ui/public/assets/{chunk-727SXJPM-JYSm3szI.js → chunk-727SXJPM-C3AxtOi9.js} +1 -1
  117. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-_KslxVEl.js → chunk-AQP2D5EJ-4mVwEFuD.js} +1 -1
  118. package/workframe-ui/public/assets/{chunk-BSJP7CBP-CpTO-jU8.js → chunk-BSJP7CBP-23kN2q0A.js} +1 -1
  119. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-JGUkCmbI.js → chunk-CSCIHK7Q-CPBKVW-L.js} +2 -2
  120. package/workframe-ui/public/assets/{chunk-FMBD7UC4-BscBwGaC.js → chunk-FMBD7UC4-CKqcYq7K.js} +1 -1
  121. package/workframe-ui/public/assets/{chunk-KSCS5N6A-5ZTZ0bpX.js → chunk-KSCS5N6A-JRQnhxQE.js} +1 -1
  122. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-DxvitYbW.js → chunk-L5ZTLDWV-BLZrdIwa.js} +1 -1
  123. package/workframe-ui/public/assets/chunk-LZXEDZCA-DisG0XJT.js +2 -0
  124. package/workframe-ui/public/assets/{chunk-ND2GUHAM-Cayl0iV9.js → chunk-ND2GUHAM-ClAOxArS.js} +1 -1
  125. package/workframe-ui/public/assets/{chunk-NZK2D7GU-_7dyBR5G.js → chunk-NZK2D7GU-XUE1aNkm.js} +1 -1
  126. package/workframe-ui/public/assets/{chunk-O5CBEL6O--xTpD0yi.js → chunk-O5CBEL6O-DDN-Ifon.js} +1 -1
  127. package/workframe-ui/public/assets/chunk-QZHKN3VN-EVQ0VMd5.js +1 -0
  128. package/workframe-ui/public/assets/chunk-WU5MYG2G-k_-ZsLOS.js +1 -0
  129. package/workframe-ui/public/assets/{chunk-XPW4576I-CwxJQaeK.js → chunk-XPW4576I-B4_iYZ6r.js} +1 -1
  130. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-W9WVQsby.js +1 -0
  131. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-W9WVQsby.js +1 -0
  132. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-CCspENYv.js → cose-bilkent-S5V4N54A-Cy1HhS7j.js} +1 -1
  133. package/workframe-ui/public/assets/{dagre-BM42HDAG-Dv8V6R60.js → dagre-BM42HDAG-Bjal81Ie.js} +1 -1
  134. package/workframe-ui/public/assets/{diagram-2AECGRRQ-Da48hFPT.js → diagram-2AECGRRQ-v5Gdvzp_.js} +1 -1
  135. package/workframe-ui/public/assets/{diagram-5GNKFQAL-dTihc7-3.js → diagram-5GNKFQAL-DuyRdfmY.js} +1 -1
  136. package/workframe-ui/public/assets/{diagram-KO2AKTUF-D_Jqu699.js → diagram-KO2AKTUF-WwWCGbIW.js} +1 -1
  137. package/workframe-ui/public/assets/{diagram-LMA3HP47-Bt8PtEaZ.js → diagram-LMA3HP47-DTYwZALT.js} +1 -1
  138. package/workframe-ui/public/assets/{diagram-OG6HWLK6-BtmPjlh0.js → diagram-OG6HWLK6-B46kBIqE.js} +1 -1
  139. package/workframe-ui/public/assets/{dist-Cz2turIT.js → dist-mQWmB3z6.js} +1 -1
  140. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-BxMGCflX.js → erDiagram-TEJ5UH35-__40xO-y.js} +1 -1
  141. package/workframe-ui/public/assets/eventmodeling-FCH6USID-Cm1uRTxU.js +1 -0
  142. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-ivyroIZt.js → flowDiagram-I6XJVG4X-Cwj6dVUz.js} +1 -1
  143. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-B9llwShx.js → ganttDiagram-6RSMTGT7-D_IOSwAS.js} +1 -1
  144. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BFQHk4bw.js → gitGraph-WXDBUCRP-C1vGecee.js} +1 -1
  145. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-ursegLbc.js → gitGraphDiagram-PVQCEYII-DMTxFJxn.js} +1 -1
  146. package/workframe-ui/public/assets/index-BdiM4-lI.js +130 -0
  147. package/workframe-ui/public/assets/index-Fnad47vV.css +1 -0
  148. package/workframe-ui/public/assets/{info-J43DQDTF-DQpifAsB.js → info-J43DQDTF-CwjmyPmD.js} +1 -1
  149. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-C16XP2Q7.js → infoDiagram-5YYISTIA-BGzh7ZL2.js} +1 -1
  150. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-CiJoz7rP.js → ishikawaDiagram-YF4QCWOH-BLXdnR_5.js} +1 -1
  151. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CVwEvvUL.js → journeyDiagram-JHISSGLW-BK3behMe.js} +1 -1
  152. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-CWQ6hX5i.js → kanban-definition-UN3LZRKU-Bx172cCl.js} +1 -1
  153. package/workframe-ui/public/assets/{line-CgQsmU35.js → line-BTCEHb7l.js} +1 -1
  154. package/workframe-ui/public/assets/{linear-Bxbc77dL.js → linear-CkbydpnK.js} +1 -1
  155. package/workframe-ui/public/assets/{mermaid-parser.core-C_jMeF0a.js → mermaid-parser.core-DvrtArpk.js} +2 -2
  156. package/workframe-ui/public/assets/{mermaid.core-4RKV9MZP.js → mermaid.core-yj_1x_qj.js} +3 -3
  157. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-DZ7y7Sz0.js → mindmap-definition-RKZ34NQL-BXbMltQ0.js} +1 -1
  158. package/workframe-ui/public/assets/{packet-YPE3B663-B9h2I7Vq.js → packet-YPE3B663-9pVCBQ7S.js} +1 -1
  159. package/workframe-ui/public/assets/{pie-LRSECV5Y-B2mP5uHm.js → pie-LRSECV5Y-CdqoWesP.js} +1 -1
  160. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-DyYKyKiP.js → pieDiagram-4H26LBE5-kGs-VfXd.js} +1 -1
  161. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-CL_6nej-.js → quadrantDiagram-W4KKPZXB-D9D8gQw5.js} +1 -1
  162. package/workframe-ui/public/assets/{radar-GUYGQ44K-CNKNDQ7S.js → radar-GUYGQ44K-d4zMZpQS.js} +1 -1
  163. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-eWU_mhFa.js → requirementDiagram-4Y6WPE33-BI7EQ9zc.js} +1 -1
  164. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DrjcXvEQ.js → sankeyDiagram-5OEKKPKP-Bcz19jQB.js} +1 -1
  165. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-BHWR1xTJ.js → sequenceDiagram-3UESZ5HK-iQiCEfYp.js} +1 -1
  166. package/workframe-ui/public/assets/{src-DfJB8kV1.js → src-D6mvlP96.js} +1 -1
  167. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BGJbSd3I.js → stateDiagram-AJRCARHV-CxaTXs02.js} +1 -1
  168. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CXikj9xi.js +1 -0
  169. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DkwoCjiZ.js → timeline-definition-PNZ67QCA-Bej-EAnf.js} +1 -1
  170. package/workframe-ui/public/assets/{treeView-BLDUP644-GxfhiqoW.js → treeView-BLDUP644-C7PnnzwX.js} +1 -1
  171. package/workframe-ui/public/assets/{treemap-LRROVOQU-DxrOsars.js → treemap-LRROVOQU-CqISLmkO.js} +1 -1
  172. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-BsWhpUfr.js → vennDiagram-CIIHVFJN-eJMTTEW-.js} +1 -1
  173. package/workframe-ui/public/assets/{wardley-L42UT6IY-CrFZWMHS.js → wardley-L42UT6IY-D4sMroBF.js} +1 -1
  174. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DV8Xpf5I.js → wardleyDiagram-YWT4CUSO-DC3DCUs7.js} +1 -1
  175. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DqMqnwdZ.js → xychartDiagram-2RQKCTM6-BeMaM6Aq.js} +1 -1
  176. package/workframe-ui/public/index.html +7 -5
  177. package/workframe-ui/public/workframe-build.json +5 -0
  178. package/workframe-ui/public/assets/architecture-7EHR7CIX-Dwwi9yA0.js +0 -1
  179. package/workframe-ui/public/assets/channel-fNHbDpV4.js +0 -1
  180. package/workframe-ui/public/assets/chunk-LZXEDZCA-DMfdMUwz.js +0 -2
  181. package/workframe-ui/public/assets/chunk-QZHKN3VN-ewTgEQK8.js +0 -1
  182. package/workframe-ui/public/assets/chunk-WU5MYG2G-D4Tg-A0l.js +0 -1
  183. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CFAgBpEl.js +0 -1
  184. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CFAgBpEl.js +0 -1
  185. package/workframe-ui/public/assets/eventmodeling-FCH6USID-BIJTP5k-.js +0 -1
  186. package/workframe-ui/public/assets/index-8CuZDEIG.css +0 -1
  187. package/workframe-ui/public/assets/index-xS9lFekI.js +0 -129
  188. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CMLuNyeC.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")
@@ -19,6 +19,7 @@ spec = importlib.util.spec_from_file_location("wf_server_model_surface", ROOT /
19
19
  server = importlib.util.module_from_spec(spec)
20
20
  sys.modules["wf_server_model_surface"] = server
21
21
  spec.loader.exec_module(server)
22
+ sys.modules["server"] = server # ponytail: extracted modules resolve _srv() to this instance
22
23
 
23
24
  PROF = "surface-test-agent"
24
25
  prof_dir = server._profile_dir(PROF)
@@ -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,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")
@@ -12,17 +12,46 @@ sys.path.insert(0, str(ROOT))
12
12
 
13
13
  import route_registry # noqa: E402
14
14
 
15
- # Auth level coverage
15
+
16
+ def _handler_source_text() -> str:
17
+ """server.Handler bodies plus extracted handler_modules mixins (WF-032)."""
18
+ parts = [(ROOT / "server.py").read_text(encoding="utf-8")]
19
+ mods = ROOT / "handler_modules"
20
+ if mods.is_dir():
21
+ for path in sorted(mods.glob("*.py")):
22
+ if path.name == "__init__.py":
23
+ continue
24
+ parts.append(path.read_text(encoding="utf-8"))
25
+ return "\n".join(parts)
26
+
27
+
28
+ def _sample_path_from_label(label: str, pattern: re.Pattern[str] | None = None) -> str:
29
+ sample = (
30
+ label.replace("{id}", "sample-id")
31
+ .replace("{slug}", "sample-slug")
32
+ .replace("{token}", "sample-token")
33
+ .replace("{provider}", "github")
34
+ .replace("{cid}", "cred-1")
35
+ .replace("{path}", "docs/readme.md")
36
+ .replace("{kind}", "og")
37
+ )
38
+ if pattern is not None and not pattern.fullmatch(sample):
39
+ raise AssertionError(f"sample path does not match pattern: {sample} {label}")
40
+ return sample
41
+
42
+
43
+ # Auth level coverage (4-value enum)
16
44
  assert route_registry.resolve_auth_level("GET", "/api/health") == route_registry.AuthLevel.PUBLIC
17
45
  assert route_registry.resolve_auth_level("GET", "/api/meta") == route_registry.AuthLevel.PUBLIC
18
- assert route_registry.resolve_auth_level("GET", "/api/snapshot") == route_registry.AuthLevel.SINGLE_USER_GET
19
- assert route_registry.resolve_auth_level("POST", "/api/auth/start") == route_registry.AuthLevel.AUTH_FLOW
46
+ assert route_registry.resolve_auth_level("GET", "/api/snapshot") == route_registry.AuthLevel.SESSION
47
+ assert route_registry.resolve_auth_level("POST", "/api/auth/start") == route_registry.AuthLevel.PUBLIC
20
48
  assert route_registry.resolve_auth_level("GET", "/api/me") == route_registry.AuthLevel.SESSION
21
- assert route_registry.resolve_auth_level("GET", "/api/activity/detail") == route_registry.AuthLevel.SESSION
49
+ assert route_registry.resolve_auth_level("GET", "/api/admin/vault/status") == route_registry.AuthLevel.ROLE_OWNER_ADMIN
50
+ assert route_registry.resolve_auth_level("GET", "/api/workspace/default/rooms") == route_registry.AuthLevel.SESSION
22
51
 
23
- # Unknown /api paths require session (deny when anonymous)
24
- assert route_registry.resolve_auth_level("GET", "/api/not-a-real-handler") == route_registry.AuthLevel.SESSION
25
- assert not route_registry.authorize_request(
52
+ # Unknown /api paths are unregistered → None (404 at dispatch, not auth-gated)
53
+ assert route_registry.resolve_auth_level("GET", "/api/not-a-real-handler") is None
54
+ assert route_registry.authorize_request(
26
55
  "GET",
27
56
  "/api/not-a-real-handler",
28
57
  "",
@@ -33,39 +62,80 @@ assert not route_registry.authorize_request(
33
62
  attach_user=lambda _u: None,
34
63
  )
35
64
 
36
- # ROUTES table: auth tier matches resolve_auth_level
37
- auth_mismatch: list[str] = []
38
- for route in route_registry.ROUTES:
39
- resolved = route_registry.resolve_auth_level(route.method, route.path)
40
- if resolved != route.auth:
41
- auth_mismatch.append(f"{route.method} {route.path}: table={route.auth} resolved={resolved}")
42
- assert not auth_mismatch, f"route auth mismatch: {auth_mismatch}"
65
+ # Registered session route without session → 401
66
+ assert not route_registry.authorize_request(
67
+ "GET",
68
+ "/api/me",
69
+ "",
70
+ deployment_mode="trusted_team",
71
+ dev_local_unsafe=False,
72
+ install_window_open=False,
73
+ validate_session=lambda _s: None,
74
+ attach_user=lambda _u: None,
75
+ )
43
76
 
44
- # ROUTES handlers exist on server.Handler
45
- server_src = (ROOT / "server.py").read_text(encoding="utf-8")
77
+ # ROUTES table: dispatched routes have handlers on server.Handler (+ handler_modules)
78
+ handler_src = _handler_source_text()
46
79
  missing_handlers: list[str] = []
47
80
  for route in route_registry.ROUTES:
48
- if f"def {route.handler}(" not in server_src:
81
+ if not route.handler:
82
+ continue
83
+ if f"def {route.handler}(" not in handler_src:
49
84
  missing_handlers.append(f"{route.method} {route.path} -> {route.handler}")
50
85
  assert not missing_handlers, f"missing handler methods: {missing_handlers}"
51
86
 
52
- # Remaining if-chain GET paths still map to a known auth tier
87
+ # Every pattern-backed route has a handler and auth via label-derived sample path
88
+ missing_pattern_handlers: list[str] = []
89
+ missing_pattern_auth: list[str] = []
90
+ for rp in route_registry.ROUTE_PATTERNS:
91
+ if not rp.handler:
92
+ missing_pattern_handlers.append(f"{rp.method} {rp.label}")
93
+ continue
94
+ if f"def {rp.handler}(" not in handler_src:
95
+ missing_pattern_handlers.append(f"{rp.method} {rp.label} -> {rp.handler}")
96
+ sample = _sample_path_from_label(rp.label, rp.pattern)
97
+ if route_registry.lookup_auth(rp.method, sample) is None:
98
+ missing_pattern_auth.append(f"{rp.method} {sample}")
99
+ assert not missing_pattern_handlers, f"missing pattern handlers: {missing_pattern_handlers}"
100
+ assert not missing_pattern_auth, f"missing pattern auth: {missing_pattern_auth}"
101
+
102
+ # Pattern handlers must not re-dispatch with legacy startswith guards
103
+ pattern_method_blocks = re.findall(
104
+ r"(def (_route_pattern_\w+)\([^)]*\)[^:]*:.*?)(?=\n def |\Z)",
105
+ handler_src,
106
+ flags=re.DOTALL,
107
+ )
108
+ legacy_guards: list[str] = []
109
+ for block, name in pattern_method_blocks:
110
+ if 'path.startswith("/api/' in block or "path.startswith('/api/" in block:
111
+ legacy_guards.append(name)
112
+ assert not legacy_guards, f"legacy startswith guards in pattern handlers: {legacy_guards}"
113
+
114
+ # Every if-chain literal /api path must be registered with explicit auth
115
+ server_src = (ROOT / "server.py").read_text(encoding="utf-8")
53
116
  handler_paths = set(re.findall(r'if path == "(/api/[^"]+)"', server_src))
54
117
  handler_paths |= set(re.findall(r"if path == \'(/api/[^\']+)\'", server_src))
55
- unmapped: list[str] = []
118
+ unregistered: list[str] = []
56
119
  for p in sorted(handler_paths):
57
- level = route_registry.resolve_auth_level("GET", p)
58
- if level not in (
59
- route_registry.AuthLevel.PUBLIC,
60
- route_registry.AuthLevel.SINGLE_USER_GET,
61
- route_registry.AuthLevel.SESSION,
62
- route_registry.AuthLevel.INSTALL,
63
- route_registry.AuthLevel.AUTH_FLOW,
64
- ):
65
- unmapped.append(p)
66
- assert not unmapped, f"unmapped auth levels: {unmapped}"
67
-
68
- # trusted_team: data GETs need session (not anonymous), not a hard deny
120
+ if route_registry.lookup_auth("GET", p) is None and route_registry.lookup_auth("POST", p) is None:
121
+ unregistered.append(p)
122
+ assert not unregistered, f"unregistered auth for if-chain paths: {unregistered}"
123
+
124
+ # Pattern-backed routes from startswith / fullmatch in do_GET/do_POST
125
+ _pattern_probes = [
126
+ ("GET", "/api/public/branding/og"),
127
+ ("GET", "/api/workspace/ws-1/rooms"),
128
+ ("GET", "/api/rooms/room-1/messages"),
129
+ ("POST", "/api/rooms/room-1/messages/send"),
130
+ ("PATCH", "/api/workspace/ws-1"),
131
+ ("DELETE", "/api/memory/mem-1"),
132
+ ]
133
+ for method, path in _pattern_probes:
134
+ assert route_registry.lookup_auth(method, path) is not None, f"missing pattern auth: {method} {path}"
135
+
136
+ # sessionless_ok data GETs
137
+ snap = route_registry.lookup_auth("GET", "/api/snapshot")
138
+ assert snap is not None and snap.sessionless_ok
69
139
  assert not route_registry.authorize_request(
70
140
  "GET",
71
141
  "/api/agents",
@@ -87,10 +157,39 @@ assert route_registry.authorize_request(
87
157
  attach_user=lambda _u: None,
88
158
  )
89
159
 
90
- data_read_get = [r for r in route_registry.ROUTES if r.method == "GET"]
91
- assert len(data_read_get) == 36
92
- registry_post = [r for r in route_registry.ROUTES if r.method == "POST"]
93
- assert len(registry_post) == 25
160
+ # owner/admin gate
161
+ assert not route_registry.authorize_request(
162
+ "GET",
163
+ "/api/admin/vault/status",
164
+ "sid-ok",
165
+ deployment_mode="trusted_team",
166
+ dev_local_unsafe=False,
167
+ install_window_open=False,
168
+ validate_session=lambda s: {"user_id": "u1"} if s == "sid-ok" else None,
169
+ attach_user=lambda _u: None,
170
+ get_workspace_role=lambda: "member",
171
+ )
172
+ assert route_registry.authorize_request(
173
+ "GET",
174
+ "/api/admin/vault/status",
175
+ "sid-ok",
176
+ deployment_mode="trusted_team",
177
+ dev_local_unsafe=False,
178
+ install_window_open=False,
179
+ validate_session=lambda s: {"user_id": "u1"} if s == "sid-ok" else None,
180
+ attach_user=lambda _u: None,
181
+ get_workspace_role=lambda: "owner",
182
+ )
183
+
184
+ dispatched_get = [r for r in route_registry.ROUTES if r.method == "GET" and r.handler]
185
+ dispatched_post = [r for r in route_registry.ROUTES if r.method == "POST" and r.handler]
186
+ dispatched_patch = [r for r in route_registry.ROUTES if r.method == "PATCH" and r.handler]
187
+ pattern_dispatched = [rp for rp in route_registry.ROUTE_PATTERNS if rp.handler]
188
+ assert len(dispatched_get) >= 50, f"unexpected GET dispatch count: {len(dispatched_get)}"
189
+ assert len(dispatched_post) >= 40, f"unexpected POST dispatch count: {len(dispatched_post)}"
190
+ assert len(dispatched_patch) >= 4, f"unexpected PATCH dispatch count: {len(dispatched_patch)}"
191
+ assert len(pattern_dispatched) >= 60, f"unexpected pattern dispatch count: {len(pattern_dispatched)}"
192
+ assert len(dispatched_get) + len(dispatched_post) + len(dispatched_patch) + len(pattern_dispatched) >= 160
94
193
 
95
194
  class _PostStub:
96
195
  def __init__(self) -> None:
@@ -106,4 +205,41 @@ assert route_registry.dispatch_post(_stub, "/api/auth/start", {"email": "a@b.c"}
106
205
  assert _stub.called and _stub.body == {"email": "a@b.c"}
107
206
  assert not route_registry.dispatch_post(_stub, "/api/not-registered", {})
108
207
 
208
+ class _GetStub:
209
+ def __init__(self) -> None:
210
+ self.called = False
211
+ self.pattern_called = False
212
+
213
+ def _route_get_admin_vault_status(self, qs: dict) -> None:
214
+ self.called = True
215
+
216
+ def _route_pattern_get_workspace_events(self, path: str, qs: dict) -> None:
217
+ self.pattern_called = True
218
+
219
+ _get_stub = _GetStub()
220
+ assert route_registry.dispatch_get(_get_stub, "/api/admin/vault/status", {})
221
+ assert _get_stub.called
222
+ assert route_registry.dispatch_get(_get_stub, "/api/admin/vault/status", {}) # idempotent call ok
223
+
224
+ assert route_registry.dispatch_pattern(
225
+ "GET", _get_stub, "/api/workspace/ws-1/events", qs={},
226
+ ) is True
227
+ assert _get_stub.pattern_called
228
+
229
+ class _PatchStub:
230
+ def __init__(self) -> None:
231
+ self.called = False
232
+
233
+ def _route_patch_me(self, body: dict) -> None:
234
+ self.called = True
235
+
236
+ _patch_stub = _PatchStub()
237
+ assert route_registry.dispatch_patch(_patch_stub, "/api/me", {"display_name": "x"})
238
+ assert _patch_stub.called
239
+
240
+ # No legacy frozenset allowlists
241
+ assert not hasattr(route_registry, "GET_ALWAYS_PUBLIC_ROUTES")
242
+ assert not hasattr(route_registry, "GET_SINGLE_USER_PUBLIC_ROUTES")
243
+ assert not hasattr(route_registry, "GET_PUBLIC_ROUTES")
244
+
109
245
  print("route registry self-check ok")
@@ -0,0 +1,112 @@
1
+ """WF-009 RunAuthorityGate 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
+ from domain.entities import ActorType, FundingSource, RunSurface # noqa: E402
11
+ from run_authority import ( # noqa: E402
12
+ DENY_DELEGATION,
13
+ DENY_NO_CREDENTIAL_BYOK,
14
+ DENY_NO_CREDENTIAL_COMPANY,
15
+ DENY_PROVIDER_USER_ONLY,
16
+ RunAuthorityContext,
17
+ RunAuthorityRequest,
18
+ evaluate_run_authority,
19
+ )
20
+
21
+
22
+ def _req() -> RunAuthorityRequest:
23
+ return RunAuthorityRequest(
24
+ surface=RunSurface.CHAT,
25
+ actor_type=ActorType.USER,
26
+ actor_id="user-1",
27
+ triggering_user_id="user-1",
28
+ profile_slug="u-user-1-native",
29
+ workspace_id="ws-1",
30
+ provider="openrouter",
31
+ )
32
+
33
+
34
+ def test_byok_user_cred() -> None:
35
+ decision = evaluate_run_authority(
36
+ _req(),
37
+ RunAuthorityContext(user_has_credential=True),
38
+ run_id="run-1",
39
+ )
40
+ assert decision.allowed
41
+ assert decision.payer_user_id == "user-1"
42
+ assert decision.funding_source == FundingSource.BYOK
43
+
44
+
45
+ def test_byok_no_cred_denied() -> None:
46
+ decision = evaluate_run_authority(_req(), RunAuthorityContext())
47
+ assert not decision.allowed
48
+ assert decision.deny_reason == DENY_NO_CREDENTIAL_BYOK
49
+
50
+
51
+ def test_company_workspace_cred() -> None:
52
+ decision = evaluate_run_authority(
53
+ _req(),
54
+ RunAuthorityContext(workspace_credential_mode="workspace", workspace_has_credential=True),
55
+ )
56
+ assert decision.allowed
57
+ assert decision.funding_source == FundingSource.COMPANY
58
+ assert decision.credential_scope == "workspace"
59
+
60
+
61
+ def test_company_user_cred_wins() -> None:
62
+ decision = evaluate_run_authority(
63
+ _req(),
64
+ RunAuthorityContext(
65
+ workspace_credential_mode="workspace",
66
+ user_has_credential=True,
67
+ workspace_has_credential=True,
68
+ ),
69
+ )
70
+ assert decision.allowed
71
+ assert decision.funding_source == FundingSource.BYOK
72
+
73
+
74
+ def test_user_only_no_fallback() -> None:
75
+ decision = evaluate_run_authority(
76
+ _req(),
77
+ RunAuthorityContext(provider_user_only=True, workspace_has_credential=True),
78
+ )
79
+ assert not decision.allowed
80
+ assert decision.deny_reason == DENY_PROVIDER_USER_ONLY
81
+
82
+
83
+ def test_delegation_grantor() -> None:
84
+ decision = evaluate_run_authority(
85
+ _req(),
86
+ RunAuthorityContext(grantor_has_credential={"owner-1": True}),
87
+ )
88
+ assert decision.allowed
89
+ assert decision.payer_user_id == "owner-1"
90
+
91
+
92
+ def test_delegation_no_grantor_cred() -> None:
93
+ decision = evaluate_run_authority(
94
+ _req(),
95
+ RunAuthorityContext(
96
+ workspace_credential_mode="workspace",
97
+ grantor_has_credential={"owner-1": False},
98
+ ),
99
+ )
100
+ assert not decision.allowed
101
+ assert decision.deny_reason == DENY_NO_CREDENTIAL_COMPANY
102
+
103
+
104
+ if __name__ == "__main__":
105
+ test_byok_user_cred()
106
+ test_byok_no_cred_denied()
107
+ test_company_workspace_cred()
108
+ test_company_user_cred_wins()
109
+ test_user_only_no_fallback()
110
+ test_delegation_grantor()
111
+ test_delegation_no_grantor_cred()
112
+ print("test_run_authority: ok")