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,166 @@
1
+ """WF-032 extract: Docker socket and gateway container exec helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import http.client
6
+ import json
7
+ import shlex
8
+ import socket
9
+ import urllib.parse
10
+ from typing import Any
11
+
12
+ import updates as stack_updates
13
+
14
+
15
+ def _srv():
16
+ import server as srv
17
+
18
+ return srv
19
+
20
+
21
+ class _UnixHTTPConnection(http.client.HTTPConnection):
22
+ def __init__(self, unix_path: str):
23
+ super().__init__("localhost")
24
+ self.unix_path = unix_path
25
+
26
+ def connect(self) -> None:
27
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
28
+ sock.connect(self.unix_path)
29
+ self.sock = sock
30
+
31
+
32
+ def _docker_request(method: str, path: str, body: dict[str, Any] | None = None) -> tuple[int, Any]:
33
+ if _srv().SECURE_MODE:
34
+ raise RuntimeError("Docker socket access is disabled in SECURE_MODE")
35
+ conn = _UnixHTTPConnection(_srv().DOCKER_SOCK)
36
+ payload = json.dumps(body).encode("utf-8") if body is not None else None
37
+ headers = {"Content-Type": "application/json"} if payload is not None else {}
38
+ conn.request(method, f"/v1.41{path}", body=payload, headers=headers)
39
+ resp = conn.getresponse()
40
+ raw = resp.read()
41
+ conn.close()
42
+ if not raw:
43
+ return resp.status, None
44
+ try:
45
+ return resp.status, json.loads(raw.decode("utf-8", errors="replace"))
46
+ except json.JSONDecodeError:
47
+ return resp.status, raw.decode("utf-8", errors="replace")
48
+
49
+
50
+ def _docker_exec_detached(container: str, cmd: list[str], acting_profile: str = "") -> tuple[int, str]:
51
+ if _srv()._exec_targets_runtime_profile_secrets(cmd, acting_profile):
52
+ return 1, "blocked: runtime profile credential paths are not readable via gateway exec"
53
+ create_status, create_data = _docker_request(
54
+ "POST",
55
+ f"/containers/{urllib.parse.quote(container, safe='')}/exec",
56
+ {
57
+ "AttachStdout": True,
58
+ "AttachStderr": True,
59
+ "Tty": False,
60
+ "Cmd": cmd,
61
+ },
62
+ )
63
+ if create_status >= 300 or not isinstance(create_data, dict) or not create_data.get("Id"):
64
+ return create_status, f"exec create failed: {create_data}"
65
+ exec_id = str(create_data["Id"])
66
+ start_status, _start_data = _docker_request(
67
+ "POST",
68
+ f"/exec/{urllib.parse.quote(exec_id, safe='')}/start",
69
+ {"Detach": True, "Tty": False},
70
+ )
71
+ if start_status >= 300:
72
+ return start_status, "exec start failed"
73
+ return 0, ""
74
+
75
+
76
+ def _docker_exec(container: str, cmd: list[str], acting_profile: str = "") -> tuple[int, str]:
77
+ if _srv()._exec_targets_runtime_profile_secrets(cmd, acting_profile):
78
+ return 1, "blocked: runtime profile credential paths are not readable via gateway exec"
79
+ create_status, create_data = _docker_request(
80
+ "POST",
81
+ f"/containers/{urllib.parse.quote(container, safe='')}/exec",
82
+ {
83
+ "AttachStdout": True,
84
+ "AttachStderr": True,
85
+ "Tty": False,
86
+ "Cmd": cmd,
87
+ },
88
+ )
89
+ if create_status >= 300 or not isinstance(create_data, dict) or not create_data.get("Id"):
90
+ return create_status, f"exec create failed: {create_data}"
91
+ exec_id = str(create_data["Id"])
92
+ start_status, start_data = _docker_request(
93
+ "POST",
94
+ f"/exec/{urllib.parse.quote(exec_id, safe='')}/start",
95
+ {"Detach": False, "Tty": False},
96
+ )
97
+ out = start_data if isinstance(start_data, str) else json.dumps(start_data or {})
98
+ if isinstance(out, str) and out:
99
+ try:
100
+ raw = out.encode("latin-1", errors="ignore")
101
+ buf = bytearray()
102
+ i = 0
103
+ # Docker non-TTY exec streams are 8-byte header framed.
104
+ while i + 8 <= len(raw):
105
+ size = int.from_bytes(raw[i + 4 : i + 8], "big")
106
+ i += 8
107
+ if size < 0 or i + size > len(raw):
108
+ break
109
+ buf.extend(raw[i : i + size])
110
+ i += size
111
+ if buf:
112
+ out = buf.decode("utf-8", errors="replace")
113
+ except Exception:
114
+ pass
115
+ inspect_status, inspect_data = _docker_request("GET", f"/exec/{urllib.parse.quote(exec_id, safe='')}/json")
116
+ if inspect_status >= 300 or not isinstance(inspect_data, dict):
117
+ return start_status, out
118
+ exit_raw = inspect_data.get("ExitCode")
119
+ exit_code = 0 if exit_raw in (0, "0") else int(exit_raw or 1)
120
+ return exit_code, out
121
+
122
+
123
+ def _gateway_container_exec(cmd: list[str]) -> tuple[int, str]:
124
+ """Run a command in the gateway container — via supervisor in SECURE_MODE."""
125
+ if _srv().SECURE_MODE:
126
+ return _srv()._supervisor_container_exec(cmd)
127
+ return _docker_exec(_srv().GATEWAY_CONTAINER_NAME, cmd)
128
+
129
+
130
+ def _gateway_container_exec_detached(cmd: list[str]) -> tuple[int, str]:
131
+ """Detached gateway exec — long-running jobs survive after exec returns."""
132
+ if _srv().SECURE_MODE:
133
+ return _srv()._supervisor_container_exec(cmd, detach=True)
134
+ return _docker_exec_detached(_srv().GATEWAY_CONTAINER_NAME, cmd)
135
+
136
+
137
+ def _profile_home_container(profile: str) -> str:
138
+ return f"/opt/data/profiles/{_srv().resolve_hermes_profile(profile)}"
139
+
140
+
141
+ def _gateway_exec(profile: str, args: list[str]) -> tuple[int, str]:
142
+ """Run hermes CLI in the gateway container with specialist profile home."""
143
+ prof = _srv().resolve_hermes_profile(profile)
144
+ if _srv().SECURE_MODE:
145
+ return _srv()._supervisor_gateway_exec(prof, args)
146
+ cli = ["/opt/hermes/bin/hermes", "-p", prof, *args]
147
+ if prof == _srv()._primary_profile():
148
+ return _docker_exec(_srv().GATEWAY_CONTAINER_NAME, cli, acting_profile=prof)
149
+ home = _profile_home_container(prof)
150
+ inner = " ".join(shlex.quote(part) for part in cli)
151
+ shell = (
152
+ f"export HERMES_HOME={shlex.quote(home)} HOME={shlex.quote(home)}; "
153
+ f"cd {shlex.quote(home)}; {inner}"
154
+ )
155
+ return _docker_exec(_srv().GATEWAY_CONTAINER_NAME, ["sh", "-lc", shell], acting_profile=prof)
156
+
157
+
158
+ def _hermes_agent_version() -> str:
159
+ """Native Hermes semver from `hermes --version` inside the gateway container."""
160
+ try:
161
+ code, out = _gateway_exec(_srv()._primary_profile(), ["--version"])
162
+ if code != 0:
163
+ return ""
164
+ return stack_updates.parse_hermes_version_output(out)
165
+ except Exception: # noqa: BLE001
166
+ return ""
@@ -0,0 +1,109 @@
1
+ """WF-032 extract: doctor audit/repair for agent DM runtime profiles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ from typing import Any
7
+
8
+
9
+ def _srv():
10
+ import server as srv
11
+
12
+ return srv
13
+
14
+
15
+ def iter_agent_dm_runtime_slots(conn: sqlite3.Connection) -> list[dict[str, str]]:
16
+ """All (workspace, agent, user) slots that need a u-* runtime for agent DM bind."""
17
+ rows = conn.execute(
18
+ """
19
+ SELECT r.workspace_id, r.agent_profile_id, rm.user_id
20
+ FROM rooms r
21
+ JOIN room_memberships rm ON rm.room_id = r.id AND rm.deleted_at IS NULL
22
+ WHERE r.deleted_at IS NULL
23
+ AND r.room_type = 'direct'
24
+ AND r.agent_profile_id IS NOT NULL
25
+ AND TRIM(r.agent_profile_id) != ''
26
+ """,
27
+ ).fetchall()
28
+ seen: set[tuple[str, str, str]] = set()
29
+ slots: list[dict[str, str]] = []
30
+ for row in rows:
31
+ workspace_id = str(row["workspace_id"] or "").strip()
32
+ agent_ref = str(row["agent_profile_id"] or "").strip()
33
+ user_id = str(row["user_id"] or "").strip()
34
+ if not workspace_id or not agent_ref or not user_id:
35
+ continue
36
+ key = (workspace_id, agent_ref, user_id)
37
+ if key in seen:
38
+ continue
39
+ seen.add(key)
40
+ agent_row = _srv()._lookup_agent_profile(conn, workspace_id, agent_ref)
41
+ if not agent_row:
42
+ continue
43
+ template = str(agent_row["slug"] or "").strip()
44
+ if not template:
45
+ continue
46
+ runtime = _srv()._runtime_profile_slug(user_id, template)
47
+ slots.append({
48
+ "workspace_id": workspace_id,
49
+ "agent_profile_id": str(agent_row["id"]),
50
+ "user_id": user_id,
51
+ "template": template,
52
+ "runtime": runtime,
53
+ "on_disk": _srv()._runtime_profile_on_disk(runtime),
54
+ })
55
+ return slots
56
+
57
+
58
+ def doctor_audit_agent_dm_runtimes() -> dict[str, Any]:
59
+ """Report missing u-* runtimes for agent DM rooms — no mutations."""
60
+ conn = _srv()._workframe_db()
61
+ try:
62
+ slots = iter_agent_dm_runtime_slots(conn)
63
+ finally:
64
+ conn.close()
65
+ missing = [s for s in slots if not s["on_disk"]]
66
+ return {
67
+ "ok": True,
68
+ "total": len(slots),
69
+ "present": len(slots) - len(missing),
70
+ "missing": missing,
71
+ }
72
+
73
+
74
+ def doctor_repair_agent_dm_runtimes(*, repair: bool = True) -> dict[str, Any]:
75
+ """Explicit repair — provision missing u-* runtimes for agent DM rooms."""
76
+ conn = _srv()._workframe_db()
77
+ try:
78
+ slots = iter_agent_dm_runtime_slots(conn)
79
+ finally:
80
+ conn.close()
81
+ missing = [s for s in slots if not s["on_disk"]]
82
+ if not repair:
83
+ return doctor_audit_agent_dm_runtimes()
84
+ repaired: list[dict[str, str]] = []
85
+ failed: list[dict[str, str]] = []
86
+ for slot in missing:
87
+ runtime = slot["runtime"]
88
+ try:
89
+ _srv().ensure_runtime_profile(
90
+ runtime,
91
+ slot["template"],
92
+ slot["user_id"],
93
+ slot["workspace_id"],
94
+ )
95
+ if _srv()._runtime_profile_on_disk(runtime):
96
+ repaired.append(slot)
97
+ else:
98
+ failed.append({**slot, "error": "runtime_still_missing"})
99
+ except Exception as exc: # noqa: BLE001
100
+ failed.append({**slot, "error": str(exc)})
101
+ still_missing = [s for s in missing if not _srv()._runtime_profile_on_disk(s["runtime"])]
102
+ return {
103
+ "ok": not failed and not still_missing,
104
+ "total": len(slots),
105
+ "missing_before": len(missing),
106
+ "repaired": repaired,
107
+ "failed": failed,
108
+ "still_missing": still_missing,
109
+ }
@@ -0,0 +1,47 @@
1
+ """Workframe domain model (WF-039)."""
2
+
3
+ from domain.entities import (
4
+ ActorType,
5
+ AgentIdentity,
6
+ Cell,
7
+ CredentialPolicy,
8
+ CredentialRef,
9
+ DeploymentMode,
10
+ FundingSource,
11
+ Grant,
12
+ GrantCapability,
13
+ Lease,
14
+ LeaseStatus,
15
+ Run,
16
+ RunEvent,
17
+ RunStatus,
18
+ RunSurface,
19
+ RuntimeBinding,
20
+ RuntimeBindingStatus,
21
+ RuntimeKind,
22
+ User,
23
+ Workspace,
24
+ )
25
+
26
+ __all__ = [
27
+ "ActorType",
28
+ "AgentIdentity",
29
+ "Cell",
30
+ "CredentialPolicy",
31
+ "CredentialRef",
32
+ "DeploymentMode",
33
+ "FundingSource",
34
+ "Grant",
35
+ "GrantCapability",
36
+ "Lease",
37
+ "LeaseStatus",
38
+ "Run",
39
+ "RunEvent",
40
+ "RunStatus",
41
+ "RunSurface",
42
+ "RuntimeBinding",
43
+ "RuntimeBindingStatus",
44
+ "RuntimeKind",
45
+ "User",
46
+ "Workspace",
47
+ ]
@@ -0,0 +1,252 @@
1
+ """Workframe domain entities — stable vocabulary for authority, runs, and credentials.
2
+
3
+ Referenced by WF-009 RunAuthorityGate, WF-NS-P2 runs tables, and public glossary (WF-NS-P0).
4
+ Implementation in server.py still uses legacy shapes; migrate incrementally (WF-032).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import asdict, dataclass, field
10
+ from datetime import datetime
11
+ from enum import Enum
12
+ from typing import Any, Literal, Mapping, TypeVar
13
+
14
+ T = TypeVar("T", bound="DomainEntity")
15
+
16
+
17
+ class DomainEntity:
18
+ """Minimal JSON round-trip for dataclass entities."""
19
+
20
+ def to_dict(self) -> dict[str, Any]:
21
+ return _encode_value(asdict(self))
22
+
23
+ @classmethod
24
+ def from_dict(cls: type[T], data: Mapping[str, Any]) -> T:
25
+ return cls(**dict(data))
26
+
27
+
28
+ def _encode_value(value: Any) -> Any:
29
+ if isinstance(value, datetime):
30
+ return value.isoformat()
31
+ if isinstance(value, Enum):
32
+ return value.value
33
+ if isinstance(value, dict):
34
+ return {k: _encode_value(v) for k, v in value.items()}
35
+ if isinstance(value, list):
36
+ return [_encode_value(v) for v in value]
37
+ return value
38
+
39
+
40
+ class DeploymentMode(str, Enum):
41
+ SINGLE_USER_LOCAL = "single_user_local"
42
+ TRUSTED_TEAM = "trusted_team"
43
+ PUBLIC_MULTI_USER = "public_multi_user"
44
+
45
+
46
+ class ActorType(str, Enum):
47
+ USER = "user"
48
+ AGENT = "agent"
49
+ SYSTEM = "system"
50
+ WEBHOOK = "webhook"
51
+
52
+
53
+ class RunStatus(str, Enum):
54
+ PENDING = "pending"
55
+ AUTHORIZED = "authorized"
56
+ RUNNING = "running"
57
+ COMPLETED = "completed"
58
+ FAILED = "failed"
59
+ CANCELLED = "cancelled"
60
+ DENIED = "denied"
61
+
62
+
63
+ class RunSurface(str, Enum):
64
+ AGENT_RAIL = "agent_rail"
65
+ CHAT = "chat"
66
+ FILES = "files"
67
+ BROWSER = "browser"
68
+ ACTIVITY = "activity"
69
+ KANBAN = "kanban"
70
+ CRON = "cron"
71
+ SLASH = "slash"
72
+ WEBHOOK = "webhook"
73
+ MENTION = "mention"
74
+
75
+
76
+ class RuntimeKind(str, Enum):
77
+ HERMES_MANAGED = "hermes_managed"
78
+ CANDIDATE = "candidate"
79
+ ADAPTER = "adapter"
80
+
81
+
82
+ class RuntimeBindingStatus(str, Enum):
83
+ ACTIVE = "active"
84
+ DETACHED = "detached"
85
+ CANDIDATE = "candidate"
86
+
87
+
88
+ class FundingSource(str, Enum):
89
+ BYOK = "byok"
90
+ COMPANY = "company"
91
+ HYBRID = "hybrid"
92
+
93
+
94
+ class CredentialPolicy(str, Enum):
95
+ BYOK = "byok"
96
+ COMPANY = "company"
97
+ USER_ONLY = "user_only"
98
+
99
+
100
+ class LeaseStatus(str, Enum):
101
+ ACTIVE = "active"
102
+ EXPIRED = "expired"
103
+ REVOKED = "revoked"
104
+
105
+
106
+ class GrantCapability(str, Enum):
107
+ LLM_TURN = "llm_turn"
108
+ TOOL_CALL = "tool_call"
109
+ FILE_READ = "file_read"
110
+ FILE_WRITE = "file_write"
111
+ FILE_DELETE = "file_delete"
112
+ RUNTIME_EXEC = "runtime_exec"
113
+ BROKER_EGRESS = "broker_egress"
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class Cell(DomainEntity):
118
+ """One Workframe install/deployment unit (generated project root)."""
119
+
120
+ cell_id: str
121
+ install_root: str
122
+ manifest_path: str
123
+ package_name: str
124
+ package_version: str
125
+ deployment_mode: DeploymentMode
126
+ created_at: datetime
127
+ git_ref: str | None = None
128
+ packed_artifact_digest: str | None = None
129
+
130
+
131
+ @dataclass(frozen=True)
132
+ class User(DomainEntity):
133
+ user_id: str
134
+ workspace_id: str
135
+ email: str
136
+ display_name: str
137
+ role: Literal["owner", "admin", "member", "guest"] = "member"
138
+ created_at: datetime | None = None
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class Workspace(DomainEntity):
143
+ workspace_id: str
144
+ cell_id: str
145
+ slug: str
146
+ name: str
147
+ created_at: datetime | None = None
148
+
149
+
150
+ @dataclass(frozen=True)
151
+ class AgentIdentity(DomainEntity):
152
+ """Persistent Workframe agent — distinct from Hermes profile (WF-013 seam)."""
153
+
154
+ agent_id: str
155
+ workspace_id: str
156
+ template_slug: str
157
+ display_name: str
158
+ is_native: bool = False
159
+ created_at: datetime | None = None
160
+
161
+
162
+ @dataclass(frozen=True)
163
+ class RuntimeBinding(DomainEntity):
164
+ """Links AgentIdentity to a concrete runtime (Hermes profile today)."""
165
+
166
+ binding_id: str
167
+ agent_id: str
168
+ runtime_kind: RuntimeKind
169
+ profile_slug: str
170
+ status: RuntimeBindingStatus = RuntimeBindingStatus.ACTIVE
171
+ detected_at: datetime | None = None
172
+
173
+
174
+ @dataclass(frozen=True)
175
+ class Run(DomainEntity):
176
+ """Unit of authority, execution, and economics (WF-009, WF-NS-P2)."""
177
+
178
+ run_id: str
179
+ workspace_id: str
180
+ surface: RunSurface
181
+ actor_type: ActorType
182
+ actor_id: str
183
+ triggering_user_id: str
184
+ agent_id: str
185
+ runtime_binding_id: str
186
+ status: RunStatus
187
+ payer_user_id: str
188
+ funding_source: FundingSource
189
+ room_id: str | None = None
190
+ card_id: str | None = None
191
+ engine: str = "hermes"
192
+ runtime: str = "hermes_managed"
193
+ risk_tier: Literal["low", "medium", "high"] = "low"
194
+ budget_usd: float | None = None
195
+ deny_reason: str | None = None
196
+ created_at: datetime | None = None
197
+ started_at: datetime | None = None
198
+ ended_at: datetime | None = None
199
+
200
+
201
+ @dataclass(frozen=True)
202
+ class RunEvent(DomainEntity):
203
+ event_id: str
204
+ run_id: str
205
+ event_type: str
206
+ payload: dict[str, Any] = field(default_factory=dict)
207
+ created_at: datetime | None = None
208
+
209
+
210
+ @dataclass(frozen=True)
211
+ class CredentialRef(DomainEntity):
212
+ """Vault pointer — never holds raw secret material."""
213
+
214
+ ref_id: str
215
+ workspace_id: str
216
+ provider: str
217
+ policy: CredentialPolicy
218
+ user_id: str | None = None
219
+ binding_id: str | None = None
220
+ label_redacted: str | None = None
221
+
222
+
223
+ @dataclass(frozen=True)
224
+ class Lease(DomainEntity):
225
+ """Scoped credential lease for one run (maps to turn_credential_leases / wf_rt_*)."""
226
+
227
+ lease_id: str
228
+ run_id: str
229
+ workspace_id: str
230
+ payer_user_id: str
231
+ provider: str
232
+ profile_slug: str
233
+ credential_ref_id: str | None
234
+ status: LeaseStatus
235
+ expires_at: datetime
236
+ revoked_at: datetime | None = None
237
+ created_at: datetime | None = None
238
+
239
+
240
+ @dataclass(frozen=True)
241
+ class Grant(DomainEntity):
242
+ """Capability granted to a run before execution (RunAuthorityGate output)."""
243
+
244
+ grant_id: str
245
+ run_id: str
246
+ capability: GrantCapability
247
+ scope: dict[str, Any] = field(default_factory=dict)
248
+ granted_by: Literal["run_authority_gate", "cell_authority_gate", "policy"] = (
249
+ "run_authority_gate"
250
+ )
251
+ expires_at: datetime | None = None
252
+ created_at: datetime | None = None