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,755 @@
1
+ """Sprint G–K workframe.db schema migrations (WF-032)."""
2
+ from __future__ import annotations
3
+
4
+ import sqlite3
5
+ import time
6
+ from collections.abc import Callable
7
+
8
+ OpenDb = Callable[[], sqlite3.Connection]
9
+
10
+
11
+ def ensure_agent_runs_schema(open_db: OpenDb) -> None:
12
+ """Create or migrate the Workframe agent_runs tracking table."""
13
+ conn = open_db()
14
+ try:
15
+ tables = {
16
+ row["name"]
17
+ for row in conn.execute(
18
+ "SELECT name FROM sqlite_master WHERE type = 'table'"
19
+ ).fetchall()
20
+ }
21
+ if "agent_runs" not in tables:
22
+ conn.execute(
23
+ """
24
+ CREATE TABLE IF NOT EXISTS agent_runs (
25
+ id TEXT PRIMARY KEY,
26
+ agent_profile_id TEXT NOT NULL,
27
+ room_id TEXT DEFAULT NULL,
28
+ triggered_by_user_id TEXT DEFAULT NULL,
29
+ session_id TEXT DEFAULT NULL,
30
+ status TEXT NOT NULL DEFAULT 'pending',
31
+ trigger_source TEXT NOT NULL DEFAULT 'manual',
32
+ prompt_text TEXT DEFAULT '',
33
+ result_summary TEXT DEFAULT '',
34
+ model_provider TEXT DEFAULT '',
35
+ model_name TEXT DEFAULT '',
36
+ input_tokens INTEGER DEFAULT 0,
37
+ output_tokens INTEGER DEFAULT 0,
38
+ cost_usd REAL DEFAULT 0.0,
39
+ error_message TEXT DEFAULT '',
40
+ started_at TEXT NOT NULL,
41
+ completed_at TEXT DEFAULT NULL,
42
+ created_at TEXT NOT NULL,
43
+ updated_at TEXT NOT NULL,
44
+ FOREIGN KEY (agent_profile_id) REFERENCES agent_profiles (id),
45
+ FOREIGN KEY (room_id) REFERENCES rooms (id),
46
+ FOREIGN KEY (triggered_by_user_id) REFERENCES users (id)
47
+ )
48
+ """
49
+ )
50
+ conn.execute(
51
+ "CREATE INDEX IF NOT EXISTS idx_agent_runs_agent "
52
+ "ON agent_runs(agent_profile_id, created_at DESC)"
53
+ )
54
+ conn.execute(
55
+ "CREATE INDEX IF NOT EXISTS idx_agent_runs_room "
56
+ "ON agent_runs(room_id, created_at DESC) WHERE room_id IS NOT NULL"
57
+ )
58
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_agent_runs_status ON agent_runs(status)")
59
+ conn.execute(
60
+ "CREATE INDEX IF NOT EXISTS idx_agent_runs_session "
61
+ "ON agent_runs(session_id) WHERE session_id IS NOT NULL"
62
+ )
63
+ conn.execute(
64
+ "CREATE INDEX IF NOT EXISTS idx_agent_runs_triggered_by "
65
+ "ON agent_runs(triggered_by_user_id) WHERE triggered_by_user_id IS NOT NULL"
66
+ )
67
+ conn.execute(
68
+ "CREATE INDEX IF NOT EXISTS idx_agent_runs_started_at "
69
+ "ON agent_runs(started_at DESC)"
70
+ )
71
+ conn.execute(
72
+ "INSERT OR IGNORE INTO schema_migrations "
73
+ "(version, description, applied_at) VALUES "
74
+ "('4', 'agent_runs tracking table with trigger, prompt, result, and cost fields', datetime('now'))"
75
+ )
76
+ conn.commit()
77
+ return
78
+
79
+ cols = {c["name"] for c in conn.execute("PRAGMA table_info(agent_runs)").fetchall()}
80
+ column_specs = {
81
+ "trigger_source": "TEXT NOT NULL DEFAULT 'manual'",
82
+ "prompt_text": "TEXT DEFAULT ''",
83
+ "result_summary": "TEXT DEFAULT ''",
84
+ "model_provider": "TEXT DEFAULT ''",
85
+ "model_name": "TEXT DEFAULT ''",
86
+ "input_tokens": "INTEGER DEFAULT 0",
87
+ "output_tokens": "INTEGER DEFAULT 0",
88
+ "cost_usd": "REAL DEFAULT 0.0",
89
+ "error_message": "TEXT DEFAULT ''",
90
+ "started_at": "TEXT DEFAULT ''",
91
+ "completed_at": "TEXT DEFAULT NULL",
92
+ "created_at": "TEXT DEFAULT ''",
93
+ "updated_at": "TEXT DEFAULT ''",
94
+ }
95
+ for column, spec in column_specs.items():
96
+ if column not in cols:
97
+ conn.execute(f"ALTER TABLE agent_runs ADD COLUMN {column} {spec}")
98
+ conn.execute(
99
+ "CREATE INDEX IF NOT EXISTS idx_agent_runs_agent "
100
+ "ON agent_runs(agent_profile_id, created_at DESC)"
101
+ )
102
+ conn.execute(
103
+ "CREATE INDEX IF NOT EXISTS idx_agent_runs_room "
104
+ "ON agent_runs(room_id, created_at DESC) WHERE room_id IS NOT NULL"
105
+ )
106
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_agent_runs_status ON agent_runs(status)")
107
+ conn.execute(
108
+ "CREATE INDEX IF NOT EXISTS idx_agent_runs_session "
109
+ "ON agent_runs(session_id) WHERE session_id IS NOT NULL"
110
+ )
111
+ conn.execute(
112
+ "CREATE INDEX IF NOT EXISTS idx_agent_runs_triggered_by "
113
+ "ON agent_runs(triggered_by_user_id) WHERE triggered_by_user_id IS NOT NULL"
114
+ )
115
+ conn.execute(
116
+ "CREATE INDEX IF NOT EXISTS idx_agent_runs_started_at "
117
+ "ON agent_runs(started_at DESC)"
118
+ )
119
+ conn.execute(
120
+ "INSERT OR IGNORE INTO schema_migrations "
121
+ "(version, description, applied_at) VALUES "
122
+ "('4', 'agent_runs tracking table with trigger, prompt, result, and cost fields', datetime('now'))"
123
+ )
124
+ conn.commit()
125
+ finally:
126
+ conn.close()
127
+
128
+
129
+ def ensure_invites_schema(open_db: OpenDb) -> None:
130
+ """Create workspace_invites table if not exists."""
131
+ conn = open_db()
132
+ conn.execute("""CREATE TABLE IF NOT EXISTS workspace_invites (
133
+ id TEXT PRIMARY KEY,
134
+ workspace_id TEXT NOT NULL,
135
+ email TEXT NOT NULL,
136
+ role TEXT NOT NULL DEFAULT 'member',
137
+ token_hash TEXT NOT NULL,
138
+ invited_by_user_id TEXT,
139
+ accepted_by_user_id TEXT,
140
+ expires_at TEXT NOT NULL,
141
+ created_at TEXT NOT NULL,
142
+ accepted_at TEXT,
143
+ deleted_at TEXT
144
+ )""")
145
+ conn.commit()
146
+ conn.close()
147
+
148
+
149
+ def ensure_memory_schema(open_db: OpenDb) -> None:
150
+ """Create memory_items table if not exists."""
151
+ conn = open_db()
152
+ conn.execute("""CREATE TABLE IF NOT EXISTS memory_items (
153
+ id TEXT PRIMARY KEY,
154
+ workspace_id TEXT NOT NULL,
155
+ agent_profile_id TEXT,
156
+ scope TEXT NOT NULL CHECK(scope IN ('workspace', 'agent_profile', 'room', 'user_agent')),
157
+ room_id TEXT,
158
+ user_id TEXT,
159
+ content TEXT NOT NULL,
160
+ source_message_id TEXT,
161
+ visibility TEXT NOT NULL DEFAULT 'shared' CHECK(visibility IN ('shared', 'private')),
162
+ created_by_user_id TEXT,
163
+ created_at TEXT NOT NULL,
164
+ updated_at TEXT NOT NULL,
165
+ deleted_at TEXT
166
+ )""")
167
+ conn.commit()
168
+ conn.close()
169
+
170
+
171
+ def ensure_policies_schema(open_db: OpenDb) -> None:
172
+ """Create workspace_provider_policies table if not exists."""
173
+ conn = open_db()
174
+ conn.execute("""CREATE TABLE IF NOT EXISTS workspace_provider_policies (
175
+ id TEXT PRIMARY KEY,
176
+ workspace_id TEXT NOT NULL,
177
+ provider TEXT NOT NULL,
178
+ allow_user_credentials INTEGER NOT NULL DEFAULT 1,
179
+ allow_workspace_fallback INTEGER NOT NULL DEFAULT 1,
180
+ default_model TEXT,
181
+ created_at TEXT NOT NULL,
182
+ updated_at TEXT NOT NULL,
183
+ UNIQUE(workspace_id, provider)
184
+ )""")
185
+ conn.commit()
186
+ conn.close()
187
+
188
+
189
+ def ensure_user_prefs_schema(open_db: OpenDb) -> None:
190
+ """Add users.current_workspace_id for active workspace selection."""
191
+ conn = open_db()
192
+ try:
193
+ cols = {c[1] for c in conn.execute("PRAGMA table_info(users)").fetchall()}
194
+ if "current_workspace_id" not in cols:
195
+ conn.execute("ALTER TABLE users ADD COLUMN current_workspace_id TEXT DEFAULT NULL")
196
+ conn.commit()
197
+ conn.execute(
198
+ """
199
+ UPDATE users
200
+ SET current_workspace_id = (
201
+ SELECT wi.workspace_id
202
+ FROM workspace_invites wi
203
+ WHERE wi.accepted_by_user_id = users.id
204
+ AND wi.accepted_at IS NOT NULL
205
+ ORDER BY wi.accepted_at DESC
206
+ LIMIT 1
207
+ )
208
+ WHERE COALESCE(current_workspace_id, '') = ''
209
+ AND EXISTS (
210
+ SELECT 1 FROM workspace_invites wi
211
+ WHERE wi.accepted_by_user_id = users.id AND wi.accepted_at IS NOT NULL
212
+ )
213
+ """
214
+ )
215
+ conn.commit()
216
+ finally:
217
+ conn.close()
218
+
219
+
220
+ def ensure_budgets_grants_schema(open_db: OpenDb) -> None:
221
+ """Create budget_policies and credential_grants tables if not exists."""
222
+ conn = open_db()
223
+ conn.execute("""CREATE TABLE IF NOT EXISTS budget_policies (
224
+ id TEXT PRIMARY KEY,
225
+ workspace_id TEXT NOT NULL,
226
+ subject_type TEXT NOT NULL CHECK(subject_type IN ('workspace', 'user', 'agent_profile')),
227
+ subject_id TEXT,
228
+ provider TEXT,
229
+ daily_limit_cents INTEGER,
230
+ monthly_limit_cents INTEGER,
231
+ allowed_models TEXT,
232
+ created_at TEXT NOT NULL,
233
+ updated_at TEXT NOT NULL
234
+ )""")
235
+ conn.execute("""CREATE TABLE IF NOT EXISTS credential_grants (
236
+ id TEXT PRIMARY KEY,
237
+ credential_binding_id TEXT NOT NULL,
238
+ granted_by_user_id TEXT NOT NULL,
239
+ grantee_type TEXT NOT NULL CHECK(grantee_type IN ('user', 'workspace', 'agent_profile', 'room')),
240
+ grantee_id TEXT NOT NULL,
241
+ provider TEXT NOT NULL,
242
+ max_daily_cents INTEGER,
243
+ max_total_cents INTEGER,
244
+ allowed_models TEXT,
245
+ allowed_agent_profile_ids TEXT,
246
+ allowed_room_ids TEXT,
247
+ expires_at TEXT,
248
+ revoked_at TEXT,
249
+ created_at TEXT NOT NULL
250
+ )""")
251
+ conn.commit()
252
+ conn.close()
253
+
254
+ WORKFRAME_DB_SCHEMA = """
255
+ PRAGMA journal_mode = WAL;
256
+ PRAGMA foreign_keys = ON;
257
+
258
+ CREATE TABLE IF NOT EXISTS users (
259
+ id TEXT PRIMARY KEY,
260
+ email TEXT UNIQUE,
261
+ display_name TEXT NOT NULL DEFAULT '',
262
+ avatar_url TEXT DEFAULT NULL,
263
+ platform_ids TEXT DEFAULT '{}',
264
+ role TEXT NOT NULL DEFAULT 'member',
265
+ status TEXT NOT NULL DEFAULT 'active',
266
+ created_at TEXT NOT NULL,
267
+ updated_at TEXT NOT NULL,
268
+ deleted_at TEXT DEFAULT NULL
269
+ );
270
+
271
+ CREATE TABLE IF NOT EXISTS workspaces (
272
+ id TEXT PRIMARY KEY,
273
+ slug TEXT UNIQUE NOT NULL,
274
+ display_name TEXT NOT NULL DEFAULT '',
275
+ description TEXT DEFAULT '',
276
+ owner_id TEXT NOT NULL,
277
+ runtime_mode TEXT NOT NULL DEFAULT 'docker',
278
+ ports TEXT DEFAULT '{}',
279
+ status TEXT NOT NULL DEFAULT 'active',
280
+ created_at TEXT NOT NULL,
281
+ updated_at TEXT NOT NULL,
282
+ deleted_at TEXT DEFAULT NULL
283
+ );
284
+
285
+ CREATE TABLE IF NOT EXISTS workspace_memberships (
286
+ id TEXT PRIMARY KEY,
287
+ workspace_id TEXT NOT NULL,
288
+ user_id TEXT NOT NULL,
289
+ role TEXT NOT NULL DEFAULT 'member',
290
+ status TEXT NOT NULL DEFAULT 'active',
291
+ invited_by TEXT DEFAULT NULL,
292
+ created_at TEXT NOT NULL,
293
+ updated_at TEXT NOT NULL,
294
+ deleted_at TEXT DEFAULT NULL
295
+ );
296
+
297
+ CREATE TABLE IF NOT EXISTS credential_bindings (
298
+ id TEXT PRIMARY KEY,
299
+ workspace_id TEXT DEFAULT NULL,
300
+ user_id TEXT DEFAULT NULL,
301
+ agent_profile_id TEXT DEFAULT NULL,
302
+ provider TEXT NOT NULL,
303
+ credential_type TEXT NOT NULL DEFAULT 'api_key',
304
+ credential_ref TEXT NOT NULL,
305
+ label TEXT DEFAULT '',
306
+ is_active INTEGER NOT NULL DEFAULT 1,
307
+ expires_at TEXT DEFAULT NULL,
308
+ created_by TEXT DEFAULT NULL,
309
+ created_at TEXT NOT NULL,
310
+ updated_at TEXT NOT NULL,
311
+ deleted_at TEXT DEFAULT NULL,
312
+ CHECK (
313
+ (workspace_id IS NOT NULL AND user_id IS NULL AND agent_profile_id IS NULL)
314
+ OR (workspace_id IS NULL AND user_id IS NOT NULL AND agent_profile_id IS NULL)
315
+ OR (workspace_id IS NULL AND user_id IS NULL AND agent_profile_id IS NOT NULL)
316
+ )
317
+ );
318
+
319
+ CREATE INDEX IF NOT EXISTS idx_credential_bindings_workspace_active
320
+ ON credential_bindings(workspace_id, provider, is_active, deleted_at);
321
+
322
+ CREATE INDEX IF NOT EXISTS idx_credential_bindings_user_active
323
+ ON credential_bindings(user_id, provider, is_active, deleted_at);
324
+
325
+ CREATE INDEX IF NOT EXISTS idx_credential_bindings_agent_active
326
+ ON credential_bindings(agent_profile_id, provider, is_active, deleted_at);
327
+
328
+ CREATE TABLE IF NOT EXISTS audit_events (
329
+ id TEXT PRIMARY KEY,
330
+ user_id TEXT DEFAULT NULL,
331
+ event_type TEXT NOT NULL,
332
+ target_type TEXT DEFAULT NULL,
333
+ target_id TEXT DEFAULT NULL,
334
+ summary TEXT DEFAULT '',
335
+ metadata TEXT DEFAULT '{}',
336
+ ip_address TEXT DEFAULT NULL,
337
+ created_at TEXT NOT NULL
338
+ );
339
+
340
+ CREATE TABLE IF NOT EXISTS schema_migrations (
341
+ version TEXT PRIMARY KEY,
342
+ description TEXT DEFAULT '',
343
+ applied_at TEXT NOT NULL
344
+ );
345
+
346
+ CREATE TABLE IF NOT EXISTS agent_profiles (
347
+ id TEXT PRIMARY KEY,
348
+ workspace_id TEXT NOT NULL,
349
+ slug TEXT NOT NULL,
350
+ display_name TEXT NOT NULL DEFAULT '',
351
+ tagline TEXT DEFAULT '',
352
+ role TEXT DEFAULT '',
353
+ model_provider TEXT DEFAULT '',
354
+ model_name TEXT DEFAULT '',
355
+ is_native INTEGER NOT NULL DEFAULT 0,
356
+ avatar_url TEXT DEFAULT NULL,
357
+ status TEXT NOT NULL DEFAULT 'available',
358
+ created_at TEXT NOT NULL,
359
+ updated_at TEXT NOT NULL,
360
+ deleted_at TEXT DEFAULT NULL,
361
+ FOREIGN KEY (workspace_id) REFERENCES workspaces (id),
362
+ UNIQUE (workspace_id, slug, deleted_at)
363
+ );
364
+
365
+ CREATE INDEX IF NOT EXISTS idx_agent_profiles_workspace
366
+ ON agent_profiles(workspace_id)
367
+ WHERE deleted_at IS NULL;
368
+
369
+ CREATE INDEX IF NOT EXISTS idx_agent_profiles_slug
370
+ ON agent_profiles(slug)
371
+ WHERE deleted_at IS NULL;
372
+
373
+ CREATE TABLE IF NOT EXISTS rooms (
374
+ id TEXT PRIMARY KEY,
375
+ workspace_id TEXT NOT NULL,
376
+ agent_profile_id TEXT DEFAULT NULL,
377
+ name TEXT NOT NULL,
378
+ slug TEXT NOT NULL,
379
+ topic TEXT DEFAULT '',
380
+ room_type TEXT NOT NULL DEFAULT 'lane',
381
+ platform_ids TEXT DEFAULT '{}',
382
+ status TEXT NOT NULL DEFAULT 'active',
383
+ created_by TEXT DEFAULT NULL,
384
+ created_at TEXT NOT NULL,
385
+ updated_at TEXT NOT NULL,
386
+ deleted_at TEXT DEFAULT NULL,
387
+ FOREIGN KEY (workspace_id) REFERENCES workspaces (id),
388
+ FOREIGN KEY (agent_profile_id) REFERENCES agent_profiles (id),
389
+ FOREIGN KEY (created_by) REFERENCES users (id),
390
+ UNIQUE (workspace_id, slug, deleted_at)
391
+ );
392
+
393
+ CREATE INDEX IF NOT EXISTS idx_rooms_workspace
394
+ ON rooms(workspace_id)
395
+ WHERE deleted_at IS NULL;
396
+
397
+ CREATE INDEX IF NOT EXISTS idx_rooms_agent_profile
398
+ ON rooms(agent_profile_id)
399
+ WHERE deleted_at IS NULL;
400
+
401
+ CREATE INDEX IF NOT EXISTS idx_rooms_type
402
+ ON rooms(room_type, workspace_id)
403
+ WHERE deleted_at IS NULL;
404
+
405
+ CREATE TABLE IF NOT EXISTS room_memberships (
406
+ id TEXT PRIMARY KEY,
407
+ room_id TEXT NOT NULL,
408
+ user_id TEXT DEFAULT NULL,
409
+ agent_profile_id TEXT DEFAULT NULL,
410
+ role TEXT NOT NULL DEFAULT 'participant',
411
+ status TEXT NOT NULL DEFAULT 'active',
412
+ joined_at TEXT NOT NULL,
413
+ updated_at TEXT NOT NULL,
414
+ deleted_at TEXT DEFAULT NULL,
415
+ FOREIGN KEY (room_id) REFERENCES rooms (id),
416
+ FOREIGN KEY (user_id) REFERENCES users (id),
417
+ FOREIGN KEY (agent_profile_id) REFERENCES agent_profiles (id),
418
+ CHECK (
419
+ (user_id IS NOT NULL AND agent_profile_id IS NULL)
420
+ OR (user_id IS NULL AND agent_profile_id IS NOT NULL)
421
+ ),
422
+ UNIQUE (room_id, user_id, agent_profile_id, deleted_at)
423
+ );
424
+
425
+ CREATE INDEX IF NOT EXISTS idx_room_memberships_room
426
+ ON room_memberships(room_id)
427
+ WHERE deleted_at IS NULL;
428
+
429
+ CREATE INDEX IF NOT EXISTS idx_room_memberships_user
430
+ ON room_memberships(user_id)
431
+ WHERE deleted_at IS NULL;
432
+
433
+ CREATE INDEX IF NOT EXISTS idx_room_memberships_agent
434
+ ON room_memberships(agent_profile_id)
435
+ WHERE deleted_at IS NULL;
436
+
437
+ CREATE TABLE IF NOT EXISTS messages (
438
+ id TEXT PRIMARY KEY,
439
+ room_id TEXT NOT NULL,
440
+ sender_user_id TEXT DEFAULT NULL,
441
+ sender_agent_id TEXT DEFAULT NULL,
442
+ parent_message_id TEXT DEFAULT NULL,
443
+ content TEXT NOT NULL DEFAULT '',
444
+ content_type TEXT NOT NULL DEFAULT 'text',
445
+ metadata TEXT DEFAULT '{}',
446
+ is_edited INTEGER NOT NULL DEFAULT 0,
447
+ created_at TEXT NOT NULL,
448
+ updated_at TEXT NOT NULL,
449
+ deleted_at TEXT DEFAULT NULL,
450
+ FOREIGN KEY (room_id) REFERENCES rooms (id),
451
+ FOREIGN KEY (sender_user_id) REFERENCES users (id),
452
+ FOREIGN KEY (sender_agent_id) REFERENCES agent_profiles (id),
453
+ FOREIGN KEY (parent_message_id) REFERENCES messages (id),
454
+ CHECK (
455
+ (sender_user_id IS NOT NULL AND sender_agent_id IS NULL)
456
+ OR (sender_user_id IS NULL AND sender_agent_id IS NOT NULL)
457
+ OR (sender_user_id IS NULL AND sender_agent_id IS NULL)
458
+ )
459
+ );
460
+
461
+ CREATE INDEX IF NOT EXISTS idx_messages_room_created
462
+ ON messages(room_id, created_at DESC)
463
+ WHERE deleted_at IS NULL;
464
+
465
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_workspace_memberships_workspace_user_active
466
+ ON workspace_memberships(workspace_id, user_id)
467
+ WHERE deleted_at IS NULL;
468
+
469
+ CREATE INDEX IF NOT EXISTS idx_workspace_memberships_user_active
470
+ ON workspace_memberships(user_id, deleted_at, status);
471
+
472
+ CREATE INDEX IF NOT EXISTS idx_workspace_memberships_workspace_active
473
+ ON workspace_memberships(workspace_id, deleted_at, status);
474
+
475
+ CREATE INDEX IF NOT EXISTS idx_workspace_memberships_workspace_role
476
+ ON workspace_memberships(workspace_id, role)
477
+ WHERE deleted_at IS NULL;
478
+
479
+ CREATE INDEX IF NOT EXISTS idx_workspaces_active
480
+ ON workspaces(deleted_at, status);
481
+
482
+ INSERT OR IGNORE INTO schema_migrations (version, description, applied_at)
483
+ VALUES ('1', 'workframe-api runtime schema for /api/me', datetime('now'));
484
+ INSERT OR IGNORE INTO schema_migrations (version, description, applied_at)
485
+ VALUES ('2', 'runtime schema with credential_bindings for credential resolution', datetime('now'));
486
+ INSERT OR IGNORE INTO schema_migrations (version, description, applied_at)
487
+ VALUES ('3', 'runtime schema with agent_profiles, rooms, room_memberships, and messages', datetime('now'));
488
+ INSERT OR IGNORE INTO schema_migrations (version, description, applied_at)
489
+ VALUES ('4', 'agent_runs tracking table with trigger, prompt, result, and cost fields', datetime('now'));
490
+
491
+ CREATE TABLE IF NOT EXISTS room_sessions (
492
+ id TEXT PRIMARY KEY,
493
+ room_id TEXT NOT NULL,
494
+ agent_profile_id TEXT NOT NULL,
495
+ session_id TEXT NOT NULL,
496
+ gateway_session_id TEXT NOT NULL,
497
+ title TEXT DEFAULT '',
498
+ status TEXT NOT NULL DEFAULT 'active',
499
+ forked_from TEXT DEFAULT NULL,
500
+ created_by TEXT DEFAULT NULL,
501
+ created_at TEXT NOT NULL,
502
+ updated_at TEXT NOT NULL,
503
+ deleted_at TEXT DEFAULT NULL,
504
+ FOREIGN KEY (room_id) REFERENCES rooms (id),
505
+ FOREIGN KEY (agent_profile_id) REFERENCES agent_profiles (id)
506
+ );
507
+
508
+ CREATE INDEX IF NOT EXISTS idx_room_sessions_room
509
+ ON room_sessions(room_id, agent_profile_id)
510
+ WHERE deleted_at IS NULL;
511
+
512
+ CREATE INDEX IF NOT EXISTS idx_room_sessions_status
513
+ ON room_sessions(room_id, status)
514
+ WHERE deleted_at IS NULL;
515
+
516
+ INSERT OR IGNORE INTO schema_migrations (version, description, applied_at)
517
+ VALUES ('5', 'room_sessions table for forkable Hermes sessions per room+agent', datetime('now'));
518
+ """
519
+
520
+
521
+ def _srv():
522
+ import server as srv
523
+ return srv
524
+
525
+
526
+ def _migrate_v6_space_rooms(conn: sqlite3.Connection) -> None:
527
+ applied = conn.execute(
528
+ "SELECT 1 FROM schema_migrations WHERE version = '6' LIMIT 1",
529
+ ).fetchone()
530
+ if applied:
531
+ return
532
+ now = str(int(time.time()))
533
+ conn.execute(
534
+ """
535
+ UPDATE rooms
536
+ SET agent_profile_id = NULL, updated_at = ?
537
+ WHERE deleted_at IS NULL
538
+ AND room_type IN ('channel', 'group')
539
+ AND agent_profile_id IS NOT NULL
540
+ """,
541
+ (now,),
542
+ )
543
+ conn.execute(
544
+ """
545
+ INSERT INTO schema_migrations (version, description, applied_at)
546
+ VALUES ('6', 'channel/group rooms are spaces not agent lanes', datetime('now'))
547
+ """,
548
+ )
549
+ def _migrate_v7_space_agent_members(conn: sqlite3.Connection) -> None:
550
+ if conn.execute("SELECT 1 FROM schema_migrations WHERE version = '7' LIMIT 1").fetchone():
551
+ return
552
+ rooms = conn.execute(
553
+ """
554
+ SELECT id, workspace_id FROM rooms
555
+ WHERE deleted_at IS NULL AND room_type IN ('channel', 'group')
556
+ """,
557
+ ).fetchall()
558
+ for room in rooms:
559
+ agents = conn.execute(
560
+ """
561
+ SELECT id FROM agent_profiles
562
+ WHERE workspace_id = ? AND deleted_at IS NULL
563
+ """,
564
+ (str(room["workspace_id"]),),
565
+ ).fetchall()
566
+ for agent in agents:
567
+ _srv()._ensure_agent_in_space_room(conn, str(room["id"]), str(agent["id"]))
568
+ conn.execute(
569
+ """
570
+ INSERT INTO schema_migrations (version, description, applied_at)
571
+ VALUES ('7', 'workspace agents joined to space rooms as members', datetime('now'))
572
+ """,
573
+ )
574
+ def _migrate_v13_workspace_members_in_spaces(conn: sqlite3.Connection) -> None:
575
+ if conn.execute("SELECT 1 FROM schema_migrations WHERE version = '13' LIMIT 1").fetchone():
576
+ return
577
+ rooms = conn.execute(
578
+ """
579
+ SELECT id, workspace_id FROM rooms
580
+ WHERE deleted_at IS NULL AND room_type IN ('channel', 'group')
581
+ """,
582
+ ).fetchall()
583
+ for room in rooms:
584
+ _srv()._add_workspace_members_to_space_room(conn, str(room["workspace_id"]), str(room["id"]))
585
+ conn.execute(
586
+ """
587
+ INSERT INTO schema_migrations (version, description, applied_at)
588
+ VALUES ('13', 'workspace members joined to all space rooms', datetime('now'))
589
+ """,
590
+ )
591
+ def _migrate_v8_room_avatars(conn: sqlite3.Connection) -> None:
592
+ if conn.execute("SELECT 1 FROM schema_migrations WHERE version = '8' LIMIT 1").fetchone():
593
+ return
594
+ cols = {str(row[1]) for row in conn.execute("PRAGMA table_info(rooms)")}
595
+ if "avatar_url" not in cols:
596
+ conn.execute("ALTER TABLE rooms ADD COLUMN avatar_url TEXT DEFAULT NULL")
597
+ conn.execute(
598
+ """
599
+ INSERT INTO schema_migrations (version, description, applied_at)
600
+ VALUES ('8', 'room avatar_url for space settings', datetime('now'))
601
+ """,
602
+ )
603
+ def _migrate_v9_workspace_avatars(conn: sqlite3.Connection) -> None:
604
+ if conn.execute("SELECT 1 FROM schema_migrations WHERE version = '9' LIMIT 1").fetchone():
605
+ return
606
+ cols = {str(row[1]) for row in conn.execute("PRAGMA table_info(workspaces)")}
607
+ if "avatar_url" not in cols:
608
+ conn.execute("ALTER TABLE workspaces ADD COLUMN avatar_url TEXT DEFAULT NULL")
609
+ conn.execute(
610
+ """
611
+ INSERT INTO schema_migrations (version, description, applied_at)
612
+ VALUES ('9', 'workspace avatar_url for workframe settings', datetime('now'))
613
+ """,
614
+ )
615
+ def _migrate_v11_delegation_kanban_boards(conn: sqlite3.Connection) -> None:
616
+ if conn.execute("SELECT 1 FROM schema_migrations WHERE version = '11' LIMIT 1").fetchone():
617
+ return
618
+ conn.execute(
619
+ """
620
+ CREATE TABLE IF NOT EXISTS agent_delegation_grants (
621
+ id TEXT PRIMARY KEY,
622
+ workspace_id TEXT NOT NULL,
623
+ grantor_user_id TEXT NOT NULL,
624
+ grantee_user_id TEXT NOT NULL,
625
+ scope TEXT NOT NULL DEFAULT 'agents:delegate',
626
+ expires_at TEXT DEFAULT NULL,
627
+ created_at TEXT NOT NULL,
628
+ updated_at TEXT NOT NULL,
629
+ deleted_at TEXT DEFAULT NULL,
630
+ FOREIGN KEY (workspace_id) REFERENCES workspaces (id)
631
+ )
632
+ """
633
+ )
634
+ conn.execute(
635
+ """
636
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_delegation_grants_active
637
+ ON agent_delegation_grants(workspace_id, grantor_user_id, grantee_user_id, scope)
638
+ WHERE deleted_at IS NULL
639
+ """
640
+ )
641
+ conn.execute(
642
+ """
643
+ CREATE TABLE IF NOT EXISTS workspace_kanban_boards (
644
+ id TEXT PRIMARY KEY,
645
+ workspace_id TEXT NOT NULL,
646
+ hermes_board_slug TEXT NOT NULL,
647
+ display_name TEXT NOT NULL DEFAULT '',
648
+ created_at TEXT NOT NULL,
649
+ updated_at TEXT NOT NULL,
650
+ deleted_at TEXT DEFAULT NULL,
651
+ FOREIGN KEY (workspace_id) REFERENCES workspaces (id)
652
+ )
653
+ """
654
+ )
655
+ conn.execute(
656
+ """
657
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_workspace_kanban_board_active
658
+ ON workspace_kanban_boards(workspace_id)
659
+ WHERE deleted_at IS NULL
660
+ """
661
+ )
662
+ conn.execute(
663
+ """
664
+ INSERT INTO schema_migrations (version, description, applied_at)
665
+ VALUES ('11', 'agent_delegation_grants and workspace_kanban_boards', datetime('now'))
666
+ """
667
+ )
668
+ def _migrate_v12_adopt_install_keys_to_owners(conn: sqlite3.Connection) -> None:
669
+ """Hermes install keys on the primary profile become the workspace owner's personal keys."""
670
+ if conn.execute("SELECT 1 FROM schema_migrations WHERE version = '12' LIMIT 1").fetchone():
671
+ return
672
+ primary = _srv()._install_profile_slug()
673
+ adopted = False
674
+ if primary:
675
+ stack_env = _srv()._read_env_map(_srv()._profile_dir(primary) / ".env")
676
+ owner_ids = _srv()._install_key_owner_user_ids(conn)
677
+ for owner_id in owner_ids:
678
+ for spec in _srv().PROVIDER_CONNECT_CATALOG:
679
+ if str(spec.get("category") or "") != "llm":
680
+ continue
681
+ env_var = str(spec.get("env_var") or "").strip()
682
+ secret = str(stack_env.get(env_var) or "").strip()
683
+ if not env_var or not secret:
684
+ continue
685
+ if _srv()._adopt_install_llm_key_for_user(
686
+ conn,
687
+ owner_id,
688
+ str(spec["id"]),
689
+ env_var,
690
+ secret,
691
+ ):
692
+ adopted = True
693
+ # ponytail: never strip install-profile .env — install keys stay on workframe-agent;
694
+ # user keys live only in profiles/{user-uuid}/. Never copy from local host Hermes.
695
+ conn.execute(
696
+ """
697
+ INSERT INTO schema_migrations (version, description, applied_at)
698
+ VALUES ('12', 'adopt primary-profile LLM install keys as owner personal keys', datetime('now'))
699
+ """
700
+ )
701
+ def _migrate_v10_workspace_settings_oauth(conn: sqlite3.Connection) -> None:
702
+ if conn.execute("SELECT 1 FROM schema_migrations WHERE version = '10' LIMIT 1").fetchone():
703
+ return
704
+ cols = {str(row[1]) for row in conn.execute("PRAGMA table_info(workspaces)")}
705
+ if "settings_json" not in cols:
706
+ conn.execute("ALTER TABLE workspaces ADD COLUMN settings_json TEXT NOT NULL DEFAULT '{}'")
707
+ conn.execute(
708
+ """
709
+ CREATE TABLE IF NOT EXISTS oauth_pending_states (
710
+ state TEXT PRIMARY KEY,
711
+ user_id TEXT NOT NULL,
712
+ provider TEXT NOT NULL,
713
+ code_verifier TEXT NOT NULL,
714
+ workspace_id TEXT DEFAULT NULL,
715
+ expires_at TEXT NOT NULL,
716
+ created_at TEXT NOT NULL
717
+ )
718
+ """
719
+ )
720
+ conn.execute(
721
+ """
722
+ INSERT INTO schema_migrations (version, description, applied_at)
723
+ VALUES ('10', 'workspace settings_json and oauth_pending_states for user GitHub OAuth', datetime('now'))
724
+ """,
725
+ )
726
+
727
+
728
+
729
+ def ensure_workframe_db_schema(open_db: OpenDb) -> None:
730
+ conn = open_db()
731
+ try:
732
+ conn.executescript(WORKFRAME_DB_SCHEMA)
733
+ _migrate_v6_space_rooms(conn)
734
+ _migrate_v7_space_agent_members(conn)
735
+ _migrate_v8_room_avatars(conn)
736
+ _migrate_v9_workspace_avatars(conn)
737
+ _migrate_v10_workspace_settings_oauth(conn)
738
+ _migrate_v11_delegation_kanban_boards(conn)
739
+ _migrate_v12_adopt_install_keys_to_owners(conn)
740
+ _migrate_v13_workspace_members_in_spaces(conn)
741
+ conn.commit()
742
+ finally:
743
+ conn.close()
744
+
745
+
746
+ def ensure_oauth_pending_migrations(open_db: OpenDb) -> None:
747
+ conn = open_db()
748
+ try:
749
+ _migrate_v10_workspace_settings_oauth(conn)
750
+ _migrate_v11_delegation_kanban_boards(conn)
751
+ _migrate_v12_adopt_install_keys_to_owners(conn)
752
+ _migrate_v13_workspace_members_in_spaces(conn)
753
+ conn.commit()
754
+ finally:
755
+ conn.close()