create-workframe 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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-aUUffclm.js} +1 -1
  106. package/workframe-ui/public/assets/architecture-7EHR7CIX-BSUP4S8J.js +1 -0
  107. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-DeBNltHS.js → architectureDiagram-3BPJPVTR-DU6oaqIb.js} +1 -1
  108. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-BDhCHu7I.js → blockDiagram-GPEHLZMM-D2p8BU6-.js} +1 -1
  109. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-olcDYvtI.js → c4Diagram-AAUBKEIU-ouwyPmTJ.js} +1 -1
  110. package/workframe-ui/public/assets/channel-DOunZZ0X.js +1 -0
  111. package/workframe-ui/public/assets/{chunk-2J33WTMH-D0obCBn_.js → chunk-2J33WTMH-DsE7U5dj.js} +1 -1
  112. package/workframe-ui/public/assets/{chunk-3OPIFGDE-DX93f-ZZ.js → chunk-3OPIFGDE-DM0kkZ9h.js} +1 -1
  113. package/workframe-ui/public/assets/{chunk-4BX2VUAB-BX9B5wUs.js → chunk-4BX2VUAB-CQ8mAyXp.js} +1 -1
  114. package/workframe-ui/public/assets/{chunk-55IACEB6-C4DGc6RO.js → chunk-55IACEB6-DN1BDtbH.js} +1 -1
  115. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-Crlja9Lf.js → chunk-5ZQYHXKU-BzK2KqOt.js} +1 -1
  116. package/workframe-ui/public/assets/{chunk-727SXJPM-JYSm3szI.js → chunk-727SXJPM-C3AxtOi9.js} +1 -1
  117. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-_KslxVEl.js → chunk-AQP2D5EJ-4mVwEFuD.js} +1 -1
  118. package/workframe-ui/public/assets/{chunk-BSJP7CBP-CpTO-jU8.js → chunk-BSJP7CBP-23kN2q0A.js} +1 -1
  119. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-JGUkCmbI.js → chunk-CSCIHK7Q-CPBKVW-L.js} +2 -2
  120. package/workframe-ui/public/assets/{chunk-FMBD7UC4-BscBwGaC.js → chunk-FMBD7UC4-CKqcYq7K.js} +1 -1
  121. package/workframe-ui/public/assets/{chunk-KSCS5N6A-5ZTZ0bpX.js → chunk-KSCS5N6A-JRQnhxQE.js} +1 -1
  122. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-DxvitYbW.js → chunk-L5ZTLDWV-BLZrdIwa.js} +1 -1
  123. package/workframe-ui/public/assets/chunk-LZXEDZCA-DisG0XJT.js +2 -0
  124. package/workframe-ui/public/assets/{chunk-ND2GUHAM-Cayl0iV9.js → chunk-ND2GUHAM-ClAOxArS.js} +1 -1
  125. package/workframe-ui/public/assets/{chunk-NZK2D7GU-_7dyBR5G.js → chunk-NZK2D7GU-XUE1aNkm.js} +1 -1
  126. package/workframe-ui/public/assets/{chunk-O5CBEL6O--xTpD0yi.js → chunk-O5CBEL6O-DDN-Ifon.js} +1 -1
  127. package/workframe-ui/public/assets/chunk-QZHKN3VN-EVQ0VMd5.js +1 -0
  128. package/workframe-ui/public/assets/chunk-WU5MYG2G-k_-ZsLOS.js +1 -0
  129. package/workframe-ui/public/assets/{chunk-XPW4576I-CwxJQaeK.js → chunk-XPW4576I-B4_iYZ6r.js} +1 -1
  130. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-W9WVQsby.js +1 -0
  131. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-W9WVQsby.js +1 -0
  132. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-CCspENYv.js → cose-bilkent-S5V4N54A-Cy1HhS7j.js} +1 -1
  133. package/workframe-ui/public/assets/{dagre-BM42HDAG-Dv8V6R60.js → dagre-BM42HDAG-Bjal81Ie.js} +1 -1
  134. package/workframe-ui/public/assets/{diagram-2AECGRRQ-Da48hFPT.js → diagram-2AECGRRQ-v5Gdvzp_.js} +1 -1
  135. package/workframe-ui/public/assets/{diagram-5GNKFQAL-dTihc7-3.js → diagram-5GNKFQAL-DuyRdfmY.js} +1 -1
  136. package/workframe-ui/public/assets/{diagram-KO2AKTUF-D_Jqu699.js → diagram-KO2AKTUF-WwWCGbIW.js} +1 -1
  137. package/workframe-ui/public/assets/{diagram-LMA3HP47-Bt8PtEaZ.js → diagram-LMA3HP47-DTYwZALT.js} +1 -1
  138. package/workframe-ui/public/assets/{diagram-OG6HWLK6-BtmPjlh0.js → diagram-OG6HWLK6-B46kBIqE.js} +1 -1
  139. package/workframe-ui/public/assets/{dist-Cz2turIT.js → dist-mQWmB3z6.js} +1 -1
  140. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-BxMGCflX.js → erDiagram-TEJ5UH35-__40xO-y.js} +1 -1
  141. package/workframe-ui/public/assets/eventmodeling-FCH6USID-Cm1uRTxU.js +1 -0
  142. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-ivyroIZt.js → flowDiagram-I6XJVG4X-Cwj6dVUz.js} +1 -1
  143. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-B9llwShx.js → ganttDiagram-6RSMTGT7-D_IOSwAS.js} +1 -1
  144. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BFQHk4bw.js → gitGraph-WXDBUCRP-C1vGecee.js} +1 -1
  145. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-ursegLbc.js → gitGraphDiagram-PVQCEYII-DMTxFJxn.js} +1 -1
  146. package/workframe-ui/public/assets/index-BdiM4-lI.js +130 -0
  147. package/workframe-ui/public/assets/index-Fnad47vV.css +1 -0
  148. package/workframe-ui/public/assets/{info-J43DQDTF-DQpifAsB.js → info-J43DQDTF-CwjmyPmD.js} +1 -1
  149. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-C16XP2Q7.js → infoDiagram-5YYISTIA-BGzh7ZL2.js} +1 -1
  150. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-CiJoz7rP.js → ishikawaDiagram-YF4QCWOH-BLXdnR_5.js} +1 -1
  151. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CVwEvvUL.js → journeyDiagram-JHISSGLW-BK3behMe.js} +1 -1
  152. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-CWQ6hX5i.js → kanban-definition-UN3LZRKU-Bx172cCl.js} +1 -1
  153. package/workframe-ui/public/assets/{line-CgQsmU35.js → line-BTCEHb7l.js} +1 -1
  154. package/workframe-ui/public/assets/{linear-Bxbc77dL.js → linear-CkbydpnK.js} +1 -1
  155. package/workframe-ui/public/assets/{mermaid-parser.core-C_jMeF0a.js → mermaid-parser.core-DvrtArpk.js} +2 -2
  156. package/workframe-ui/public/assets/{mermaid.core-4RKV9MZP.js → mermaid.core-yj_1x_qj.js} +3 -3
  157. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-DZ7y7Sz0.js → mindmap-definition-RKZ34NQL-BXbMltQ0.js} +1 -1
  158. package/workframe-ui/public/assets/{packet-YPE3B663-B9h2I7Vq.js → packet-YPE3B663-9pVCBQ7S.js} +1 -1
  159. package/workframe-ui/public/assets/{pie-LRSECV5Y-B2mP5uHm.js → pie-LRSECV5Y-CdqoWesP.js} +1 -1
  160. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-DyYKyKiP.js → pieDiagram-4H26LBE5-kGs-VfXd.js} +1 -1
  161. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-CL_6nej-.js → quadrantDiagram-W4KKPZXB-D9D8gQw5.js} +1 -1
  162. package/workframe-ui/public/assets/{radar-GUYGQ44K-CNKNDQ7S.js → radar-GUYGQ44K-d4zMZpQS.js} +1 -1
  163. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-eWU_mhFa.js → requirementDiagram-4Y6WPE33-BI7EQ9zc.js} +1 -1
  164. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DrjcXvEQ.js → sankeyDiagram-5OEKKPKP-Bcz19jQB.js} +1 -1
  165. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-BHWR1xTJ.js → sequenceDiagram-3UESZ5HK-iQiCEfYp.js} +1 -1
  166. package/workframe-ui/public/assets/{src-DfJB8kV1.js → src-D6mvlP96.js} +1 -1
  167. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BGJbSd3I.js → stateDiagram-AJRCARHV-CxaTXs02.js} +1 -1
  168. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CXikj9xi.js +1 -0
  169. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DkwoCjiZ.js → timeline-definition-PNZ67QCA-Bej-EAnf.js} +1 -1
  170. package/workframe-ui/public/assets/{treeView-BLDUP644-GxfhiqoW.js → treeView-BLDUP644-C7PnnzwX.js} +1 -1
  171. package/workframe-ui/public/assets/{treemap-LRROVOQU-DxrOsars.js → treemap-LRROVOQU-CqISLmkO.js} +1 -1
  172. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-BsWhpUfr.js → vennDiagram-CIIHVFJN-eJMTTEW-.js} +1 -1
  173. package/workframe-ui/public/assets/{wardley-L42UT6IY-CrFZWMHS.js → wardley-L42UT6IY-D4sMroBF.js} +1 -1
  174. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DV8Xpf5I.js → wardleyDiagram-YWT4CUSO-DC3DCUs7.js} +1 -1
  175. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DqMqnwdZ.js → xychartDiagram-2RQKCTM6-BeMaM6Aq.js} +1 -1
  176. package/workframe-ui/public/index.html +7 -5
  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,542 @@
1
+ """Admin / meta / doctor / supervisor / data-read routes (WF-032 slice)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import sqlite3
9
+ import time
10
+ import urllib.parse
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import credential_vault
15
+ import internal_proxy_auth
16
+ import site_meta
17
+ import updates as stack_updates
18
+ from supervisor_client import _supervisor_request
19
+
20
+
21
+ def _srv():
22
+ import server
23
+
24
+ return server
25
+
26
+
27
+ class AdminRoutesMixin:
28
+
29
+ def _route_get_meta(self, qs: dict[str, list[str]]) -> None:
30
+ srv = _srv()
31
+ self._json(200, srv.workframe_meta())
32
+
33
+ def _route_get_health(self, qs: dict[str, list[str]]) -> None:
34
+ srv = _srv()
35
+ vault_stat = credential_vault.vault_status()
36
+ self._json(
37
+ 200,
38
+ {
39
+ "ok": True,
40
+ "version": srv.VERSION,
41
+ "mode": "dev_unsafe" if srv.DEV_LOCAL_UNSAFE else "secure",
42
+ "secure_mode": srv.SECURE_MODE,
43
+ "deployment_mode": srv.DEPLOYMENT_MODE,
44
+ "admin_updates_enabled": os.environ.get("WORKFRAME_ENABLE_ADMIN_UPDATES") == "1",
45
+ "docker_sock_on_api": Path("/var/run/docker.sock").exists(),
46
+ "proxy_token_configured": internal_proxy_auth.proxy_token_configured(),
47
+ "vault_sealed": vault_stat.get("sealed"),
48
+ "vault_envelope": not vault_stat.get("sealed"),
49
+ "install_window_open": srv._install_window_open(),
50
+ "workframe_e2e": os.environ.get("WORKFRAME_E2E") == "1",
51
+ "dev_local_unsafe": srv.DEV_LOCAL_UNSAFE,
52
+ },
53
+ )
54
+
55
+ def _route_get_setup_status(self, qs: dict[str, list[str]]) -> None:
56
+ srv = _srv()
57
+ try:
58
+ db = srv._workframe_db()
59
+ ws = db.execute("SELECT id FROM workspaces WHERE slug='default' AND deleted_at IS NULL").fetchone()
60
+ if not ws:
61
+ db.close()
62
+ self._json(200, {"setup_complete": False})
63
+ return
64
+ agents = db.execute(
65
+ "SELECT COUNT(*) AS c FROM agent_profiles WHERE workspace_id = ? AND deleted_at IS NULL",
66
+ (ws["id"],),
67
+ ).fetchone()
68
+ db.close()
69
+ self._json(200, {"setup_complete": bool(agents and agents["c"] > 0)})
70
+ except Exception:
71
+ self._json(200, {"setup_complete": False})
72
+
73
+ def _route_get_public_site_meta(self, qs: dict[str, list[str]]) -> None:
74
+ srv = _srv()
75
+ self._json(200, srv._public_site_meta_payload())
76
+
77
+ def _route_get_public_link_preview(self, qs: dict[str, list[str]]) -> None:
78
+ srv = _srv()
79
+ meta = srv._public_site_meta_payload()
80
+ html = site_meta.link_preview_html(meta).encode("utf-8")
81
+ self._send(200, html, "text/html; charset=utf-8")
82
+
83
+ def _route_get_public_manifest(self, qs: dict[str, list[str]]) -> None:
84
+ srv = _srv()
85
+ meta = srv._public_site_meta_payload()
86
+ body = json.dumps(site_meta.manifest_payload(meta), indent=2).encode("utf-8")
87
+ self._send(200, body, "application/manifest+json; charset=utf-8")
88
+
89
+ def _route_get_me_cohort(self, qs: dict[str, list[str]]) -> None:
90
+ srv = _srv()
91
+ user_id = str(getattr(self, "auth_user", "") or "")
92
+ if not user_id:
93
+ self._json(401, {"ok": False, "error": "no_session"})
94
+ return
95
+ workspace_id = str(qs.get("workspace_id", [""])[0] or "").strip()
96
+ if not workspace_id:
97
+ self._json(400, {"ok": False, "error": "workspace_id_required"})
98
+ return
99
+ ws_id = srv._resolve_wid(workspace_id) or workspace_id
100
+ if not srv._user_is_workspace_member(user_id, ws_id):
101
+ self._json(403, {"ok": False, "error": "forbidden"})
102
+ return
103
+ cohort = srv.ensure_user_agent_cohort(user_id, ws_id)
104
+ self._json(
105
+ 200,
106
+ {
107
+ "ok": True,
108
+ "workspace_id": ws_id,
109
+ "user_id": user_id,
110
+ "cohort": cohort,
111
+ },
112
+ )
113
+
114
+ def _route_get_agents(self, qs: dict[str, list[str]]) -> None:
115
+ srv = _srv()
116
+ self._json(200, srv.workframe_agents())
117
+
118
+ def _route_get_snapshot(self, qs: dict[str, list[str]]) -> None:
119
+ srv = _srv()
120
+ self._json(200, srv.build_snapshot())
121
+
122
+ def _route_get_activity_detail(self, qs: dict[str, list[str]]) -> None:
123
+ srv = _srv()
124
+ profile = qs.get("profile", [""])[0].strip()
125
+ tool_call_id = qs.get("tool_call_id", [""])[0].strip()
126
+ session_id = qs.get("session_id", [""])[0].strip()
127
+ message_id = qs.get("message_id", [""])[0].strip()
128
+ self._json(200, srv.activity_detail(profile, tool_call_id, session_id, message_id))
129
+
130
+ def _route_get_files_tree(self, qs: dict[str, list[str]]) -> None:
131
+ srv = _srv()
132
+ self._json(200, {"root": srv.files_tree()})
133
+
134
+ def _route_get_files_list(self, qs: dict[str, list[str]]) -> None:
135
+ srv = _srv()
136
+ rel = qs.get("path", [""])[0]
137
+ reason = srv._workspace_protected_reason(rel)
138
+ if reason:
139
+ self._json(403, {"ok": False, "error": "protected_file", "reason": reason})
140
+ return
141
+ self._json(200, srv.files_list(rel))
142
+
143
+ def _route_get_files_state(self, qs: dict[str, list[str]]) -> None:
144
+ srv = _srv()
145
+ self._json(200, srv.workspace_state())
146
+
147
+ def _route_get_files_read(self, qs: dict[str, list[str]]) -> None:
148
+ srv = _srv()
149
+ rel = qs.get("path", [""])[0]
150
+ reason = srv._workspace_protected_reason(rel)
151
+ if reason:
152
+ self._json(403, {"ok": False, "error": "protected_file", "reason": reason})
153
+ return
154
+ self._json(200, srv.file_read(rel))
155
+
156
+ def _route_get_files_raw(self, qs: dict[str, list[str]]) -> None:
157
+ srv = _srv()
158
+ rel = qs.get("path", [""])[0]
159
+ reason = srv._workspace_protected_reason(rel)
160
+ if reason:
161
+ self._json(403, {"ok": False, "error": "protected_file", "reason": reason})
162
+ return
163
+ try:
164
+ data, mime = srv.file_raw(rel)
165
+ except ValueError:
166
+ self._json(404, {"error": "not found"})
167
+ return
168
+ self._send(200, data, mime)
169
+
170
+ def _route_get_routes(self, qs: dict[str, list[str]]) -> None:
171
+ srv = _srv()
172
+ self._json(200, srv.load_routes())
173
+
174
+ def _route_post_files_write(self, body: dict) -> None:
175
+ srv = _srv()
176
+ if not srv._check_auth(self):
177
+ self._json(401, {"error": "unauthorized"})
178
+ return
179
+ rel = str(body.get("path") or "").strip()
180
+ reason = srv._workspace_protected_reason(rel)
181
+ if reason:
182
+ self._json(403, {"ok": False, "error": "protected_file", "reason": reason})
183
+ return
184
+ content = str(body.get("content") or "")
185
+ self._json(200, srv.file_write(rel, content))
186
+
187
+ def _route_post_files_upload(self, body: dict) -> None:
188
+ srv = _srv()
189
+ if not srv._check_auth(self):
190
+ self._json(401, {"error": "unauthorized"})
191
+ return
192
+ rel = str(body.get("path") or "").strip()
193
+ reason = srv._workspace_protected_reason(rel)
194
+ if reason:
195
+ self._json(403, {"ok": False, "error": "protected_file", "reason": reason})
196
+ return
197
+ content_b64 = str(body.get("content_base64") or body.get("content") or "")
198
+ self._json(200, srv.file_upload_binary(rel, content_b64))
199
+
200
+ def _route_get_admin_vault_status(self, qs: dict[str, list[str]]) -> None:
201
+ srv = _srv()
202
+ if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
203
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
204
+ return
205
+ self._json(200, {"ok": True, **credential_vault.vault_status()})
206
+
207
+ def _route_get_doctor_agent_dm_runtimes(self, qs: dict[str, list[str]]) -> None:
208
+ srv = _srv()
209
+ if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
210
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
211
+ return
212
+ self._json(200, srv.doctor_audit_agent_dm_runtimes())
213
+
214
+ def _route_get_admin_updates(self, qs: dict[str, list[str]]) -> None:
215
+ srv = _srv()
216
+ if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
217
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
218
+ return
219
+ desktop_version = str((qs.get("desktop_version") or [""])[0] or "").strip()
220
+ try:
221
+ self._json(
222
+ 200,
223
+ stack_updates.updates_available(
224
+ desktop_version=desktop_version,
225
+ hermes_agent_version=srv._hermes_agent_version(),
226
+ ),
227
+ )
228
+ except Exception as exc: # noqa: BLE001
229
+ self._json(500, {"ok": False, "error": str(exc)})
230
+
231
+ def _route_post_admin_updates_apply(self, body: dict) -> None:
232
+ srv = _srv()
233
+ if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
234
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
235
+ return
236
+ if not srv._admin_write_allowed(self):
237
+ self._json(403, {"ok": False, "error": "admin_write_forbidden", "hint": "X-Workframe-Local required in dev-unsafe"})
238
+ return
239
+ target = str(body.get("target") or "all").strip().lower()
240
+ if target not in {"hermes", "workframe", "all"}:
241
+ self._json(400, {"ok": False, "error": "invalid_target"})
242
+ return
243
+ user_ack = body.get("user_ack") is True or str(body.get("user_ack") or "").lower() in {
244
+ "1",
245
+ "true",
246
+ "yes",
247
+ }
248
+ try:
249
+ result = srv._apply_stack_update(target, user_ack=user_ack)
250
+ self._log_audit("stack_update_apply", "stack", target, str(result.get("ok")))
251
+ self._json(200 if result.get("ok") else 500, result)
252
+ except (OSError, RuntimeError, ValueError) as exc:
253
+ srv._log_handler_error("POST /api/admin/updates/apply", exc)
254
+ self._json(500, {"ok": False, "error": str(exc)})
255
+
256
+ def _route_post_admin_stack_restart_gateway(self, body: dict) -> None:
257
+ srv = _srv()
258
+ if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
259
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
260
+ return
261
+ if not srv._admin_write_allowed(self):
262
+ self._json(403, {"ok": False, "error": "admin_write_forbidden", "hint": "X-Workframe-Local required in dev-unsafe"})
263
+ return
264
+ try:
265
+ result = srv._restart_stack_gateway()
266
+ self._log_audit("stack_restart_gateway", "stack", "gateway", str(result.get("ok")))
267
+ self._json(200 if result.get("ok") else 500, result)
268
+ except (OSError, RuntimeError, ValueError) as exc:
269
+ srv._log_handler_error("POST /api/admin/stack/restart-gateway", exc)
270
+ self._json(500, {"ok": False, "error": str(exc)})
271
+
272
+ def _route_post_admin_vault_init(self, body: dict) -> None:
273
+ srv = _srv()
274
+ if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
275
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
276
+ return
277
+ if not srv._admin_write_allowed(self):
278
+ self._json(403, {"ok": False, "error": "admin_write_forbidden"})
279
+ return
280
+ passphrase = str(body.get("passphrase") or "").strip()
281
+ if not passphrase:
282
+ self._json(400, {"ok": False, "error": "passphrase required"})
283
+ return
284
+ try:
285
+ status = credential_vault.init_vault_passphrase(passphrase)
286
+ self._log_audit("vault_passphrase_init", "vault", "meta", "passphrase_enabled")
287
+ self._json(200, {"ok": True, **status})
288
+ except ValueError as exc:
289
+ self._json(400, {"ok": False, "error": str(exc)})
290
+ except (RuntimeError, OSError) as exc:
291
+ srv._log_handler_error("POST /api/admin/vault/init", exc)
292
+ self._json(500, {"ok": False, "error": str(exc)})
293
+
294
+ def _route_post_admin_vault_unlock(self, body: dict) -> None:
295
+ srv = _srv()
296
+ if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
297
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
298
+ return
299
+ passphrase = str(body.get("passphrase") or "").strip()
300
+ if not passphrase:
301
+ self._json(400, {"ok": False, "error": "passphrase required"})
302
+ return
303
+ try:
304
+ status = credential_vault.unlock_vault(passphrase)
305
+ self._log_audit("vault_unlock", "vault", "meta", "unsealed")
306
+ self._json(200, {"ok": True, **status})
307
+ except ValueError as exc:
308
+ self._log_audit("vault_unlock_failed", "vault", "meta", str(exc))
309
+ self._json(403, {"ok": False, "error": str(exc)})
310
+ except (RuntimeError, OSError) as exc:
311
+ srv._log_handler_error("POST /api/admin/vault/unlock", exc)
312
+ self._json(500, {"ok": False, "error": str(exc)})
313
+
314
+ def _route_post_admin_vault_seal(self, body: dict) -> None:
315
+ srv = _srv()
316
+ if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
317
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
318
+ return
319
+ if not srv._admin_write_allowed(self):
320
+ self._json(403, {"ok": False, "error": "admin_write_forbidden"})
321
+ return
322
+ status = credential_vault.seal_vault()
323
+ self._log_audit("vault_seal", "vault", "meta", "sealed")
324
+ self._json(200, {"ok": True, **status})
325
+
326
+ def _route_post_admin_vault_wipe(self, body: dict) -> None:
327
+ srv = _srv()
328
+ if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
329
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
330
+ return
331
+ if not srv._admin_write_allowed(self):
332
+ self._json(403, {"ok": False, "error": "admin_write_forbidden"})
333
+ return
334
+ if not body.get("confirm"):
335
+ self._json(400, {"ok": False, "error": "confirm required"})
336
+ return
337
+ deleted = credential_vault.wipe_all_secrets()
338
+ credential_vault.seal_vault()
339
+ self._log_audit("vault_wipe", "vault", "secrets", f"deleted={deleted}")
340
+ self._json(200, {"ok": True, "deleted": deleted, **credential_vault.vault_status()})
341
+
342
+ def _route_get_supervisor_profile_status(self, qs: dict[str, list[str]]) -> None:
343
+ srv = _srv()
344
+ profile = qs.get("profile", [""])[0]
345
+ if not profile:
346
+ self._json(400, {"error": "profile required"})
347
+ return
348
+ if srv.SUPERVISOR_URL:
349
+ self._json(
350
+ *_supervisor_request(
351
+ "GET",
352
+ f"/v1/profile.status?profile={urllib.parse.quote(profile, safe='')}",
353
+ timeout=10.0,
354
+ )
355
+ )
356
+ return
357
+ self._json(503, {"ok": False, "error": "WORKFRAME_SUPERVISOR_URL not configured"})
358
+
359
+ def _route_get_supervisor_stack_status(self, qs: dict[str, list[str]]) -> None:
360
+ srv = _srv()
361
+ if srv.SUPERVISOR_URL:
362
+ self._json(*_supervisor_request("GET", "/v1/stack.status", timeout=15.0))
363
+ return
364
+ self._json(503, {"ok": False, "error": "WORKFRAME_SUPERVISOR_URL not configured"})
365
+
366
+ def _route_get_admin_audit(self, qs: dict[str, list[str]]) -> None:
367
+ srv = _srv()
368
+ if srv.SECURE_MODE and not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
369
+ self._json(403, {"ok": False, "error": "forbidden"})
370
+ return
371
+ try:
372
+ limit = min(int(qs.get("limit", ["50"])[0]), 200)
373
+ offset = max(int(qs.get("offset", ["0"])[0]), 0)
374
+ except (ValueError, IndexError):
375
+ limit, offset = 50, 0
376
+ event_type_filter = str(qs.get("event_type", [""])[0]).strip()
377
+ user_filter = str(qs.get("user_id", [""])[0]).strip()
378
+ try:
379
+ conn = sqlite3.connect(str(srv._workframe_db_path()), timeout=3.0)
380
+ conn.row_factory = sqlite3.Row
381
+ where_clauses: list[str] = []
382
+ params: list = []
383
+ if event_type_filter:
384
+ where_clauses.append("event_type = ?")
385
+ params.append(event_type_filter)
386
+ if user_filter:
387
+ where_clauses.append("user_id = ?")
388
+ params.append(user_filter)
389
+ where_str = ("WHERE " + " AND ".join(where_clauses)) if where_clauses else ""
390
+ rows = conn.execute(
391
+ f"""SELECT id, user_id, event_type, target_type, target_id,
392
+ summary, metadata, ip_address, created_at
393
+ FROM audit_events
394
+ {where_str}
395
+ ORDER BY created_at DESC LIMIT ? OFFSET ?""",
396
+ params + [limit, offset],
397
+ ).fetchall()
398
+ total = conn.execute(
399
+ f"SELECT COUNT(*) AS c FROM audit_events {where_str}",
400
+ params,
401
+ ).fetchone()["c"]
402
+ conn.close()
403
+ except sqlite3.Error as exc:
404
+ self._json(500, {"ok": False, "error": str(exc)})
405
+ return
406
+ self._json(200, {
407
+ "ok": True,
408
+ "total": total,
409
+ "limit": limit,
410
+ "offset": offset,
411
+ "events": [dict(r) for r in rows],
412
+ })
413
+
414
+ def _route_get_events(self, qs: dict[str, list[str]]) -> None:
415
+ srv = _srv()
416
+ self.send_response(200)
417
+ self.send_header("Content-Type", "text/event-stream; charset=utf-8")
418
+ self.send_header("Cache-Control", "no-cache")
419
+ self.send_header("Connection", "keep-alive")
420
+ cors_origin = srv._cors_origin_for(self.headers)
421
+ if cors_origin:
422
+ self.send_header("Access-Control-Allow-Origin", cors_origin)
423
+ self.send_header("Vary", "Origin")
424
+ self.end_headers()
425
+ try:
426
+ while True:
427
+ payload = json.dumps(srv.build_snapshot())
428
+ self.wfile.write(f"data: {payload}\n\n".encode("utf-8"))
429
+ self.wfile.flush()
430
+ time.sleep(srv.SSE_INTERVAL)
431
+ except (BrokenPipeError, ConnectionResetError, OSError):
432
+ return
433
+
434
+ def _route_pattern_get_public_branding(self, path: str, qs: dict[str, list[str]]) -> None:
435
+ srv = _srv()
436
+ branding_match = re.fullmatch(r"/api/public/branding/(og|favicon)", path)
437
+ if not branding_match:
438
+ self._json(404, {"ok": False, "error": "not_found"})
439
+ return
440
+ asset = site_meta.branding_asset_bytes(branding_match.group(1))
441
+ if not asset:
442
+ self._json(404, {"ok": False, "error": "not_found"})
443
+ return
444
+ data, ctype = asset
445
+ self._send(200, data, ctype)
446
+
447
+ def _supervisor_proxy_post(self, subpath: str, body: dict) -> None:
448
+ srv = _srv()
449
+ if not srv._check_auth(self):
450
+ self._json(401, {"error": "unauthorized"})
451
+ return
452
+ if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
453
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
454
+ return
455
+ if srv.SUPERVISOR_URL:
456
+ self._json(*_supervisor_request("POST", subpath, body))
457
+ return
458
+ self._json(503, {"ok": False, "error": "WORKFRAME_SUPERVISOR_URL not configured"})
459
+
460
+ def _route_post_supervisor_profile_start(self, body: dict) -> None:
461
+ srv = _srv()
462
+ self._supervisor_proxy_post("/v1/profile.start", body)
463
+
464
+ def _route_post_supervisor_profile_stop(self, body: dict) -> None:
465
+ srv = _srv()
466
+ self._supervisor_proxy_post("/v1/profile.stop", body)
467
+
468
+ def _route_post_supervisor_profile_disable(self, body: dict) -> None:
469
+ srv = _srv()
470
+ self._supervisor_proxy_post("/v1/profile.disable", body)
471
+
472
+ def _route_pattern_post_memory(self, path: str, body: dict) -> None:
473
+ srv = _srv()
474
+ parts = path.strip("/").split("/")
475
+ if len(parts) != 3:
476
+ self._json(404, {"error": "not_found"})
477
+ return
478
+ mem_id = parts[2]
479
+ user_id = str(getattr(self, "auth_user", "") or "")
480
+ try:
481
+ conn = sqlite3.connect(str(srv.AUTH_DB_PATH.parent / "workframe.db"), timeout=3.0)
482
+ conn.row_factory = sqlite3.Row
483
+ item = conn.execute("SELECT * FROM memory_items WHERE id = ? AND deleted_at IS NULL", (mem_id,)).fetchone()
484
+ if not item:
485
+ conn.close()
486
+ self._json(404, {"error": "memory_not_found"})
487
+ return
488
+ if not srv.DEV_LOCAL_UNSAFE and str(item["created_by_user_id"]) != user_id:
489
+ auth_role = str(getattr(self, "auth_role", ""))
490
+ if auth_role not in srv.OWNER_ADMIN_ROLES:
491
+ conn.close()
492
+ self._json(403, {"error": "forbidden"})
493
+ return
494
+ now_ts = str(int(time.time()))
495
+ conn.execute("UPDATE memory_items SET deleted_at = ?, updated_at = ? WHERE id = ?", (now_ts, now_ts, mem_id))
496
+ conn.commit()
497
+ conn.close()
498
+ except sqlite3.Error as exc:
499
+ self._json(500, {"error": f"db_error: {exc}"})
500
+ return
501
+ self._log_audit("memory_deleted", "memory_item", mem_id, f"user={user_id}")
502
+ self._json(200, {"ok": True, "memory_id": mem_id, "status": "deleted"})
503
+
504
+ def _route_pattern_delete_memory(self, path: str, body: dict) -> None:
505
+ srv = _srv()
506
+ parts = path.strip("/").split("/")
507
+ if len(parts) != 3:
508
+ self._json(404, {"ok": False, "error": "memory_not_found"})
509
+ return
510
+ mem_id = parts[2]
511
+ try:
512
+ db = srv._workframe_db()
513
+ cur = db.execute(
514
+ "UPDATE memory_items SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL",
515
+ (str(int(time.time())), mem_id),
516
+ )
517
+ db.commit()
518
+ affected = cur.rowcount
519
+ db.close()
520
+ except Exception:
521
+ self._json(500, {"ok": False, "error": "delete_failed"})
522
+ return
523
+ if not affected:
524
+ self._json(404, {"ok": False, "error": "memory_not_found"})
525
+ return
526
+ self._json(200, {"ok": True, "memory_id": mem_id, "status": "deleted"})
527
+
528
+ def _route_patch_doctor_repair(self, body: dict) -> None:
529
+ srv = _srv()
530
+ if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
531
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
532
+ return
533
+ repair = body.get("repair", True) is not False
534
+ result = srv.doctor_repair_agent_dm_runtimes(repair=repair)
535
+ if repair:
536
+ self._log_audit(
537
+ "doctor_repair_agent_dm_runtimes",
538
+ "runtime_profile",
539
+ "",
540
+ f"repaired={len(result.get('repaired') or [])} failed={len(result.get('failed') or [])}",
541
+ )
542
+ self._json(200, result)