create-workframe 0.1.12 → 0.1.14

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-BT9iZUSd.js} +1 -1
  106. package/workframe-ui/public/assets/architecture-7EHR7CIX-m1aPUQ_r.js +1 -0
  107. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-DeBNltHS.js → architectureDiagram-3BPJPVTR-CaEDd3np.js} +1 -1
  108. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-BDhCHu7I.js → blockDiagram-GPEHLZMM-DHJhMrIS.js} +1 -1
  109. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-olcDYvtI.js → c4Diagram-AAUBKEIU-Cdyz9hIV.js} +1 -1
  110. package/workframe-ui/public/assets/channel-3M69ekE5.js +1 -0
  111. package/workframe-ui/public/assets/{chunk-2J33WTMH-D0obCBn_.js → chunk-2J33WTMH-BbZx76LX.js} +1 -1
  112. package/workframe-ui/public/assets/{chunk-3OPIFGDE-DX93f-ZZ.js → chunk-3OPIFGDE-DcSxjTIP.js} +1 -1
  113. package/workframe-ui/public/assets/{chunk-4BX2VUAB-BX9B5wUs.js → chunk-4BX2VUAB-B7F-kTY1.js} +1 -1
  114. package/workframe-ui/public/assets/{chunk-55IACEB6-C4DGc6RO.js → chunk-55IACEB6-BwwEiRTK.js} +1 -1
  115. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-Crlja9Lf.js → chunk-5ZQYHXKU-DAi_NksX.js} +1 -1
  116. package/workframe-ui/public/assets/{chunk-727SXJPM-JYSm3szI.js → chunk-727SXJPM-BIaF4XCN.js} +1 -1
  117. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-_KslxVEl.js → chunk-AQP2D5EJ-DjYSk1dG.js} +1 -1
  118. package/workframe-ui/public/assets/{chunk-BSJP7CBP-CpTO-jU8.js → chunk-BSJP7CBP-_twnyfgV.js} +1 -1
  119. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-JGUkCmbI.js → chunk-CSCIHK7Q-CkNmdy5s.js} +2 -2
  120. package/workframe-ui/public/assets/{chunk-FMBD7UC4-BscBwGaC.js → chunk-FMBD7UC4-CjLqd-gl.js} +1 -1
  121. package/workframe-ui/public/assets/{chunk-KSCS5N6A-5ZTZ0bpX.js → chunk-KSCS5N6A-_6oNqFjj.js} +1 -1
  122. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-DxvitYbW.js → chunk-L5ZTLDWV-CeO9Am_D.js} +1 -1
  123. package/workframe-ui/public/assets/chunk-LZXEDZCA-sRM4YeIe.js +2 -0
  124. package/workframe-ui/public/assets/{chunk-ND2GUHAM-Cayl0iV9.js → chunk-ND2GUHAM-CH8tMb21.js} +1 -1
  125. package/workframe-ui/public/assets/{chunk-NZK2D7GU-_7dyBR5G.js → chunk-NZK2D7GU-DAI1bsII.js} +1 -1
  126. package/workframe-ui/public/assets/{chunk-O5CBEL6O--xTpD0yi.js → chunk-O5CBEL6O-B2e-5a2G.js} +1 -1
  127. package/workframe-ui/public/assets/chunk-QZHKN3VN-BMl9ed8P.js +1 -0
  128. package/workframe-ui/public/assets/chunk-WU5MYG2G-C08HH6BU.js +1 -0
  129. package/workframe-ui/public/assets/{chunk-XPW4576I-CwxJQaeK.js → chunk-XPW4576I-DM2-0q37.js} +1 -1
  130. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BFcOE4Aq.js +1 -0
  131. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BFcOE4Aq.js +1 -0
  132. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-CCspENYv.js → cose-bilkent-S5V4N54A-BnbbewLv.js} +1 -1
  133. package/workframe-ui/public/assets/{dagre-BM42HDAG-Dv8V6R60.js → dagre-BM42HDAG-C4zmB1Vt.js} +1 -1
  134. package/workframe-ui/public/assets/{diagram-2AECGRRQ-Da48hFPT.js → diagram-2AECGRRQ-n-BJmsiC.js} +1 -1
  135. package/workframe-ui/public/assets/{diagram-5GNKFQAL-dTihc7-3.js → diagram-5GNKFQAL-BzQGQnZi.js} +1 -1
  136. package/workframe-ui/public/assets/{diagram-KO2AKTUF-D_Jqu699.js → diagram-KO2AKTUF-Cx_De-wS.js} +1 -1
  137. package/workframe-ui/public/assets/{diagram-LMA3HP47-Bt8PtEaZ.js → diagram-LMA3HP47-CIOI3Lok.js} +1 -1
  138. package/workframe-ui/public/assets/{diagram-OG6HWLK6-BtmPjlh0.js → diagram-OG6HWLK6-DkRuPLWY.js} +1 -1
  139. package/workframe-ui/public/assets/{dist-Cz2turIT.js → dist-XhCT1Q-V.js} +1 -1
  140. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-BxMGCflX.js → erDiagram-TEJ5UH35-C3GwhDU1.js} +1 -1
  141. package/workframe-ui/public/assets/eventmodeling-FCH6USID-DIVBL6CF.js +1 -0
  142. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-ivyroIZt.js → flowDiagram-I6XJVG4X-CLWH8z2e.js} +1 -1
  143. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-B9llwShx.js → ganttDiagram-6RSMTGT7-D4h66Vy3.js} +1 -1
  144. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BFQHk4bw.js → gitGraph-WXDBUCRP-IW5F-Q6c.js} +1 -1
  145. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-ursegLbc.js → gitGraphDiagram-PVQCEYII-BjNIbmRM.js} +1 -1
  146. package/workframe-ui/public/assets/index-DL2vJP4L.css +1 -0
  147. package/workframe-ui/public/assets/index-ElS_D59P.js +130 -0
  148. package/workframe-ui/public/assets/{info-J43DQDTF-DQpifAsB.js → info-J43DQDTF-CcJspM79.js} +1 -1
  149. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-C16XP2Q7.js → infoDiagram-5YYISTIA-DP4xR9jt.js} +1 -1
  150. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-CiJoz7rP.js → ishikawaDiagram-YF4QCWOH-D4cZb21V.js} +1 -1
  151. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CVwEvvUL.js → journeyDiagram-JHISSGLW-DsLuHkIJ.js} +1 -1
  152. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-CWQ6hX5i.js → kanban-definition-UN3LZRKU-Wu65nOsR.js} +1 -1
  153. package/workframe-ui/public/assets/{line-CgQsmU35.js → line-ByBDzRpy.js} +1 -1
  154. package/workframe-ui/public/assets/{linear-Bxbc77dL.js → linear-DzXi2vnq.js} +1 -1
  155. package/workframe-ui/public/assets/{mermaid-parser.core-C_jMeF0a.js → mermaid-parser.core-CaB-1Ckn.js} +2 -2
  156. package/workframe-ui/public/assets/{mermaid.core-4RKV9MZP.js → mermaid.core-D7HTHC17.js} +3 -3
  157. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-DZ7y7Sz0.js → mindmap-definition-RKZ34NQL-BfKsC9aj.js} +1 -1
  158. package/workframe-ui/public/assets/{packet-YPE3B663-B9h2I7Vq.js → packet-YPE3B663-ClpU98TO.js} +1 -1
  159. package/workframe-ui/public/assets/{pie-LRSECV5Y-B2mP5uHm.js → pie-LRSECV5Y-KeOyY_-u.js} +1 -1
  160. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-DyYKyKiP.js → pieDiagram-4H26LBE5-CUpjnzbs.js} +1 -1
  161. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-CL_6nej-.js → quadrantDiagram-W4KKPZXB-CJdPTszF.js} +1 -1
  162. package/workframe-ui/public/assets/{radar-GUYGQ44K-CNKNDQ7S.js → radar-GUYGQ44K-C9BpandZ.js} +1 -1
  163. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-eWU_mhFa.js → requirementDiagram-4Y6WPE33-CHDZPwry.js} +1 -1
  164. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DrjcXvEQ.js → sankeyDiagram-5OEKKPKP-CMJJh91s.js} +1 -1
  165. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-BHWR1xTJ.js → sequenceDiagram-3UESZ5HK-BsV4GppP.js} +1 -1
  166. package/workframe-ui/public/assets/{src-DfJB8kV1.js → src-WHks82Up.js} +1 -1
  167. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BGJbSd3I.js → stateDiagram-AJRCARHV-eSX9P8z0.js} +1 -1
  168. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-FzMK4PkM.js +1 -0
  169. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DkwoCjiZ.js → timeline-definition-PNZ67QCA-Dg1UkhGo.js} +1 -1
  170. package/workframe-ui/public/assets/{treeView-BLDUP644-GxfhiqoW.js → treeView-BLDUP644-B7yGsBuj.js} +1 -1
  171. package/workframe-ui/public/assets/{treemap-LRROVOQU-DxrOsars.js → treemap-LRROVOQU-j_G2oyQ_.js} +1 -1
  172. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-BsWhpUfr.js → vennDiagram-CIIHVFJN-BRrT0jqC.js} +1 -1
  173. package/workframe-ui/public/assets/{wardley-L42UT6IY-CrFZWMHS.js → wardley-L42UT6IY-DEdUqez2.js} +1 -1
  174. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DV8Xpf5I.js → wardleyDiagram-YWT4CUSO-C-bBK9Bb.js} +1 -1
  175. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DqMqnwdZ.js → xychartDiagram-2RQKCTM6-DhH5xuR2.js} +1 -1
  176. package/workframe-ui/public/index.html +11 -7
  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,27 @@
1
+ """WF-032 credential_store 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 credential_store # noqa: E402
11
+
12
+
13
+ def test_quote_env_value() -> None:
14
+ assert credential_store._quote_env_value("sk-abc123") == "sk-abc123"
15
+ assert credential_store._quote_env_value('has "quotes"') == '"has \\"quotes\\""'
16
+
17
+
18
+ def test_remove_env_secret_missing_file(tmp_path: Path) -> None:
19
+ path = tmp_path / ".env"
20
+ credential_store._remove_env_secret(path, "OPENROUTER_API_KEY")
21
+ assert not path.exists()
22
+
23
+
24
+ if __name__ == "__main__":
25
+ test_quote_env_value()
26
+ test_remove_env_secret_missing_file(Path("/tmp/wf-cred-store-check"))
27
+ print("test_credential_store: ok")
@@ -0,0 +1,346 @@
1
+ #!/usr/bin/env python3
2
+ """Dogfood API smoke — rooms, mention, session, invite, OTP, agent, project.
3
+
4
+ Run against live stack:
5
+ python test_dogfood_flows.py --base http://127.0.0.1:18644
6
+
7
+ Mention tests default to invoke_agents=False (routing only, no LLM spend).
8
+ Pass --live-mention to invoke agents; dogfood should bill via Codex gpt-5.4-mini.
9
+
10
+ Requires WORKFRAME_E2E=1 + install window OR DEV_LOCAL_UNSAFE for OTP in auth/start response.
11
+ Falls back to zk session mint when --data-dir points at install data (same host as API).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import os
18
+ import sqlite3
19
+ import sys
20
+ import time
21
+ import urllib.error
22
+ import urllib.request
23
+ import uuid
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ ROOT = Path(__file__).resolve().parent
28
+ sys.path.insert(0, str(ROOT))
29
+
30
+
31
+ def _resolve_install_id(data_dir: Path) -> str:
32
+ env_id = os.environ.get("WORKFRAME_INSTALL_ID", "").strip()
33
+ if env_id:
34
+ return env_id
35
+ env_file = data_dir.parent.parent / ".env"
36
+ if env_file.is_file():
37
+ for line in env_file.read_text(encoding="utf-8").splitlines():
38
+ if line.startswith("WORKFRAME_INSTALL_ID="):
39
+ return line.split("=", 1)[1].strip().strip("'\"")
40
+ raise RuntimeError("WORKFRAME_INSTALL_ID not set — export it or pass --data-dir with install .env")
41
+
42
+
43
+ def _req(
44
+ base: str,
45
+ method: str,
46
+ path: str,
47
+ *,
48
+ body: dict | None = None,
49
+ cookie: str = "",
50
+ session_id: str = "",
51
+ timeout: float = 60,
52
+ ) -> tuple[int, dict[str, Any]]:
53
+ url = f"{base.rstrip('/')}{path}"
54
+ data = json.dumps(body).encode("utf-8") if body is not None else None
55
+ headers = {"Content-Type": "application/json", "Accept": "application/json"}
56
+ if session_id and not cookie:
57
+ import zk_auth
58
+
59
+ if not os.environ.get("WORKFRAME_INSTALL_ID", "").strip():
60
+ raise RuntimeError("WORKFRAME_INSTALL_ID required when passing session_id without cookie")
61
+ cookie = f"{zk_auth.session_cookie_name()}={session_id}"
62
+ if cookie:
63
+ headers["Cookie"] = cookie
64
+ if session_id:
65
+ headers["X-Workframe-Session"] = session_id
66
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
67
+ try:
68
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
69
+ raw = resp.read().decode("utf-8")
70
+ return resp.status, json.loads(raw) if raw else {}
71
+ except urllib.error.HTTPError as exc:
72
+ raw = exc.read().decode("utf-8")
73
+ try:
74
+ payload = json.loads(raw) if raw else {"error": exc.reason}
75
+ except json.JSONDecodeError:
76
+ payload = {"error": raw or str(exc)}
77
+ return exc.code, payload
78
+
79
+
80
+ def _mint_session_cookie(data_dir: Path, user_id: str) -> tuple[str, str]:
81
+ """Return (cookie_header, session_id) for API auth."""
82
+ import zk_auth
83
+
84
+ os.environ["WORKFRAME_API_DATA_DIR"] = str(data_dir)
85
+ os.environ["WORKFRAME_INSTALL_ID"] = _resolve_install_id(data_dir)
86
+
87
+ # Read-only sqlite — avoid WAL sidecars on the shared data mount.
88
+ db_path = data_dir / "zk_auth.db"
89
+ conn = sqlite3.connect(f"file:{db_path.as_posix()}?mode=ro", uri=True, timeout=5.0)
90
+ conn.row_factory = sqlite3.Row
91
+ try:
92
+ row = conn.execute(
93
+ """
94
+ SELECT s.id
95
+ FROM sessions s
96
+ JOIN users u ON u.id = s.user_id
97
+ WHERE s.user_id = ?
98
+ AND s.revoked_at IS NULL
99
+ AND s.expires_at > datetime('now')
100
+ AND u.status = 'active'
101
+ ORDER BY s.created_at DESC
102
+ LIMIT 1
103
+ """,
104
+ (user_id,),
105
+ ).fetchone()
106
+ finally:
107
+ conn.close()
108
+
109
+ session_id = str(row["id"]) if row else ""
110
+ if not session_id:
111
+ raise RuntimeError(f"no valid session for user {user_id}")
112
+ cookie_name = zk_auth.session_cookie_name()
113
+ return f"{cookie_name}={session_id}", session_id
114
+
115
+
116
+ def _pick_user(data_dir: Path) -> tuple[str, str]:
117
+ db = data_dir / "workframe.db"
118
+ conn = sqlite3.connect(db)
119
+ conn.row_factory = sqlite3.Row
120
+ row = conn.execute(
121
+ """
122
+ SELECT u.id, u.email
123
+ FROM users u
124
+ JOIN workspace_memberships m ON m.user_id = u.id AND m.deleted_at IS NULL
125
+ WHERE u.deleted_at IS NULL AND m.role IN ('owner', 'admin')
126
+ ORDER BY CASE m.role WHEN 'owner' THEN 0 ELSE 1 END, u.created_at ASC
127
+ LIMIT 1
128
+ """
129
+ ).fetchone()
130
+ if not row:
131
+ row = conn.execute(
132
+ "SELECT id, email FROM users WHERE deleted_at IS NULL ORDER BY created_at ASC LIMIT 1"
133
+ ).fetchone()
134
+ conn.close()
135
+ if not row:
136
+ raise SystemExit("no users in workframe.db")
137
+ return str(row["id"]), str(row["email"])
138
+
139
+
140
+ def main() -> None:
141
+ ap = argparse.ArgumentParser()
142
+ ap.add_argument("--base", default="http://127.0.0.1:18644")
143
+ ap.add_argument("--data-dir", default=os.environ.get("WORKFRAME_API_DATA_DIR", ""))
144
+ ap.add_argument("--email", default="")
145
+ ap.add_argument("--live-mention", action="store_true", help="invoke agents on @mention (Codex gpt-5.4-mini; costs tokens)")
146
+ args = ap.parse_args()
147
+ base = args.base
148
+ failures: list[str] = []
149
+
150
+ def check(name: str, ok: bool, detail: str = "") -> None:
151
+ mark = "ok" if ok else "FAIL"
152
+ print(f"[{mark}] {name}" + (f" — {detail}" if detail else ""))
153
+ if not ok:
154
+ failures.append(name)
155
+
156
+ code, meta = _req(base, "GET", "/api/meta")
157
+ check("meta", code == 200 and meta.get("ok"), str(meta.get("error") or meta.get("project_name")))
158
+
159
+ cookie = ""
160
+ session_id = ""
161
+ user_id = ""
162
+ email = args.email.strip().lower()
163
+
164
+ def api(method: str, path: str, body: dict | None = None, *, timeout: float = 60) -> tuple[int, dict[str, Any]]:
165
+ return _req(base, method, path, body=body, cookie=cookie, session_id=session_id, timeout=timeout)
166
+
167
+ if args.data_dir:
168
+ data_dir = Path(args.data_dir)
169
+ user_id, db_email = _pick_user(data_dir)
170
+ if not email:
171
+ email = db_email
172
+ try:
173
+ cookie, session_id = _mint_session_cookie(data_dir, user_id)
174
+ check("session mint", bool(session_id), user_id)
175
+ except Exception as exc: # noqa: BLE001
176
+ check("session mint", False, str(exc))
177
+
178
+ if not session_id and not cookie:
179
+ if not email:
180
+ email = f"smoke-{uuid.uuid4().hex[:8]}@workframe.test"
181
+ code, start = api("POST", "/api/auth/start", body={"email": email})
182
+ otp = str(start.get("otp_code") or "")
183
+ check("auth/start", code == 200 and start.get("ok"), start.get("error") or ("otp" if otp else "no otp"))
184
+ if otp:
185
+ code, verify = api("POST", "/api/auth/verify", body={"email": email, "code": otp})
186
+ check("auth/verify", code == 200 and verify.get("ok"), verify.get("error") or "")
187
+ user_id = str(verify.get("user_id") or "")
188
+ session_id = str(verify.get("session_id") or "")
189
+ if session_id:
190
+ import zk_auth
191
+
192
+ cookie = f"{zk_auth.session_cookie_name()}={session_id}"
193
+ elif args.data_dir and user_id:
194
+ cookie, session_id = _mint_session_cookie(Path(args.data_dir), user_id)
195
+
196
+ if not session_id:
197
+ raise SystemExit("no session — pass --data-dir or enable OTP exposure")
198
+
199
+ code, me = api("GET", "/api/me")
200
+ me_user = me.get("user") if isinstance(me.get("user"), dict) else me
201
+ check("me", code == 200 and (me.get("ok", True) or me_user.get("id")), me.get("error") or str(me_user.get("email") or me_user.get("id")))
202
+ user_id = str(me_user.get("id") or me_user.get("user_id") or user_id)
203
+
204
+ workspaces = me.get("workspaces") if isinstance(me.get("workspaces"), list) else []
205
+ current_ws = me.get("current_workspace") if isinstance(me.get("current_workspace"), dict) else None
206
+ ws_id = str((current_ws or {}).get("id") or (workspaces[0].get("id") if workspaces else "") or "")
207
+ if not ws_id and args.data_dir:
208
+ conn = sqlite3.connect(str(Path(args.data_dir) / "workframe.db"))
209
+ conn.row_factory = sqlite3.Row
210
+ row = conn.execute(
211
+ "SELECT workspace_id FROM workspace_memberships WHERE user_id = ? AND deleted_at IS NULL LIMIT 1",
212
+ (user_id,),
213
+ ).fetchone()
214
+ conn.close()
215
+ if row:
216
+ ws_id = str(row["workspace_id"])
217
+ if not ws_id:
218
+ code, snap = api("GET", "/api/snapshot")
219
+ ws = snap.get("workspace") if isinstance(snap.get("workspace"), dict) else {}
220
+ ws_id = str(ws.get("id") or "")
221
+ check("workspace", bool(ws_id), ws_id or "missing")
222
+
223
+ code, rooms_payload = api("GET", f"/api/workspace/{ws_id}/rooms")
224
+ if rooms_payload.get("error") == "workspace_not_found":
225
+ slug_candidates = ["default"]
226
+ if workspaces:
227
+ slug_candidates.extend(str(ws.get("slug") or "") for ws in workspaces)
228
+ for alt in slug_candidates:
229
+ if not alt or alt == ws_id:
230
+ continue
231
+ code, rooms_payload = api("GET", f"/api/workspace/{alt}/rooms")
232
+ if code == 200:
233
+ ws_id = str(rooms_payload.get("workspace_id") or ws_id)
234
+ break
235
+ rooms = rooms_payload.get("rooms") or []
236
+ check("list rooms", code == 200 and isinstance(rooms, list), f"count={len(rooms)}")
237
+
238
+ channel = next((r for r in rooms if r.get("room_type") == "channel"), rooms[0] if rooms else None)
239
+ room_id = str(channel.get("id") if channel else "")
240
+ check("pick channel room", bool(room_id), room_id)
241
+
242
+ # Create project (room)
243
+ proj_slug = f"smoke-{int(time.time())}"
244
+ code, proj = api(
245
+ "POST",
246
+ f"/api/workspace/{ws_id}/rooms/create",
247
+ body={"name": f"Smoke {proj_slug}", "slug": proj_slug, "topic": "smoke test", "room_type": "channel"},
248
+ )
249
+ check("create project", code in (200, 201) and proj.get("ok"), proj.get("error") or str(proj.get("room_id")))
250
+
251
+ # Create agent
252
+ agent_slug = f"smoke-agent-{int(time.time()) % 100000}"
253
+ code, agent = api(
254
+ "POST",
255
+ "/api/hermes/profiles/create",
256
+ body={
257
+ "name": agent_slug,
258
+ "display_name": "Smoke Agent",
259
+ "workspace_id": ws_id,
260
+ "role": "helper",
261
+ "model": "gpt-5.4-mini",
262
+ },
263
+ timeout=180,
264
+ )
265
+ check("create agent", code == 200 and agent.get("ok"), agent.get("error") or str(agent.get("profile")))
266
+
267
+ # Invite email
268
+ invite_email = f"invite-{uuid.uuid4().hex[:6]}@workframe.test"
269
+ code, inv = api(
270
+ "POST",
271
+ f"/api/workspace/{ws_id}/invites",
272
+ body={"email": invite_email, "role": "member"},
273
+ )
274
+ check("invite email", code in (200, 201) and inv.get("ok"), inv.get("error") or invite_email)
275
+
276
+ # Add contact = bootstrap DM with native agent template
277
+ native = str(meta.get("native_profile") or "workframe-agent")
278
+ code, dm = api(
279
+ "POST",
280
+ f"/api/hermes/profiles/{native}/bootstrap-dm",
281
+ body={"workspace_id": ws_id},
282
+ timeout=120,
283
+ )
284
+ check("add contact (bootstrap-dm)", code == 200 and dm.get("ok"), dm.get("error") or str(dm.get("room_id")))
285
+
286
+ # New session — UI uses room bind with new_session, not activate with a fresh uuid.
287
+ if room_id:
288
+ code, sessions = api("GET", f"/api/rooms/{room_id}/sessions")
289
+ check("list sessions", code == 200, str(len(sessions.get("sessions") or [])))
290
+ code, bound = api(
291
+ "POST",
292
+ f"/api/rooms/{room_id}/bind",
293
+ body={
294
+ "source_id": "room",
295
+ "client_id": room_id,
296
+ "new_session": True,
297
+ },
298
+ timeout=120,
299
+ )
300
+ new_sid = str(bound.get("session_id") or "")
301
+ check("new session", code == 200 and bound.get("ok") and bool(new_sid), bound.get("error") or new_sid)
302
+ if new_sid:
303
+ code, activated = api(
304
+ "POST",
305
+ f"/api/rooms/{room_id}/sessions/activate",
306
+ body={"session_id": new_sid, "source_id": "room", "client_id": room_id},
307
+ )
308
+ check("activate session", code == 200 and activated.get("ok"), activated.get("error") or "")
309
+
310
+ # @mention message
311
+ mention_slug = agent_slug if agent.get("ok") else native
312
+ content = f"Hello @{mention_slug} — smoke mention {int(time.time())}"
313
+ code, sent = api(
314
+ "POST",
315
+ f"/api/rooms/{room_id}/messages/send",
316
+ body={"content": content, "invoke_agents": bool(args.live_mention)},
317
+ )
318
+ mentions = sent.get("agent_mentions") or []
319
+ check(
320
+ "mention send",
321
+ code == 200 and sent.get("ok"),
322
+ f"mentions={mentions} err={sent.get('error')}",
323
+ )
324
+
325
+ # Reply (thread)
326
+ if sent.get("message_id"):
327
+ code, reply = api(
328
+ "POST",
329
+ f"/api/rooms/{room_id}/messages/send",
330
+ body={
331
+ "content": "Reply in thread — smoke",
332
+ "parent_message_id": sent["message_id"],
333
+ "invoke_agents": False,
334
+ },
335
+ )
336
+ check("reply send", code == 200 and reply.get("ok"), reply.get("error") or "")
337
+
338
+ print("---")
339
+ if failures:
340
+ print(f"FAILED ({len(failures)}): {', '.join(failures)}")
341
+ raise SystemExit(1)
342
+ print("dogfood flows smoke: all ok")
343
+
344
+
345
+ if __name__ == "__main__":
346
+ main()
@@ -0,0 +1,152 @@
1
+ """WF-039 domain entity self-check — fails if round-trip or imports break."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+
10
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
11
+
12
+ from domain.entities import ( # noqa: E402
13
+ ActorType,
14
+ AgentIdentity,
15
+ Cell,
16
+ CredentialPolicy,
17
+ CredentialRef,
18
+ DeploymentMode,
19
+ FundingSource,
20
+ Grant,
21
+ GrantCapability,
22
+ Lease,
23
+ LeaseStatus,
24
+ Run,
25
+ RunEvent,
26
+ RunStatus,
27
+ RunSurface,
28
+ RuntimeBinding,
29
+ RuntimeBindingStatus,
30
+ RuntimeKind,
31
+ User,
32
+ Workspace,
33
+ )
34
+
35
+
36
+ def _ts() -> datetime:
37
+ return datetime(2026, 7, 6, 12, 0, 0, tzinfo=timezone.utc)
38
+
39
+
40
+ def test_round_trip() -> None:
41
+ cell = Cell(
42
+ cell_id="cell-1",
43
+ install_root="/tmp/wf/MyBusiness",
44
+ manifest_path="/tmp/wf/MyBusiness/workframe-manifest.json",
45
+ package_name="create-workframe",
46
+ package_version="0.1.12",
47
+ deployment_mode=DeploymentMode.SINGLE_USER_LOCAL,
48
+ created_at=_ts(),
49
+ )
50
+ payload = cell.to_dict()
51
+ assert payload["deployment_mode"] == "single_user_local"
52
+ assert payload["cell_id"] == "cell-1"
53
+ json.dumps(payload)
54
+
55
+ run = Run(
56
+ run_id="run-1",
57
+ workspace_id="ws-default",
58
+ surface=RunSurface.CHAT,
59
+ actor_type=ActorType.USER,
60
+ actor_id="user-1",
61
+ triggering_user_id="user-1",
62
+ agent_id="agent-native",
63
+ runtime_binding_id="bind-1",
64
+ status=RunStatus.PENDING,
65
+ payer_user_id="user-1",
66
+ funding_source=FundingSource.BYOK,
67
+ created_at=_ts(),
68
+ )
69
+ run_payload = run.to_dict()
70
+ assert run_payload["surface"] == "chat"
71
+ assert run_payload["status"] == "pending"
72
+
73
+ lease = Lease(
74
+ lease_id="wf_rt_test",
75
+ run_id="run-1",
76
+ workspace_id="ws-default",
77
+ payer_user_id="user-1",
78
+ provider="openrouter",
79
+ profile_slug="u-user-1-native",
80
+ credential_ref_id="cred-1",
81
+ status=LeaseStatus.ACTIVE,
82
+ expires_at=_ts(),
83
+ )
84
+ assert Lease.from_dict(lease.to_dict()).provider == "openrouter"
85
+
86
+
87
+ def test_schema_file_present() -> None:
88
+ schema = Path(__file__).resolve().parent / "domain" / "schema" / "workframe-domain.schema.json"
89
+ data = json.loads(schema.read_text(encoding="utf-8"))
90
+ defs = data["$defs"]
91
+ for name in (
92
+ "Cell",
93
+ "User",
94
+ "Workspace",
95
+ "AgentIdentity",
96
+ "RuntimeBinding",
97
+ "Run",
98
+ "RunEvent",
99
+ "Lease",
100
+ "Grant",
101
+ "CredentialRef",
102
+ ):
103
+ assert name in defs, f"missing schema def: {name}"
104
+
105
+
106
+ def test_agent_runtime_seam_types() -> None:
107
+ agent = AgentIdentity(
108
+ agent_id="agent-1",
109
+ workspace_id="ws-1",
110
+ template_slug="dev",
111
+ display_name="Dev",
112
+ )
113
+ binding = RuntimeBinding(
114
+ binding_id="bind-1",
115
+ agent_id=agent.agent_id,
116
+ runtime_kind=RuntimeKind.HERMES_MANAGED,
117
+ profile_slug="u-alice-dev",
118
+ status=RuntimeBindingStatus.ACTIVE,
119
+ )
120
+ assert binding.agent_id == agent.agent_id
121
+
122
+ grant = Grant(
123
+ grant_id="grant-1",
124
+ run_id="run-1",
125
+ capability=GrantCapability.LLM_TURN,
126
+ )
127
+ assert grant.capability == GrantCapability.LLM_TURN
128
+
129
+ user = User(
130
+ user_id="u-1",
131
+ workspace_id="ws-1",
132
+ email="a@example.com",
133
+ display_name="Alice",
134
+ )
135
+ ws = Workspace(workspace_id="ws-1", cell_id="cell-1", slug="default", name="Workframe")
136
+ cred = CredentialRef(
137
+ ref_id="cred-1",
138
+ workspace_id=ws.workspace_id,
139
+ provider="openrouter",
140
+ policy=CredentialPolicy.BYOK,
141
+ )
142
+ assert cred.provider == "openrouter"
143
+
144
+ event = RunEvent(event_id="ev-1", run_id="run-1", event_type="run.authorized")
145
+ assert event.event_type == "run.authorized"
146
+
147
+
148
+ if __name__ == "__main__":
149
+ test_round_trip()
150
+ test_schema_file_present()
151
+ test_agent_runtime_seam_types()
152
+ print("test_domain_entities: ok")
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env python3
2
+ """WF-035 smoke: auth/vault handler paths log structured errors."""
3
+ from pathlib import Path
4
+
5
+ ROOT = Path(__file__).resolve().parent
6
+ src = (ROOT / "server.py").read_text(encoding="utf-8")
7
+ messaging_src = (ROOT / "workspace_messaging.py").read_text(encoding="utf-8")
8
+ assert "_log_handler_error" in src
9
+ assert 'POST /api/auth/start' in src
10
+ assert 'POST /api/admin/vault/init' in src
11
+ assert "_sync_workspace_messaging_gateway restart" in messaging_src
12
+ print("exception hygiene self-check ok")
@@ -0,0 +1,72 @@
1
+ """WF-032 hermes_admin self-check."""
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 hermes_admin # noqa: E402
18
+
19
+
20
+ def test_resolve_command_known() -> None:
21
+ entry = hermes_admin._resolve_command("/help")
22
+ assert entry is not None
23
+ assert entry["name"] == "/help"
24
+ assert entry["dispatch"] == "client:openHelp"
25
+
26
+
27
+ def test_resolve_command_alias() -> None:
28
+ entry = hermes_admin._resolve_command("reset")
29
+ assert entry is not None
30
+ assert entry["name"] == "/new"
31
+
32
+
33
+ def test_resolve_command_unknown() -> None:
34
+ assert hermes_admin._resolve_command("/not-a-real-command") is None
35
+
36
+
37
+ def test_commands_catalog_shape() -> None:
38
+ catalog = hermes_admin.hermes_commands_catalog()
39
+ assert catalog["ok"] is True
40
+ assert isinstance(catalog["categories"], list)
41
+ assert isinstance(catalog["entries"], list)
42
+ assert all(entry.get("wired") is True for entry in catalog["entries"])
43
+ assert not any(entry.get("wip") for entry in catalog["entries"])
44
+
45
+
46
+ def test_commands_exec_client_dispatch() -> None:
47
+ result = hermes_admin.hermes_commands_exec("/help")
48
+ assert result["ok"] is True
49
+ assert result["dispatched"] == "client"
50
+ assert result["handler"] == "openHelp"
51
+
52
+
53
+ def test_commands_exec_not_slash() -> None:
54
+ result = hermes_admin.hermes_commands_exec("hello")
55
+ assert result["ok"] is False
56
+ assert result["error"] == "not a slash command"
57
+
58
+
59
+ def test_gquota_stub() -> None:
60
+ result = hermes_admin.hermes_gquota()
61
+ assert result["ok"] is False or "configured" in result
62
+
63
+
64
+ if __name__ == "__main__":
65
+ test_resolve_command_known()
66
+ test_resolve_command_alias()
67
+ test_resolve_command_unknown()
68
+ test_commands_catalog_shape()
69
+ test_commands_exec_client_dispatch()
70
+ test_commands_exec_not_slash()
71
+ test_gquota_stub()
72
+ print("test_hermes_admin: ok")
@@ -0,0 +1,78 @@
1
+ """Install wizard resume + admin verify persistence self-check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
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 install_api # noqa: E402
13
+ import stack_config # noqa: E402
14
+
15
+ td = Path(tempfile.mkdtemp())
16
+ stack_config.DATA_DIR = td
17
+ stack_config.CONFIG_PATH = td / "stack_config.json"
18
+ db_path = str(td / "workframe.db")
19
+
20
+ # SMTP saved + tested
21
+ stack_config.patch_stack_config(
22
+ {
23
+ "deployment_mode": "public_multi_user",
24
+ "app_base_url": "https://dev.example.com",
25
+ "smtp": {
26
+ "host": "smtp.example.com",
27
+ "port": 587,
28
+ "user": "relay@example.com",
29
+ "password": "secret",
30
+ "from": "noreply@example.com",
31
+ },
32
+ }
33
+ )
34
+ stack_config.mark_smtp_tested()
35
+ stack_config.patch_stack_config({"smtp": {"admin_email": "owner@example.com"}})
36
+
37
+ assert stack_config.smtp_setup_complete()
38
+ assert not stack_config.install_admin_verified()
39
+ assert install_api.resolve_install_wizard_step(db_path) == "smtp"
40
+
41
+ stack_config.mark_install_admin_verified("owner@example.com")
42
+ assert stack_config.install_admin_verified()
43
+ raw = stack_config.read_stack_raw()
44
+ assert raw["smtp"]["admin_email"] == "owner@example.com"
45
+ assert raw.get("wizard_step") == "workframe"
46
+
47
+ payload = stack_config.public_stack_payload()
48
+ assert payload["smtp"]["admin_email"] == "owner@example.com"
49
+ assert payload["smtp"]["admin_verified"] is True
50
+
51
+ wizard = install_api.install_wizard_public_payload(db_path)
52
+ assert wizard["admin_verified"] is True
53
+ assert wizard["resume_step"] == "workframe"
54
+
55
+ # Workspace progress → billing
56
+ import sqlite3
57
+
58
+ conn = sqlite3.connect(db_path)
59
+ conn.executescript(
60
+ """
61
+ CREATE TABLE workspaces (
62
+ id TEXT PRIMARY KEY, slug TEXT, display_name TEXT, owner_id TEXT,
63
+ settings_json TEXT, deleted_at INTEGER, status TEXT, created_at TEXT, updated_at TEXT
64
+ );
65
+ """
66
+ )
67
+ settings = {"admin_onboarding_done": True}
68
+ conn.execute(
69
+ "INSERT INTO workspaces (id, slug, display_name, owner_id, settings_json, deleted_at, status, created_at, updated_at) "
70
+ "VALUES ('ws1', 'default', 'Workframe', 'u1', ?, NULL, 'active', '1', '1')",
71
+ (json.dumps(settings),),
72
+ )
73
+ conn.commit()
74
+ conn.close()
75
+
76
+ assert install_api.resolve_install_wizard_step(db_path) == "billing"
77
+
78
+ print("test_install_wizard_resume: ok")