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,85 @@
1
+ """WF-032 runtime_cohort self-check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ import tempfile
8
+ import uuid
9
+ from pathlib import Path
10
+
11
+ API_DIR = Path(__file__).resolve().parent
12
+ if str(API_DIR) not in sys.path:
13
+ sys.path.insert(0, str(API_DIR))
14
+
15
+ os.environ.setdefault("WORKFRAME_API_DATA_DIR", str(API_DIR / ".tmp-test-data"))
16
+ os.environ.setdefault("HERMES_DATA", str(API_DIR / ".tmp-test-hermes"))
17
+ os.environ.setdefault("DEV_LOCAL_UNSAFE", "true")
18
+
19
+ import runtime_cohort # noqa: E402
20
+ import server # noqa: E402
21
+
22
+
23
+ def test_runtime_profile_slug_format() -> None:
24
+ slug = runtime_cohort._runtime_profile_slug("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "architect")
25
+ assert slug.startswith("u-")
26
+ assert slug.endswith("-architect")
27
+ assert server._runtime_profile_slug("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "architect") == slug
28
+
29
+
30
+ def test_self_grant_forbidden() -> None:
31
+ try:
32
+ runtime_cohort.create_delegation_grant("ws-1", "user-1", "user-1")
33
+ raise AssertionError("expected ValueError")
34
+ except ValueError as exc:
35
+ assert str(exc) == "self_grant_forbidden"
36
+
37
+
38
+ def test_delegation_grant_round_trip() -> None:
39
+ with tempfile.TemporaryDirectory() as tmp:
40
+ os.environ["WORKFRAME_API_DATA_DIR"] = tmp
41
+ server.DATA_DIR = Path(tmp)
42
+ server.AUTH_DB_PATH = Path(tmp) / "auth.db"
43
+ server._ensure_workframe_db_schema()
44
+ ws_id = str(uuid.uuid4())
45
+ grantor = str(uuid.uuid4())
46
+ grantee = str(uuid.uuid4())
47
+ now = server._utc_now()
48
+ conn = server._workframe_db()
49
+ try:
50
+ conn.execute(
51
+ "INSERT INTO workspaces (id, slug, display_name, owner_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
52
+ (ws_id, "test-ws", "Test", grantor, now, now),
53
+ )
54
+ for uid, email in ((grantor, "a@test.local"), (grantee, "b@test.local")):
55
+ conn.execute(
56
+ "INSERT INTO users (id, email, display_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
57
+ (uid, email, email.split("@")[0], now, now),
58
+ )
59
+ conn.execute(
60
+ """
61
+ INSERT INTO workspace_memberships (
62
+ id, workspace_id, user_id, role, status, created_at, updated_at
63
+ ) VALUES (?, ?, ?, 'member', 'active', ?, ?)
64
+ """,
65
+ (str(uuid.uuid4()), ws_id, uid, now, now),
66
+ )
67
+ conn.commit()
68
+ finally:
69
+ conn.close()
70
+ created = runtime_cohort.create_delegation_grant(ws_id, grantor, grantee)
71
+ assert created["ok"] is True
72
+ grant_id = created["grant"]["id"]
73
+ listed = runtime_cohort.list_delegation_grants(ws_id, grantor)
74
+ assert any(row["id"] == grant_id for row in listed["grants"])
75
+ revoked = runtime_cohort.revoke_delegation_grant(ws_id, grant_id, grantor)
76
+ assert revoked["ok"] is True
77
+ grantors = runtime_cohort._delegation_grantor_ids_for_grantee(grantee, ws_id)
78
+ assert grantor not in grantors
79
+
80
+
81
+ if __name__ == "__main__":
82
+ test_runtime_profile_slug_format()
83
+ test_self_grant_forbidden()
84
+ test_delegation_grant_round_trip()
85
+ print("test_runtime_cohort: ok")
@@ -0,0 +1,39 @@
1
+ """WF-NS-P1 secure-mode docker socket boundary 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 server # noqa: E402
11
+
12
+
13
+ def test_docker_request_blocked_in_secure_mode() -> None:
14
+ prev = server.SECURE_MODE
15
+ try:
16
+ server.SECURE_MODE = True
17
+ try:
18
+ server._docker_request("GET", "/containers/json")
19
+ raise AssertionError("expected RuntimeError in SECURE_MODE")
20
+ except RuntimeError as exc:
21
+ assert "SECURE_MODE" in str(exc)
22
+ finally:
23
+ server.SECURE_MODE = prev
24
+
25
+
26
+ def test_public_compose_api_has_no_docker_sock() -> None:
27
+ """Compose config assertion — complements WF-011 supervisor negatives."""
28
+ repo = Path(__file__).resolve().parents[2]
29
+ public = (repo / "infra" / "compose" / "workframe" / "docker-compose.public.yml").read_text(
30
+ encoding="utf-8"
31
+ )
32
+ api_block = public.split("workframe-api:")[1].split("workframe-supervisor:")[0]
33
+ assert "/var/run/docker.sock" not in api_block
34
+
35
+
36
+ if __name__ == "__main__":
37
+ test_docker_request_blocked_in_secure_mode()
38
+ test_public_compose_api_has_no_docker_sock()
39
+ print("test_secure_mode_docker_boundary: ok")
@@ -0,0 +1,99 @@
1
+ """WF-032 guard: server.py must re-export symbols still called from extracted modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ API_DIR = Path(__file__).resolve().parent
10
+ if str(API_DIR) not in sys.path:
11
+ sys.path.insert(0, str(API_DIR))
12
+
13
+ os.environ.setdefault("WORKFRAME_API_DATA_DIR", str(API_DIR / ".tmp-test-data"))
14
+ os.environ.setdefault("HERMES_DATA", str(API_DIR / ".tmp-test-hermes"))
15
+ os.environ.setdefault("DEV_LOCAL_UNSAFE", "true")
16
+
17
+ import server # noqa: E402
18
+
19
+
20
+ _REQUIRED = (
21
+ "_parse_workspace_settings",
22
+ "_strip_profile_action_env",
23
+ "_strip_profile_llm_env",
24
+ "_user_action_env_specs",
25
+ "_overlay_turn_user_env",
26
+ "_overlay_turn_provider_env",
27
+ "_llm_proxy_base_url",
28
+ "_require_user_provider",
29
+ "_ensure_profile_llm_proxy",
30
+ "_revoke_turn_credential_lease",
31
+ "_sync_profile_provider_env",
32
+ "_session_info",
33
+ "chat_messages",
34
+ "chat_bootstrap",
35
+ "_gateway_container_exec",
36
+ "_gateway_container_exec_detached",
37
+ "_docker_exec",
38
+ "_gateway_exec",
39
+ "_hermes_agent_version",
40
+ "ensure_profile_api",
41
+ "hermes_commands_catalog",
42
+ "hermes_commands_exec",
43
+ "hermes_usage",
44
+ "_runtime_profile_slug",
45
+ "ensure_runtime_profile",
46
+ "ensure_user_agent_cohort",
47
+ "resolve_runtime_assignee",
48
+ "cohort_runtime_slugs",
49
+ "_prepare_runtime_profile_credentials",
50
+ "purge_stale_runtime_profiles",
51
+ "list_delegation_grants",
52
+ "create_delegation_grant",
53
+ "revoke_delegation_grant",
54
+ "_delegation_grantor_ids_for_grantee",
55
+ "_runtime_profile_on_disk",
56
+ "_invalidate_gateway_registered_cache",
57
+ "_normalize_user_avatar_url",
58
+ "_normalize_logo_url",
59
+ "_normalize_agent_avatar_patch",
60
+ "_resolve_avatar_fields",
61
+ "_upsert_agent_registry_row",
62
+ "_assign_agent_avatar",
63
+ "_avatar_id_for_display_name",
64
+ "_pick_logo_url",
65
+ "_supervisor_ready",
66
+ "_supervisor_request",
67
+ "_maybe_sync_compose_public_url",
68
+ "_supervisor_gateway_exec",
69
+ "_supervisor_container_exec",
70
+ "_supervisor_profile_lifecycle",
71
+ "_parse_messaging_settings_patch",
72
+ "_workspace_messaging_integrations_payload",
73
+ "_sync_workspace_messaging_gateway",
74
+ "room_chat_bind",
75
+ "profile_chat_bind",
76
+ "list_room_sessions",
77
+ "profile_chat_activate_room_session",
78
+ "profile_chat_message",
79
+ "_enrich_room_chat_payload",
80
+ "_room_session_rows",
81
+ "_extract_title",
82
+ "bootstrap_agent_dm_lane",
83
+ "_provision_agent_dm_runtimes",
84
+ "_ensure_default_workspace",
85
+ "_bootstrap_after_setup",
86
+ "_promote_workspace_owner_if_unclaimed",
87
+ "_sync_workspace_home_room",
88
+ "_onboard_workspace_member_rooms",
89
+ )
90
+
91
+
92
+ def test_server_wf032_reexports_present() -> None:
93
+ missing = [name for name in _REQUIRED if not hasattr(server, name)]
94
+ assert not missing, f"missing server re-exports: {missing}"
95
+
96
+
97
+ if __name__ == "__main__":
98
+ test_server_wf032_reexports_present()
99
+ print("ok")
@@ -53,4 +53,11 @@ os.environ["WORKFRAME_DEPLOYMENT_MODE"] = "trusted_team"
53
53
  stack_config.patch_stack_config({"deployment_mode": "single_user_local"})
54
54
  assert stack_config.resolve_deployment_mode() == "single_user_local"
55
55
 
56
+ # Admin verify survives reload
57
+ stack_config.patch_stack_config({"install_complete": False})
58
+ stack_config.mark_install_admin_verified("owner@example.com")
59
+ assert stack_config.install_admin_verified()
60
+ cfg = stack_config.get_stack_config()
61
+ assert cfg["smtp"]["admin_email"] == "owner@example.com"
62
+
56
63
  print("stack_config install-window smtp self-check ok")
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env python3
2
+ """Granular bind + stream lease timings (docker exec)."""
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ import sys
7
+ import time
8
+ import uuid
9
+
10
+ import server
11
+
12
+
13
+ def ms(t0: float) -> int:
14
+ return round((time.perf_counter() - t0) * 1000)
15
+
16
+
17
+ def main() -> int:
18
+ conn = sqlite3.connect(str(server._workframe_db_path()))
19
+ conn.row_factory = sqlite3.Row
20
+ room = conn.execute(
21
+ """
22
+ SELECT id, workspace_id FROM rooms
23
+ WHERE room_type = 'direct' AND deleted_at IS NULL
24
+ ORDER BY updated_at DESC LIMIT 1
25
+ """,
26
+ ).fetchone()
27
+ user = conn.execute(
28
+ "SELECT id FROM users WHERE status = 'active' ORDER BY created_at LIMIT 1",
29
+ ).fetchone()
30
+ conn.close()
31
+ if not room or not user:
32
+ print("no room/user", file=sys.stderr)
33
+ return 1
34
+
35
+ uid = str(user["id"])
36
+ rid = str(room["id"])
37
+ ws = str(room["workspace_id"])
38
+ prof = server._resolve_chat_hermes_profile("mybusiness-agent", uid, rid, ws)
39
+ payload = {
40
+ "room_id": rid,
41
+ "workspace_id": ws,
42
+ "source_id": "steps",
43
+ "client_id": "steps",
44
+ "binding_version": 2,
45
+ }
46
+ print(f"profile={prof}")
47
+
48
+ block = server._read_model_block(prof)
49
+ t = time.perf_counter()
50
+ block2 = server._read_model_block(prof)
51
+ print(f"read_model_block_ms={ms(t)} repeat_ms={ms(t)}")
52
+
53
+ t = time.perf_counter()
54
+ server.ensure_profile_api(prof, uid, ws)
55
+ print(f"ensure_api_ms={ms(t)}")
56
+
57
+ provider = server._llm_billing_provider(prof, user_id=uid, workspace_id=ws, block=block)
58
+ run_a, run_b = "steps-a", "steps-b"
59
+ t = time.perf_counter()
60
+ server._apply_turn_credential_lease(prof, uid, ws, provider, run_a)
61
+ print(f"lease_cold_ms={ms(t)} provider={provider}")
62
+ t = time.perf_counter()
63
+ server._apply_turn_credential_lease(prof, uid, ws, provider, run_b)
64
+ print(f"lease_warm_ms={ms(t)}")
65
+
66
+ for i in range(3):
67
+ t = time.perf_counter()
68
+ server.profile_chat_bind("", payload, user_id=uid)
69
+ print(f"bind_{i}_ms={ms(t)}")
70
+
71
+ return 0
72
+
73
+
74
+ if __name__ == "__main__":
75
+ raise SystemExit(main())
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env python3
2
+ """Measure model-save, gateway reload, bind, and reconcile latency (docker exec)."""
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ import sys
7
+ import time
8
+
9
+ import server
10
+
11
+
12
+ def ms(t0: float) -> int:
13
+ return round((time.perf_counter() - t0) * 1000)
14
+
15
+
16
+ def main() -> int:
17
+ conn = sqlite3.connect(str(server._workframe_db_path()))
18
+ conn.row_factory = sqlite3.Row
19
+ room = conn.execute(
20
+ """
21
+ SELECT r.id, r.workspace_id
22
+ FROM rooms r
23
+ WHERE r.room_type = 'direct' AND r.deleted_at IS NULL
24
+ ORDER BY r.updated_at DESC
25
+ LIMIT 1
26
+ """,
27
+ ).fetchone()
28
+ user = conn.execute(
29
+ "SELECT id FROM users WHERE status = 'active' ORDER BY created_at LIMIT 1",
30
+ ).fetchone()
31
+ conn.close()
32
+ if not room or not user:
33
+ print("no room/user", file=sys.stderr)
34
+ return 1
35
+
36
+ user_id = str(user["id"])
37
+ room_id = str(room["id"])
38
+ ws_id = str(room["workspace_id"])
39
+ prof = server._resolve_chat_hermes_profile("mybusiness-agent", user_id, room_id, ws_id)
40
+ print(f"profile={prof}")
41
+
42
+ t = time.perf_counter()
43
+ ok, err = server._apply_model_persisted(
44
+ prof, "codex", "gpt-5.4-mini", user_id, restart_gateway=False,
45
+ )
46
+ print(f"yaml_write_ms={ms(t)} ok={ok} err={err!r}")
47
+
48
+ t = time.perf_counter()
49
+ code, out = server._gateway_exec(prof, ["gateway", "restart"])
50
+ # ponytail: ~7s vs stop+start ~45s on dogfood
51
+ healthy = server._wait_profile_api_healthy(prof, attempts=24, delay=0.25)
52
+ print(f"gateway_restart_ms={ms(t)} code={code} healthy={healthy} out={out[:80]!r}")
53
+
54
+ t = time.perf_counter()
55
+ changed = server._reconcile_profile_llm_for_user(prof, user_id, ws_id)
56
+ print(f"reconcile_ms={ms(t)} changed={changed}")
57
+
58
+ t = time.perf_counter()
59
+ server.ensure_profile_api(prof, user_id, ws_id)
60
+ print(f"ensure_api_ms={ms(t)}")
61
+
62
+ payload = {
63
+ "room_id": room_id,
64
+ "workspace_id": ws_id,
65
+ "source_id": "perf",
66
+ "client_id": "perf",
67
+ "binding_version": 2,
68
+ }
69
+ t = time.perf_counter()
70
+ session = server.profile_chat_session("", payload, user_id=user_id)
71
+ print(f"bind_ms={ms(t)} session={str(session.get('session_id') or '')[:24]}")
72
+
73
+ t = time.perf_counter()
74
+ changed2 = server._reconcile_profile_llm_for_user(prof, user_id, ws_id)
75
+ print(f"reconcile_repeat_ms={ms(t)} changed={changed2}")
76
+
77
+ return 0
78
+
79
+
80
+ if __name__ == "__main__":
81
+ raise SystemExit(main())
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env python3
2
+ """Warm-path timings: model save API return + bind + first stream token."""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sqlite3
7
+ import sys
8
+ import time
9
+ import urllib.error
10
+ import urllib.request
11
+
12
+ import server
13
+
14
+
15
+ def ms(t0: float) -> int:
16
+ return round((time.perf_counter() - t0) * 1000)
17
+
18
+
19
+ def main() -> int:
20
+ conn = sqlite3.connect(str(server._workframe_db_path()))
21
+ conn.row_factory = sqlite3.Row
22
+ room = conn.execute(
23
+ """
24
+ SELECT id, workspace_id FROM rooms
25
+ WHERE room_type = 'direct' AND deleted_at IS NULL
26
+ ORDER BY updated_at DESC LIMIT 1
27
+ """,
28
+ ).fetchone()
29
+ user = conn.execute(
30
+ "SELECT id FROM users WHERE status = 'active' ORDER BY created_at LIMIT 1",
31
+ ).fetchone()
32
+ conn.close()
33
+ if not room or not user:
34
+ print("no room/user", file=sys.stderr)
35
+ return 1
36
+
37
+ uid = str(user["id"])
38
+ rid = str(room["id"])
39
+ ws = str(room["workspace_id"])
40
+ prof = server._resolve_chat_hermes_profile("mybusiness-agent", uid, rid, ws)
41
+ payload = {
42
+ "room_id": rid,
43
+ "workspace_id": ws,
44
+ "source_id": "warm",
45
+ "client_id": "warm",
46
+ "binding_version": 2,
47
+ }
48
+ print(f"profile={prof} healthy={server._profile_api_healthy(prof)}")
49
+
50
+ t = time.perf_counter()
51
+ result = server.hermes_model_set(
52
+ prof, "gpt-5.4-medium", user_id=uid, workspace_id=ws, billing_provider="codex",
53
+ )
54
+ print(f"model_set_ms={ms(t)} ok={result.get('ok')}")
55
+
56
+ time.sleep(0.5)
57
+ t = time.perf_counter()
58
+ session = server.profile_chat_session("", payload, user_id=uid)
59
+ sid = str(session.get("session_id") or "")
60
+ print(f"bind_ms={ms(t)} session={sid[:28]}")
61
+
62
+ t = time.perf_counter()
63
+ server.profile_chat_session("", payload, user_id=uid)
64
+ print(f"bind_repeat_ms={ms(t)}")
65
+
66
+ for i in range(2):
67
+ t = time.perf_counter()
68
+ server._reconcile_profile_llm_for_user(prof, uid, ws)
69
+ print(f"reconcile_cached_{i}_ms={ms(t)}")
70
+
71
+ t = time.perf_counter()
72
+ server.profile_chat_session("", payload, user_id=uid)
73
+ print(f"bind_cached_ms={ms(t)}")
74
+
75
+ port = server._profile_api_port(prof)
76
+ key = server._profile_api_key(prof)
77
+ body = json.dumps(server._profile_turn_payload(prof, "Reply with exactly: pong", rid)).encode()
78
+ url = f"http://gateway:{port}/api/sessions/{sid}/chat/stream"
79
+ req = urllib.request.Request(
80
+ url,
81
+ data=body,
82
+ headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
83
+ method="POST",
84
+ )
85
+ t = time.perf_counter()
86
+ first_ms = None
87
+ try:
88
+ with urllib.request.urlopen(req, timeout=120) as resp:
89
+ while True:
90
+ chunk = resp.read(4096)
91
+ if not chunk:
92
+ break
93
+ if first_ms is None and b"data:" in chunk:
94
+ first_ms = ms(t)
95
+ print(f"first_upstream_chunk_ms={first_ms} sample={chunk[:120]!r}")
96
+ break
97
+ except urllib.error.HTTPError as exc:
98
+ print(f"stream_http_error={exc.code} body={exc.read()[:200]!r}")
99
+ return 1
100
+ if first_ms is None:
101
+ print("no_stream_chunk")
102
+ return 1
103
+ return 0
104
+
105
+
106
+ if __name__ == "__main__":
107
+ raise SystemExit(main())
@@ -157,11 +157,22 @@ def parse_lease_token(value: str) -> str:
157
157
  return ""
158
158
 
159
159
 
160
- def validate_lease(token: str) -> dict[str, Any] | None:
161
- """Return lease metadata if token is active (not revoked/expired)."""
160
+ def _lease_row_to_meta(row: sqlite3.Row) -> dict[str, Any]:
161
+ return {
162
+ "run_id": str(row["run_id"]),
163
+ "payer_user_id": str(row["payer_user_id"]),
164
+ "workspace_id": str(row["workspace_id"]),
165
+ "provider": str(row["provider"]),
166
+ "credential_binding_id": str(row["credential_binding_id"] or ""),
167
+ "profile_slug": str(row["profile_slug"]),
168
+ }
169
+
170
+
171
+ def inspect_lease(token: str) -> tuple[str | None, dict[str, Any] | None]:
172
+ """Return (deny_reason, lease_meta). deny_reason is None when the lease is active."""
162
173
  token = parse_lease_token(token)
163
174
  if not token:
164
- return None
175
+ return "missing_token", None
165
176
  ensure_schema()
166
177
  conn = _connect()
167
178
  try:
@@ -176,16 +187,20 @@ def validate_lease(token: str) -> dict[str, Any] | None:
176
187
  ).fetchone()
177
188
  finally:
178
189
  conn.close()
179
- if not row or row["revoked_at"] or _expired(str(row["expires_at"] or "")):
180
- return None
181
- return {
182
- "run_id": str(row["run_id"]),
183
- "payer_user_id": str(row["payer_user_id"]),
184
- "workspace_id": str(row["workspace_id"]),
185
- "provider": str(row["provider"]),
186
- "credential_binding_id": str(row["credential_binding_id"] or ""),
187
- "profile_slug": str(row["profile_slug"]),
188
- }
190
+ if not row:
191
+ return "not_found", None
192
+ meta = _lease_row_to_meta(row)
193
+ if row["revoked_at"]:
194
+ return "revoked", meta
195
+ if _expired(str(row["expires_at"] or "")):
196
+ return "expired", meta
197
+ return None, meta
198
+
199
+
200
+ def validate_lease(token: str) -> dict[str, Any] | None:
201
+ """Return lease metadata if token is active (not revoked/expired)."""
202
+ deny_reason, lease = inspect_lease(token)
203
+ return lease if deny_reason is None else None
189
204
 
190
205
 
191
206
  def revoke_lease(run_id: str) -> None: