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,344 @@
1
+ """WF-NS-P2: run ledger wiring for slash, cron, and webhook surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from typing import Any, Mapping
7
+
8
+ import run_authority
9
+ import run_ledger
10
+ from domain.entities import ActorType, FundingSource, RunStatus, RunSurface
11
+
12
+ # ponytail: slash commands that never bill LLM — audit run only, no authority gate
13
+ SLASH_NO_AUTHORITY = frozenset(
14
+ {
15
+ "help",
16
+ "status",
17
+ "cron",
18
+ "skills",
19
+ "skill",
20
+ "memory",
21
+ "sessions",
22
+ "profile",
23
+ "auth",
24
+ "config",
25
+ "kanban",
26
+ "board",
27
+ "version",
28
+ "whoami",
29
+ "stop",
30
+ "steer",
31
+ }
32
+ )
33
+
34
+
35
+ def _srv():
36
+ import server as srv
37
+
38
+ return srv
39
+
40
+
41
+ def authority_context_for_user(
42
+ user_id: str,
43
+ workspace_id: str,
44
+ provider: str,
45
+ ) -> run_authority.RunAuthorityContext:
46
+ user = str(user_id or "").strip()
47
+ ws = str(workspace_id or "").strip()
48
+ provider_name = str(provider or "openrouter").strip().lower()
49
+ mode = _srv()._workspace_credential_mode(None, ws)
50
+ user_only = _srv()._provider_user_only(provider_name)
51
+ oauth_connected = False
52
+ oauth_spec = _srv()._oauth_llm_provider_spec(provider_name)
53
+ if oauth_spec and user:
54
+ oauth_connected = _srv()._hermes_oauth_tokens_present(
55
+ user, _srv()._hermes_auth_id_for_spec(oauth_spec),
56
+ )
57
+ user_resolved = _srv()._resolve_credential(user, ws, provider_name, user_only=True) if user else None
58
+ user_has = bool(user_resolved and _srv()._credential_secret(user_resolved, user))
59
+ ws_has = False
60
+ if ws and not user_only:
61
+ ws_resolved = _srv()._resolve_credential(user, ws, provider_name, user_only=False)
62
+ ws_has = bool(ws_resolved and _srv()._credential_secret(ws_resolved, user))
63
+ grantors: dict[str, bool] = {}
64
+ if user and ws:
65
+ for grantor_id in _srv()._delegation_grantor_ids_for_grantee(user, ws):
66
+ g_resolved = _srv()._resolve_credential(grantor_id, ws, provider_name, user_only=True)
67
+ grantors[grantor_id] = bool(g_resolved and _srv()._credential_secret(g_resolved, grantor_id))
68
+ return run_authority.RunAuthorityContext(
69
+ workspace_credential_mode=mode,
70
+ provider_user_only=user_only,
71
+ user_has_credential=user_has,
72
+ workspace_has_credential=ws_has,
73
+ grantor_has_credential=grantors,
74
+ oauth_connected=oauth_connected,
75
+ )
76
+
77
+
78
+ def _resolve_workspace_id(user_id: str, workspace_id: str) -> str:
79
+ ws = str(workspace_id or "").strip()
80
+ if ws:
81
+ return ws
82
+ user = str(user_id or "").strip()
83
+ if not user:
84
+ return "default"
85
+ try:
86
+ conn = _srv()._workframe_db()
87
+ row = conn.execute(
88
+ "SELECT current_workspace_id FROM users WHERE id = ? AND deleted_at IS NULL",
89
+ (user,),
90
+ ).fetchone()
91
+ conn.close()
92
+ if row and row["current_workspace_id"]:
93
+ return str(row["current_workspace_id"])
94
+ except Exception: # noqa: BLE001
95
+ pass
96
+ return "default"
97
+
98
+
99
+ def slash_requires_authority(cmd_token: str) -> bool:
100
+ return str(cmd_token or "").strip().lower() not in SLASH_NO_AUTHORITY
101
+
102
+
103
+ def record_gated_surface_run(
104
+ *,
105
+ surface: RunSurface,
106
+ build_request: Any,
107
+ triggering_user_id: str,
108
+ profile_slug: str,
109
+ workspace_id: str,
110
+ provider: str,
111
+ room_id: str | None = None,
112
+ session_id: str | None = None,
113
+ run_id: str | None = None,
114
+ ) -> tuple[str, run_authority.RunAuthorityDecision]:
115
+ """Run authority gate + ledger row for user-triggered surfaces (slash with LLM)."""
116
+ rid = str(run_id or uuid.uuid4())
117
+ ws = _resolve_workspace_id(triggering_user_id, workspace_id)
118
+ run_ledger.ensure_schema()
119
+ auth_req = build_request(
120
+ triggering_user_id=triggering_user_id,
121
+ profile_slug=profile_slug,
122
+ workspace_id=ws,
123
+ provider=provider,
124
+ room_id=room_id,
125
+ )
126
+ auth_ctx = authority_context_for_user(triggering_user_id, ws, provider)
127
+ decision = run_authority.evaluate_run_authority(auth_req, auth_ctx, run_id=rid)
128
+ conn = _srv()._workframe_db()
129
+ try:
130
+ run_ledger.record_authority_decision(
131
+ conn,
132
+ run_id=rid,
133
+ request_surface=surface,
134
+ actor_type=auth_req.actor_type,
135
+ actor_id=auth_req.actor_id,
136
+ triggering_user_id=triggering_user_id,
137
+ workspace_id=ws,
138
+ agent_id=auth_req.agent_id,
139
+ runtime_binding_id=auth_req.runtime_binding_id,
140
+ profile_slug=profile_slug,
141
+ provider=provider,
142
+ room_id=room_id,
143
+ session_id=session_id,
144
+ decision=decision,
145
+ )
146
+ conn.commit()
147
+ finally:
148
+ conn.close()
149
+ return rid, decision
150
+
151
+
152
+ def record_audit_surface_run(
153
+ *,
154
+ surface: RunSurface,
155
+ actor_type: ActorType,
156
+ actor_id: str,
157
+ triggering_user_id: str,
158
+ profile_slug: str,
159
+ workspace_id: str,
160
+ event_type: str,
161
+ payload: Mapping[str, Any] | None = None,
162
+ room_id: str | None = None,
163
+ provider: str = "",
164
+ payer_user_id: str = "",
165
+ funding_source: FundingSource = FundingSource.BYOK,
166
+ ) -> str:
167
+ """Audit-only run row (no LLM authority gate) — slash meta commands, kanban-style."""
168
+ rid = str(uuid.uuid4())
169
+ ws = _resolve_workspace_id(triggering_user_id, workspace_id)
170
+ payer = str(payer_user_id or triggering_user_id or actor_id).strip()
171
+ run_ledger.ensure_schema()
172
+ conn = _srv()._workframe_db()
173
+ try:
174
+ run_ledger.insert_run(
175
+ conn,
176
+ run_id=rid,
177
+ workspace_id=ws,
178
+ surface=surface,
179
+ actor_type=actor_type,
180
+ actor_id=actor_id,
181
+ triggering_user_id=triggering_user_id or payer,
182
+ agent_id=profile_slug,
183
+ runtime_binding_id=profile_slug,
184
+ status=RunStatus.RUNNING,
185
+ payer_user_id=payer,
186
+ funding_source=funding_source,
187
+ room_id=room_id,
188
+ profile_slug=profile_slug,
189
+ provider=provider or None,
190
+ )
191
+ run_ledger.insert_run_event(
192
+ conn,
193
+ run_id=rid,
194
+ event_type=event_type,
195
+ payload=dict(payload or {}),
196
+ room_id=room_id,
197
+ )
198
+ conn.commit()
199
+ finally:
200
+ conn.close()
201
+ return rid
202
+
203
+
204
+ def record_automated_surface_run(body: dict[str, Any]) -> dict[str, Any]:
205
+ """Internal hook for Hermes cron/webhook — POST /internal/runs/record."""
206
+ surface_raw = str(body.get("surface") or "").strip().lower()
207
+ if surface_raw == "cron":
208
+ surface = RunSurface.CRON
209
+ default_actor = ActorType.SYSTEM
210
+ elif surface_raw == "webhook":
211
+ surface = RunSurface.WEBHOOK
212
+ default_actor = ActorType.WEBHOOK
213
+ else:
214
+ raise ValueError("surface must be cron or webhook")
215
+
216
+ actor_type_raw = str(body.get("actor_type") or default_actor.value).strip().lower()
217
+ try:
218
+ actor_type = ActorType(actor_type_raw)
219
+ except ValueError as exc:
220
+ raise ValueError("invalid actor_type") from exc
221
+
222
+ profile_slug = _srv().safe_profile_slug(str(body.get("profile_slug") or "").strip())
223
+ if not profile_slug:
224
+ raise ValueError("profile_slug required")
225
+
226
+ triggering_user_id = str(body.get("triggering_user_id") or "").strip()
227
+ if not triggering_user_id:
228
+ triggering_user_id = _srv()._runtime_profile_owner(profile_slug, str(body.get("workspace_id") or ""))
229
+
230
+ workspace_id = _resolve_workspace_id(triggering_user_id, str(body.get("workspace_id") or ""))
231
+ actor_id = str(body.get("actor_id") or f"{surface_raw}:unknown").strip()
232
+ event_type = str(body.get("event_type") or f"{surface_raw}.triggered").strip()
233
+ payload = body.get("payload") if isinstance(body.get("payload"), dict) else {}
234
+ room_id = str(body.get("room_id") or "").strip() or None
235
+ provider = str(body.get("provider") or "").strip()
236
+ if not provider:
237
+ block = _srv()._read_model_block(profile_slug)
238
+ provider = _srv()._llm_billing_provider(
239
+ profile_slug,
240
+ user_id=triggering_user_id,
241
+ workspace_id=workspace_id,
242
+ block=block,
243
+ )
244
+
245
+ run_id = record_audit_surface_run(
246
+ surface=surface,
247
+ actor_type=actor_type,
248
+ actor_id=actor_id,
249
+ triggering_user_id=triggering_user_id,
250
+ profile_slug=profile_slug,
251
+ workspace_id=workspace_id,
252
+ event_type=event_type,
253
+ payload={**payload, "provider": provider, "actor_id": actor_id},
254
+ room_id=room_id,
255
+ provider=provider,
256
+ )
257
+ finish_surface_run(run_id, ok=True)
258
+ return {"ok": True, "run_id": run_id, "surface": surface.value}
259
+
260
+
261
+ def finish_surface_run(run_id: str, *, ok: bool, detail: str = "") -> None:
262
+ rid = str(run_id or "").strip()
263
+ if not rid:
264
+ return
265
+ run_ledger.ensure_schema()
266
+ conn = _srv()._workframe_db()
267
+ try:
268
+ row = conn.execute(
269
+ "SELECT payer_user_id, funding_source, provider, profile_slug FROM runs WHERE run_id = ?",
270
+ (rid,),
271
+ ).fetchone()
272
+ if not row:
273
+ return
274
+ if ok:
275
+ run_ledger.complete_run(
276
+ conn,
277
+ rid,
278
+ model="",
279
+ provider=str(row["provider"] or ""),
280
+ funding_source=FundingSource(str(row["funding_source"] or FundingSource.BYOK.value)),
281
+ payer_user_id=str(row["payer_user_id"] or ""),
282
+ receipt={"detail": detail[:500]} if detail else {},
283
+ )
284
+ else:
285
+ run_ledger.fail_run(conn, rid, reason=detail[:500] if detail else "failed")
286
+ conn.commit()
287
+ finally:
288
+ conn.close()
289
+
290
+
291
+ def begin_slash_run(
292
+ *,
293
+ cmd_token: str,
294
+ line: str,
295
+ profile_slug: str,
296
+ user_id: str,
297
+ workspace_id: str,
298
+ ) -> tuple[str | None, run_authority.RunAuthorityDecision | None]:
299
+ """Create slash run ledger row; return (run_id, decision) or (None, None) when anonymous."""
300
+ user = str(user_id or "").strip()
301
+ if not user:
302
+ return None, None
303
+
304
+ prof = _srv().safe_profile_slug(profile_slug)
305
+ ws = _resolve_workspace_id(user, workspace_id)
306
+ block = _srv()._read_model_block(prof)
307
+ provider = _srv()._llm_billing_provider(prof, user_id=user, workspace_id=ws, block=block)
308
+ payload = {"command": cmd_token, "line": line[:200]}
309
+
310
+ if not slash_requires_authority(cmd_token):
311
+ rid = record_audit_surface_run(
312
+ surface=RunSurface.SLASH,
313
+ actor_type=ActorType.USER,
314
+ actor_id=user,
315
+ triggering_user_id=user,
316
+ profile_slug=prof,
317
+ workspace_id=ws,
318
+ event_type="slash.command",
319
+ payload=payload,
320
+ provider=provider,
321
+ )
322
+ return rid, None
323
+
324
+ rid, decision = record_gated_surface_run(
325
+ surface=RunSurface.SLASH,
326
+ build_request=run_authority.slash_run_request,
327
+ triggering_user_id=user,
328
+ profile_slug=prof,
329
+ workspace_id=ws,
330
+ provider=provider,
331
+ )
332
+ run_ledger.ensure_schema()
333
+ conn = _srv()._workframe_db()
334
+ try:
335
+ run_ledger.insert_run_event(
336
+ conn,
337
+ run_id=rid,
338
+ event_type="slash.command",
339
+ payload=payload,
340
+ )
341
+ conn.commit()
342
+ finally:
343
+ conn.close()
344
+ return rid, decision