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,536 @@
1
+ """WF-032 extract: workspace bootstrap and onboarding helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sqlite3
8
+ import time
9
+ import uuid
10
+ from typing import Any
11
+
12
+
13
+ def _srv():
14
+ import server as srv
15
+
16
+ return srv
17
+
18
+
19
+ def _provision_invited_member_agent_runtimes(workspace_id: str, user_id: str) -> None:
20
+ """Provision u-* runtimes for workspace agents when a member joins."""
21
+ workspace_id = str(workspace_id or "").strip()
22
+ user_id = str(user_id or "").strip()
23
+ if not workspace_id or not user_id:
24
+ return
25
+ try:
26
+ conn = _srv()._workframe_db()
27
+ rows = conn.execute(
28
+ """
29
+ SELECT id FROM agent_profiles
30
+ WHERE workspace_id = ? AND deleted_at IS NULL AND status = 'available'
31
+ """,
32
+ (workspace_id,),
33
+ ).fetchall()
34
+ conn.close()
35
+ except Exception: # noqa: BLE001
36
+ return
37
+ for row in rows:
38
+ _provision_agent_dm_runtimes(workspace_id, str(row["id"]), [user_id])
39
+
40
+
41
+ def _provision_agent_dm_runtimes(
42
+ workspace_id: str,
43
+ agent_profile_id: str,
44
+ user_ids: list[str],
45
+ ) -> None:
46
+ """Create u-* runtime profiles when an agent DM room is created — install/onboarding only, not bind."""
47
+ workspace_id = str(workspace_id or "").strip()
48
+ agent_profile_id = str(agent_profile_id or "").strip()
49
+ if not workspace_id or not agent_profile_id:
50
+ return
51
+ try:
52
+ conn = _srv()._workframe_db()
53
+ agent_row = _srv()._lookup_agent_profile(conn, workspace_id, agent_profile_id)
54
+ conn.close()
55
+ except Exception: # noqa: BLE001
56
+ return
57
+ if not agent_row:
58
+ return
59
+ template = str(agent_row["slug"] or "").strip()
60
+ if not template:
61
+ return
62
+ try:
63
+ template = _srv().resolve_validated_profile(template)
64
+ except ValueError as exc:
65
+ print(
66
+ f"[provision_agent_dm_runtimes] skip {agent_profile_id}: invalid template {template!r}: {exc}",
67
+ flush=True,
68
+ )
69
+ return
70
+ for uid in user_ids:
71
+ user = str(uid or "").strip()
72
+ if not user:
73
+ continue
74
+ runtime = _srv()._runtime_profile_slug(user, template)
75
+ if _srv()._runtime_profile_on_disk(runtime):
76
+ continue
77
+ try:
78
+ _srv().ensure_runtime_profile(runtime, template, user, workspace_id)
79
+ except Exception as exc: # noqa: BLE001
80
+ print(
81
+ f"[provision_agent_dm_runtimes] failed {runtime} for {user}: {exc}",
82
+ flush=True,
83
+ )
84
+
85
+
86
+ def bootstrap_agent_dm_lane(
87
+ user_id: str,
88
+ workspace_id: str,
89
+ template_slug: str,
90
+ *,
91
+ model: str = "",
92
+ soul: str = "",
93
+ bind_session: bool = True,
94
+ room_name: str = "",
95
+ role: str = "",
96
+ tagline: str = "",
97
+ created_by: str = "",
98
+ ) -> dict[str, Any]:
99
+ """Provision u-* runtime, model/proxy, DM room, and optional session bind — install/create-agent parity."""
100
+ user_id = str(user_id or "").strip()
101
+ workspace_id = str(workspace_id or "").strip()
102
+ if not _srv()._native_profile_present():
103
+ _srv()._ensure_native_hermes_profile()
104
+ template = _srv().resolve_validated_profile(str(template_slug or "").strip())
105
+ if not user_id or not workspace_id:
106
+ return {"ok": False, "error": "user_id and workspace_id required"}
107
+ runtime = _srv()._runtime_profile_slug(user_id, template)
108
+ steps: list[dict[str, Any]] = []
109
+ try:
110
+ _srv().ensure_runtime_profile(runtime, template, user_id, workspace_id)
111
+ steps.append({"step": "runtime_profile", "ok": True, "runtime": runtime})
112
+ except Exception as exc:
113
+ steps.append({"step": "runtime_profile", "ok": False, "error": str(exc)})
114
+ return {"ok": False, "template": template, "runtime": runtime, "steps": steps, "error": str(exc)}
115
+
116
+ model_id = str(model or "").strip()
117
+ if model_id:
118
+ try:
119
+ applied = _srv().hermes_model_set(runtime, model_id, user_id, workspace_id)
120
+ steps.append(
121
+ {
122
+ "step": "set_runtime_model",
123
+ "ok": bool(applied.get("ok")),
124
+ "model": model_id,
125
+ **({"error": applied.get("error")} if not applied.get("ok") else {}),
126
+ },
127
+ )
128
+ if not applied.get("ok"):
129
+ return {
130
+ "ok": False,
131
+ "template": template,
132
+ "runtime": runtime,
133
+ "steps": steps,
134
+ "error": str(applied.get("error") or "set_runtime_model_failed"),
135
+ }
136
+ except Exception as exc:
137
+ steps.append({"step": "set_runtime_model", "ok": False, "error": str(exc)})
138
+ return {
139
+ "ok": False,
140
+ "template": template,
141
+ "runtime": runtime,
142
+ "steps": steps,
143
+ "error": str(exc),
144
+ }
145
+ else:
146
+ try:
147
+ _srv()._bootstrap_profile_providers(runtime, user_id, workspace_id)
148
+ steps.append({"step": "bootstrap_providers", "ok": True})
149
+ user_primary, user_chain = _srv()._read_user_llm_prefs(user_id)
150
+ if user_primary:
151
+ applied = _srv().hermes_model_set(runtime, user_primary, user_id, workspace_id)
152
+ steps.append(
153
+ {
154
+ "step": "user_default_model",
155
+ "ok": bool(applied.get("ok")),
156
+ "model": user_primary,
157
+ },
158
+ )
159
+ if user_chain:
160
+ chained = _srv().hermes_fallback_chain_set(runtime, user_chain, user_id=user_id)
161
+ steps.append(
162
+ {
163
+ "step": "user_default_fallbacks",
164
+ "ok": bool(chained.get("ok")),
165
+ "count": len(user_chain),
166
+ },
167
+ )
168
+ except Exception as exc:
169
+ steps.append({"step": "bootstrap_providers", "ok": False, "error": str(exc)})
170
+
171
+ soul_text = str(soul or "").strip()
172
+ identity_role = str(role or "").strip()
173
+ identity_tagline = str(tagline or "").strip()
174
+ identity_name = room_name.strip()
175
+ if soul_text or identity_name or identity_role or identity_tagline:
176
+ _srv()._seed_native_user_overlay(
177
+ runtime,
178
+ template,
179
+ display_name=identity_name,
180
+ role=identity_role,
181
+ tagline=identity_tagline,
182
+ user_soul=soul_text,
183
+ )
184
+
185
+ gateway_started = False
186
+ last_gw_exc: Exception | None = None
187
+ for attempt in range(3):
188
+ try:
189
+ _srv().ensure_profile_api(runtime, user_id, workspace_id, bootstrap_providers=False)
190
+ step_row: dict[str, Any] = {"step": "start_gateway", "ok": True, "runtime": runtime}
191
+ if attempt:
192
+ step_row["retried"] = True
193
+ steps.append(step_row)
194
+ gateway_started = True
195
+ break
196
+ except Exception as exc:
197
+ last_gw_exc = exc
198
+ if attempt < 2:
199
+ time.sleep(1.5)
200
+ if not gateway_started:
201
+ steps.append({"step": "start_gateway", "ok": False, "error": str(last_gw_exc)})
202
+ return {
203
+ "ok": False,
204
+ "template": template,
205
+ "runtime": runtime,
206
+ "steps": steps,
207
+ "error": str(last_gw_exc),
208
+ }
209
+
210
+ display = _srv()._runtime_display_label(user_id, template, workspace_id)
211
+ _srv()._ensure_workspace_agent_profile_row(
212
+ template,
213
+ workspace_id=workspace_id,
214
+ display_name=room_name.strip() or display,
215
+ role=identity_role,
216
+ tagline=identity_tagline,
217
+ is_native=_srv()._is_native_profile(template),
218
+ )
219
+ status, room_payload = _srv()._create_room(
220
+ workspace_id,
221
+ {
222
+ "name": room_name.strip() or display,
223
+ "room_type": "direct",
224
+ "agent_profile_id": template,
225
+ "member_user_ids": [user_id],
226
+ },
227
+ created_by or user_id,
228
+ )
229
+ room = room_payload.get("room") if isinstance(room_payload.get("room"), dict) else {}
230
+ room_id = str(room.get("id") or "").strip()
231
+ dm_error = ""
232
+ if not room_id:
233
+ dm_error = str(
234
+ room_payload.get("error")
235
+ or room_payload.get("detail")
236
+ or ("dm_room_failed" if status not in (200, 201) else "dm_room_missing_id"),
237
+ ).strip()
238
+ steps.append(
239
+ {
240
+ "step": "dm_room",
241
+ "ok": status in (200, 201) and bool(room_id),
242
+ "room_id": room_id,
243
+ "status": status,
244
+ **({"error": dm_error} if dm_error else {}),
245
+ },
246
+ )
247
+ if not room_id:
248
+ return {
249
+ "ok": False,
250
+ "template": template,
251
+ "runtime": runtime,
252
+ "steps": steps,
253
+ "error": dm_error or "dm_room_failed",
254
+ "room_error": room_payload,
255
+ }
256
+
257
+ session_id = ""
258
+ if bind_session:
259
+ bind_payload: dict[str, Any] = {
260
+ "workspace_id": workspace_id,
261
+ "source_id": "ui",
262
+ "client_id": "default",
263
+ "new_session": False,
264
+ }
265
+ if template == _srv()._primary_profile():
266
+ bind_payload["binding_version"] = 2
267
+ try:
268
+ bound = _srv().room_chat_bind(room_id, bind_payload, user_id)
269
+ session_id = str(bound.get("session_id") or "").strip()
270
+ steps.append({"step": "room_chat_bind", "ok": bool(session_id), "session_id": session_id})
271
+ except Exception as exc:
272
+ steps.append({"step": "room_chat_bind", "ok": False, "error": str(exc)})
273
+
274
+ bind_ok = not bind_session or bool(session_id)
275
+ steps_ok = all(s.get("ok") is not False for s in steps)
276
+ return {
277
+ "ok": bind_ok and steps_ok,
278
+ "template": template,
279
+ "runtime": runtime,
280
+ "room_id": room_id,
281
+ "room": room,
282
+ "session_id": session_id,
283
+ "steps": steps,
284
+ **({"error": "room_chat_bind_failed"} if bind_session and not session_id else {}),
285
+ }
286
+
287
+
288
+ def _onboard_workspace_member_rooms(
289
+ conn: sqlite3.Connection,
290
+ workspace_id: str,
291
+ user_id: str,
292
+ *,
293
+ inviter_user_id: str | None = None,
294
+ ) -> dict[str, Any]:
295
+ room_join = _join_workspace_default_room(
296
+ conn,
297
+ workspace_id,
298
+ user_id,
299
+ inviter_user_id=inviter_user_id,
300
+ )
301
+ spaces_joined = _srv()._add_workspace_member_to_space_rooms(conn, workspace_id, user_id)
302
+ return {**room_join, "spaces_joined": spaces_joined}
303
+
304
+
305
+ def _join_workspace_default_room(
306
+ conn: sqlite3.Connection,
307
+ workspace_id: str,
308
+ user_id: str,
309
+ *,
310
+ inviter_user_id: str | None = None,
311
+ ) -> dict[str, Any]:
312
+ room = _srv()._default_workspace_room(conn, workspace_id)
313
+ if not room:
314
+ return {"room_id": None, "joined": False}
315
+ room_id = str(room["id"])
316
+ joined = _srv()._ensure_user_in_room(conn, room_id, user_id)
317
+ inviter_joined = False
318
+ if inviter_user_id and inviter_user_id != user_id:
319
+ inviter_joined = _srv()._ensure_user_in_room(conn, room_id, inviter_user_id)
320
+ return {
321
+ "room_id": room_id,
322
+ "room_slug": str(room["slug"]),
323
+ "joined": joined,
324
+ "inviter_joined": inviter_joined,
325
+ }
326
+
327
+
328
+ def _ensure_user_dm_room(
329
+ conn: sqlite3.Connection,
330
+ workspace_id: str,
331
+ user_a: str,
332
+ user_b: str,
333
+ ) -> dict[str, Any]:
334
+ """Idempotent DM between two workspace users (no agent)."""
335
+ left_id, right_id = sorted([str(user_a).strip(), str(user_b).strip()])
336
+ if not left_id or not right_id or left_id == right_id:
337
+ return {"room_id": None, "created": False}
338
+ existing = conn.execute(
339
+ """
340
+ SELECT id FROM rooms
341
+ WHERE workspace_id = ?
342
+ AND room_type = 'direct'
343
+ AND deleted_at IS NULL
344
+ AND agent_profile_id IS NULL
345
+ AND EXISTS (
346
+ SELECT 1 FROM room_memberships rm
347
+ WHERE rm.room_id = rooms.id AND rm.deleted_at IS NULL AND rm.user_id = ?
348
+ )
349
+ AND EXISTS (
350
+ SELECT 1 FROM room_memberships rm
351
+ WHERE rm.room_id = rooms.id AND rm.deleted_at IS NULL AND rm.user_id = ?
352
+ )
353
+ AND NOT EXISTS (
354
+ SELECT 1 FROM room_memberships rm
355
+ WHERE rm.room_id = rooms.id AND rm.deleted_at IS NULL AND rm.user_id NOT IN (?, ?)
356
+ )
357
+ LIMIT 1
358
+ """,
359
+ (workspace_id, left_id, right_id, left_id, right_id),
360
+ ).fetchone()
361
+ if existing:
362
+ return {"room_id": str(existing["id"]), "created": False}
363
+ now = str(int(time.time()))
364
+ room_id = str(uuid.uuid4())
365
+ slug = _srv()._normalize_room_slug(f"dm-{left_id}-{right_id}", "dm")
366
+ conn.execute(
367
+ """
368
+ INSERT INTO rooms
369
+ (id, workspace_id, agent_profile_id, name, slug, topic, room_type, status, created_by, created_at, updated_at)
370
+ VALUES (?, ?, NULL, ?, ?, '', 'direct', 'active', ?, ?, ?)
371
+ """,
372
+ (room_id, workspace_id, "DM", slug, left_id, now, now),
373
+ )
374
+ for uid in (left_id, right_id):
375
+ _srv()._ensure_user_in_room(conn, room_id, uid)
376
+ return {"room_id": room_id, "created": True}
377
+
378
+
379
+ def _promote_workspace_owner_if_unclaimed(conn: sqlite3.Connection, workspace_id: str, user_id: str) -> bool:
380
+ if not _srv()._install_window_open():
381
+ return False
382
+ row = conn.execute(
383
+ "SELECT owner_id FROM workspaces WHERE id = ? AND deleted_at IS NULL",
384
+ (workspace_id,),
385
+ ).fetchone()
386
+ if not row:
387
+ return False
388
+ if str(row["owner_id"] or "").strip():
389
+ return False
390
+ now = str(int(time.time()))
391
+ conn.execute(
392
+ "UPDATE workspaces SET owner_id = ?, updated_at = ? WHERE id = ?",
393
+ (user_id, now, workspace_id),
394
+ )
395
+ mem = conn.execute(
396
+ "SELECT id FROM workspace_memberships WHERE workspace_id = ? AND user_id = ? AND deleted_at IS NULL",
397
+ (workspace_id, user_id),
398
+ ).fetchone()
399
+ if mem:
400
+ conn.execute(
401
+ "UPDATE workspace_memberships SET role = ?, updated_at = ? WHERE id = ?",
402
+ ("owner", now, mem["id"]),
403
+ )
404
+ else:
405
+ conn.execute(
406
+ """
407
+ INSERT INTO workspace_memberships
408
+ (id, workspace_id, user_id, role, status, created_at, updated_at)
409
+ VALUES (?,?,?,?,?,?,?)
410
+ """,
411
+ (str(uuid.uuid4()), workspace_id, user_id, "owner", "active", now, now),
412
+ )
413
+ return True
414
+
415
+
416
+ def _sync_workspace_home_room(conn: sqlite3.Connection, workspace_id: str) -> None:
417
+ """Keep slug=general space aligned with workspace display_name, tagline, avatar."""
418
+ workspace_id = str(workspace_id or "").strip()
419
+ if not workspace_id:
420
+ return
421
+ ws_row = conn.execute(
422
+ "SELECT display_name, avatar_url, settings_json FROM workspaces WHERE id = ? AND deleted_at IS NULL",
423
+ (workspace_id,),
424
+ ).fetchone()
425
+ if not ws_row:
426
+ return
427
+ settings = _srv()._parse_workspace_settings(ws_row)
428
+ name = str(ws_row["display_name"] or "").strip() or "Workspace"
429
+ tagline = str(settings.get("tagline") or "").strip()
430
+ avatar = str(ws_row["avatar_url"] or "").strip() or None
431
+ now = str(int(time.time()))
432
+ room = conn.execute(
433
+ """
434
+ SELECT id FROM rooms
435
+ WHERE workspace_id = ? AND slug = 'general' AND deleted_at IS NULL
436
+ """,
437
+ (workspace_id,),
438
+ ).fetchone()
439
+ if room:
440
+ conn.execute(
441
+ """
442
+ UPDATE rooms
443
+ SET name = ?, topic = ?, avatar_url = COALESCE(?, avatar_url), updated_at = ?
444
+ WHERE id = ?
445
+ """,
446
+ (name, tagline, avatar, now, str(room["id"])),
447
+ )
448
+
449
+
450
+ def _ensure_default_workspace() -> bool:
451
+ """Idempotent first-run DB seed: Workframe workspace + native agent + General room."""
452
+ project_name = str(os.environ.get("WORKFRAME_PROJECT", "Workframe") or "Workframe").strip() or "Workframe"
453
+ native_slug = str(_srv().NATIVE_PROFILE or "workframe-agent").strip() or "workframe-agent"
454
+ native_display = f"{project_name} Agent"
455
+ conn = _srv()._workframe_db()
456
+ try:
457
+ ws = conn.execute(
458
+ "SELECT id FROM workspaces WHERE slug = 'default' AND deleted_at IS NULL",
459
+ ).fetchone()
460
+ now = str(int(time.time()))
461
+ if not ws:
462
+ ws_id = str(uuid.uuid4())
463
+ settings = json.dumps({
464
+ "credential_mode": "byok",
465
+ "admin_onboarding_done": False,
466
+ "admin_integrations_done": False,
467
+ })
468
+ ws_logo = _srv()._pick_logo_url() or None
469
+ conn.execute(
470
+ """
471
+ INSERT INTO workspaces (
472
+ id, slug, display_name, description, owner_id, status,
473
+ settings_json, avatar_url, created_at, updated_at
474
+ ) VALUES (?, 'default', ?, '', '', 'active', ?, ?, ?, ?)
475
+ """,
476
+ (ws_id, project_name, settings, ws_logo, now, now),
477
+ )
478
+ else:
479
+ ws_id = str(ws["id"])
480
+ ws_row = conn.execute(
481
+ "SELECT display_name, avatar_url, settings_json FROM workspaces WHERE id = ?",
482
+ (ws_id,),
483
+ ).fetchone()
484
+ settings = _srv()._parse_workspace_settings(ws_row) if ws_row else {}
485
+ home_name = str(ws_row["display_name"] or "").strip() if ws_row else project_name
486
+ home_name = home_name or project_name
487
+ home_tagline = str(settings.get("tagline") or "").strip()
488
+ home_avatar = str(ws_row["avatar_url"] or "").strip() if ws_row else ""
489
+ if not home_avatar:
490
+ home_avatar = _srv()._pick_logo_url() or ""
491
+ room = conn.execute(
492
+ """
493
+ SELECT id FROM rooms
494
+ WHERE workspace_id = ? AND slug = 'general' AND deleted_at IS NULL
495
+ """,
496
+ (ws_id,),
497
+ ).fetchone()
498
+ if not room:
499
+ conn.execute(
500
+ """
501
+ INSERT INTO rooms (
502
+ id, workspace_id, name, slug, topic, avatar_url, room_type, status, created_at, updated_at
503
+ ) VALUES (?, ?, ?, 'general', ?, ?, 'channel', 'active', ?, ?)
504
+ """,
505
+ (
506
+ str(uuid.uuid4()),
507
+ ws_id,
508
+ home_name,
509
+ home_tagline,
510
+ home_avatar or None,
511
+ now,
512
+ now,
513
+ ),
514
+ )
515
+ _sync_workspace_home_room(conn, ws_id)
516
+ conn.commit()
517
+ finally:
518
+ conn.close()
519
+ _srv()._ensure_workspace_agent_profile_row(
520
+ native_slug,
521
+ display_name=native_display,
522
+ role="native",
523
+ is_native=True,
524
+ )
525
+ return True
526
+
527
+
528
+ def _bootstrap_after_setup(agent_personality: str = "") -> dict[str, Any]:
529
+ """ponytail: verify native gateway after setup; optional SOUL overlay from onboarding text."""
530
+ profile = _srv()._primary_profile()
531
+ result: dict[str, Any] = {"profile": profile}
532
+ personality = str(agent_personality or "").strip()
533
+ if personality:
534
+ _srv()._seed_native_user_overlay(profile, profile, user_soul=personality)
535
+ result["soul_seeded"] = True
536
+ return result