create-workframe 0.1.11 → 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 (195) 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 +85 -0
  5. package/package.json +1 -1
  6. package/scripts/bundle-workframe-ui.mjs +9 -0
  7. package/scripts/sync-canonical-to-package.mjs +9 -0
  8. package/scripts/verify-public-deploy.sh +121 -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/openrouter_catalog.py +4 -4
  51. package/workframe-api/package.json +2 -2
  52. package/workframe-api/profile_api_lifecycle.py +66 -0
  53. package/workframe-api/profile_config_yaml.py +126 -0
  54. package/workframe-api/profile_gateway.py +436 -0
  55. package/workframe-api/provider_bindings.py +673 -0
  56. package/workframe-api/provider_bootstrap.py +347 -0
  57. package/workframe-api/provider_catalog.py +180 -0
  58. package/workframe-api/rooms.py +1613 -0
  59. package/workframe-api/route_registry.py +470 -0
  60. package/workframe-api/run-typecheck.mjs +26 -0
  61. package/workframe-api/run_authority.py +287 -0
  62. package/workframe-api/run_ledger.py +470 -0
  63. package/workframe-api/run_surface_wiring.py +344 -0
  64. package/workframe-api/runtime_cohort.py +752 -0
  65. package/workframe-api/runtime_tokens.py +127 -0
  66. package/workframe-api/server.py +1376 -19305
  67. package/workframe-api/snapshot_feed.py +49 -0
  68. package/workframe-api/stack_config.py +33 -1
  69. package/workframe-api/supervisor_client.py +154 -0
  70. package/workframe-api/test_api_meta_build_stamp.py +34 -0
  71. package/workframe-api/test_cell_authority.py +79 -0
  72. package/workframe-api/test_chat_bind.py +48 -0
  73. package/workframe-api/test_credential_lease_matrix.py +171 -0
  74. package/workframe-api/test_credential_resolve.py +51 -0
  75. package/workframe-api/test_credential_store.py +27 -0
  76. package/workframe-api/test_dogfood_flows.py +346 -0
  77. package/workframe-api/test_domain_entities.py +152 -0
  78. package/workframe-api/test_exception_hygiene.py +12 -0
  79. package/workframe-api/test_hermes_admin.py +72 -0
  80. package/workframe-api/test_install_wizard_resume.py +78 -0
  81. package/workframe-api/test_local_bootstrap.py +67 -0
  82. package/workframe-api/test_mention_helpers.py +35 -0
  83. package/workframe-api/test_mention_invoke.py +26 -0
  84. package/workframe-api/test_model_surface_consistency.py +117 -0
  85. package/workframe-api/test_oauth_pending.py +41 -0
  86. package/workframe-api/test_profile_model_yaml.py +99 -0
  87. package/workframe-api/test_provider_bindings.py +52 -0
  88. package/workframe-api/test_provider_catalog.py +28 -0
  89. package/workframe-api/test_public_routes.py +41 -0
  90. package/workframe-api/test_route_registry.py +245 -0
  91. package/workframe-api/test_run_authority.py +112 -0
  92. package/workframe-api/test_run_ledger.py +157 -0
  93. package/workframe-api/test_run_surface_wiring.py +130 -0
  94. package/workframe-api/test_runtime_cohort.py +85 -0
  95. package/workframe-api/test_secure_mode_docker_boundary.py +39 -0
  96. package/workframe-api/test_server_reexports.py +99 -0
  97. package/workframe-api/test_stack_config_install_smtp.py +7 -0
  98. package/workframe-api/time-bind-steps.py +75 -0
  99. package/workframe-api/time-perf.py +81 -0
  100. package/workframe-api/time-warm.py +107 -0
  101. package/workframe-api/turn_credentials.py +28 -13
  102. package/workframe-api/turn_overlay.py +715 -0
  103. package/workframe-api/updates.py +16 -1
  104. package/workframe-api/user_prefs.py +387 -0
  105. package/workframe-api/wf032_extract_remaining_handlers.py +417 -0
  106. package/workframe-api/workspace_bootstrap.py +536 -0
  107. package/workframe-api/workspace_files.py +344 -0
  108. package/workframe-api/workspace_messaging.py +206 -0
  109. package/workframe-api/zk_auth.py +3 -2
  110. package/workframe-supervisor/server.py +10 -1
  111. package/workframe-supervisor/test_supervisor_negative.py +75 -0
  112. package/workframe-ui/public/assets/{arc-DjpS9a2b.js → arc-aUUffclm.js} +1 -1
  113. package/workframe-ui/public/assets/architecture-7EHR7CIX-BSUP4S8J.js +1 -0
  114. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-puCKmKHV.js → architectureDiagram-3BPJPVTR-DU6oaqIb.js} +1 -1
  115. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-C2P0g-eH.js → blockDiagram-GPEHLZMM-D2p8BU6-.js} +1 -1
  116. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-DdvYEITH.js → c4Diagram-AAUBKEIU-ouwyPmTJ.js} +1 -1
  117. package/workframe-ui/public/assets/channel-DOunZZ0X.js +1 -0
  118. package/workframe-ui/public/assets/{chunk-2J33WTMH-Cxe8QsEF.js → chunk-2J33WTMH-DsE7U5dj.js} +1 -1
  119. package/workframe-ui/public/assets/{chunk-3OPIFGDE-DPNyTwR3.js → chunk-3OPIFGDE-DM0kkZ9h.js} +1 -1
  120. package/workframe-ui/public/assets/{chunk-4BX2VUAB-XKG9gfRN.js → chunk-4BX2VUAB-CQ8mAyXp.js} +1 -1
  121. package/workframe-ui/public/assets/{chunk-55IACEB6-oPMTulgb.js → chunk-55IACEB6-DN1BDtbH.js} +1 -1
  122. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BWwXvpa_.js → chunk-5ZQYHXKU-BzK2KqOt.js} +1 -1
  123. package/workframe-ui/public/assets/{chunk-727SXJPM-Cml2twO_.js → chunk-727SXJPM-C3AxtOi9.js} +1 -1
  124. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-DzrRBhQu.js → chunk-AQP2D5EJ-4mVwEFuD.js} +1 -1
  125. package/workframe-ui/public/assets/{chunk-BSJP7CBP-EZ7STU6h.js → chunk-BSJP7CBP-23kN2q0A.js} +1 -1
  126. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-BGTQzWYw.js → chunk-CSCIHK7Q-CPBKVW-L.js} +2 -2
  127. package/workframe-ui/public/assets/{chunk-FMBD7UC4-Cm4stfRa.js → chunk-FMBD7UC4-CKqcYq7K.js} +1 -1
  128. package/workframe-ui/public/assets/{chunk-KSCS5N6A-exbMijjy.js → chunk-KSCS5N6A-JRQnhxQE.js} +1 -1
  129. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-C75kbQwN.js → chunk-L5ZTLDWV-BLZrdIwa.js} +1 -1
  130. package/workframe-ui/public/assets/chunk-LZXEDZCA-DisG0XJT.js +2 -0
  131. package/workframe-ui/public/assets/{chunk-ND2GUHAM-D84zyIu2.js → chunk-ND2GUHAM-ClAOxArS.js} +1 -1
  132. package/workframe-ui/public/assets/{chunk-NZK2D7GU-CUNNo1bj.js → chunk-NZK2D7GU-XUE1aNkm.js} +1 -1
  133. package/workframe-ui/public/assets/{chunk-O5CBEL6O-DN6WEIh-.js → chunk-O5CBEL6O-DDN-Ifon.js} +1 -1
  134. package/workframe-ui/public/assets/chunk-QZHKN3VN-EVQ0VMd5.js +1 -0
  135. package/workframe-ui/public/assets/chunk-WU5MYG2G-k_-ZsLOS.js +1 -0
  136. package/workframe-ui/public/assets/{chunk-XPW4576I-Ct5VQv2D.js → chunk-XPW4576I-B4_iYZ6r.js} +1 -1
  137. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-W9WVQsby.js +1 -0
  138. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-W9WVQsby.js +1 -0
  139. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-Bj4W5oMk.js → cose-bilkent-S5V4N54A-Cy1HhS7j.js} +1 -1
  140. package/workframe-ui/public/assets/{dagre-BM42HDAG-DAjHBkAy.js → dagre-BM42HDAG-Bjal81Ie.js} +1 -1
  141. package/workframe-ui/public/assets/{diagram-2AECGRRQ-Ch2gGFPG.js → diagram-2AECGRRQ-v5Gdvzp_.js} +1 -1
  142. package/workframe-ui/public/assets/{diagram-5GNKFQAL-BzIIvgYH.js → diagram-5GNKFQAL-DuyRdfmY.js} +1 -1
  143. package/workframe-ui/public/assets/{diagram-KO2AKTUF-Df0Q8nv6.js → diagram-KO2AKTUF-WwWCGbIW.js} +1 -1
  144. package/workframe-ui/public/assets/{diagram-LMA3HP47-D-G3ekmF.js → diagram-LMA3HP47-DTYwZALT.js} +1 -1
  145. package/workframe-ui/public/assets/{diagram-OG6HWLK6-TBHhT9f5.js → diagram-OG6HWLK6-B46kBIqE.js} +1 -1
  146. package/workframe-ui/public/assets/{dist-CPbiDkEZ.js → dist-mQWmB3z6.js} +1 -1
  147. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-YxkUmLLV.js → erDiagram-TEJ5UH35-__40xO-y.js} +1 -1
  148. package/workframe-ui/public/assets/eventmodeling-FCH6USID-Cm1uRTxU.js +1 -0
  149. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-kfZ7-RSd.js → flowDiagram-I6XJVG4X-Cwj6dVUz.js} +1 -1
  150. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-DmTx3hzR.js → ganttDiagram-6RSMTGT7-D_IOSwAS.js} +1 -1
  151. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BXa0ewp3.js → gitGraph-WXDBUCRP-C1vGecee.js} +1 -1
  152. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-CQr2qMxU.js → gitGraphDiagram-PVQCEYII-DMTxFJxn.js} +1 -1
  153. package/workframe-ui/public/assets/index-BdiM4-lI.js +130 -0
  154. package/workframe-ui/public/assets/index-Fnad47vV.css +1 -0
  155. package/workframe-ui/public/assets/{info-J43DQDTF-t-Qlx8zv.js → info-J43DQDTF-CwjmyPmD.js} +1 -1
  156. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-CzkYofPx.js → infoDiagram-5YYISTIA-BGzh7ZL2.js} +1 -1
  157. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-djK3Oxgb.js → ishikawaDiagram-YF4QCWOH-BLXdnR_5.js} +1 -1
  158. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CWxYXxdF.js → journeyDiagram-JHISSGLW-BK3behMe.js} +1 -1
  159. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-D7leCAD9.js → kanban-definition-UN3LZRKU-Bx172cCl.js} +1 -1
  160. package/workframe-ui/public/assets/{line-BSFj2OC5.js → line-BTCEHb7l.js} +1 -1
  161. package/workframe-ui/public/assets/{linear-pfPwnu-o.js → linear-CkbydpnK.js} +1 -1
  162. package/workframe-ui/public/assets/{mermaid-parser.core-Dm2L9Ib_.js → mermaid-parser.core-DvrtArpk.js} +2 -2
  163. package/workframe-ui/public/assets/{mermaid.core-Bevx9bif.js → mermaid.core-yj_1x_qj.js} +3 -3
  164. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-UkjDssmP.js → mindmap-definition-RKZ34NQL-BXbMltQ0.js} +1 -1
  165. package/workframe-ui/public/assets/{packet-YPE3B663-JO2ezH3_.js → packet-YPE3B663-9pVCBQ7S.js} +1 -1
  166. package/workframe-ui/public/assets/{pie-LRSECV5Y-DdMFsfR8.js → pie-LRSECV5Y-CdqoWesP.js} +1 -1
  167. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-CEPHFBU1.js → pieDiagram-4H26LBE5-kGs-VfXd.js} +1 -1
  168. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-_RF9_86k.js → quadrantDiagram-W4KKPZXB-D9D8gQw5.js} +1 -1
  169. package/workframe-ui/public/assets/{radar-GUYGQ44K-Cb-YfdaA.js → radar-GUYGQ44K-d4zMZpQS.js} +1 -1
  170. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DBVOxmBn.js → requirementDiagram-4Y6WPE33-BI7EQ9zc.js} +1 -1
  171. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-BTpxsGUA.js → sankeyDiagram-5OEKKPKP-Bcz19jQB.js} +1 -1
  172. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-DHP_Bq3l.js → sequenceDiagram-3UESZ5HK-iQiCEfYp.js} +1 -1
  173. package/workframe-ui/public/assets/{src-CY_By-r7.js → src-D6mvlP96.js} +1 -1
  174. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BVtp_LPR.js → stateDiagram-AJRCARHV-CxaTXs02.js} +1 -1
  175. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CXikj9xi.js +1 -0
  176. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-D38HJhop.js → timeline-definition-PNZ67QCA-Bej-EAnf.js} +1 -1
  177. package/workframe-ui/public/assets/{treeView-BLDUP644-CFCG3Sc_.js → treeView-BLDUP644-C7PnnzwX.js} +1 -1
  178. package/workframe-ui/public/assets/{treemap-LRROVOQU-BeiMs_dF.js → treemap-LRROVOQU-CqISLmkO.js} +1 -1
  179. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CKMeT6PW.js → vennDiagram-CIIHVFJN-eJMTTEW-.js} +1 -1
  180. package/workframe-ui/public/assets/{wardley-L42UT6IY-BljDy8ph.js → wardley-L42UT6IY-D4sMroBF.js} +1 -1
  181. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DEE8D94J.js → wardleyDiagram-YWT4CUSO-DC3DCUs7.js} +1 -1
  182. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-BRaAWV_g.js → xychartDiagram-2RQKCTM6-BeMaM6Aq.js} +1 -1
  183. package/workframe-ui/public/index.html +7 -6
  184. package/workframe-ui/public/workframe-build.json +5 -0
  185. package/workframe-ui/public/assets/architecture-7EHR7CIX-CvhbXEvt.js +0 -1
  186. package/workframe-ui/public/assets/channel-D63s_yN9.js +0 -1
  187. package/workframe-ui/public/assets/chunk-LZXEDZCA-CsRdBqUB.js +0 -2
  188. package/workframe-ui/public/assets/chunk-QZHKN3VN-Dj_Yxhgo.js +0 -1
  189. package/workframe-ui/public/assets/chunk-WU5MYG2G-AFchshb6.js +0 -1
  190. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-Opj8t01I.js +0 -1
  191. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-Opj8t01I.js +0 -1
  192. package/workframe-ui/public/assets/eventmodeling-FCH6USID-LQOYL50n.js +0 -1
  193. package/workframe-ui/public/assets/index-B08aShJy.css +0 -1
  194. package/workframe-ui/public/assets/index-DCF3W3G_.js +0 -129
  195. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CvJhS_BJ.js +0 -1
@@ -0,0 +1,470 @@
1
+ """Workframe-owned run ledger — runs, run_events, run_line_items (WF-NS-P2, WF-016)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sqlite3
8
+ import time
9
+ import uuid
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import Any, Mapping
13
+
14
+ from domain.entities import (
15
+ ActorType,
16
+ FundingSource,
17
+ Run,
18
+ RunEvent,
19
+ RunStatus,
20
+ RunSurface,
21
+ )
22
+ from run_authority import RunAuthorityDecision
23
+
24
+ DATA_DIR = Path(os.environ.get("WORKFRAME_API_DATA_DIR", "/app/data"))
25
+ WORKFRAME_DB = DATA_DIR / "workframe.db"
26
+
27
+ _SCHEMA_READY: set[str] = set()
28
+
29
+
30
+ def _connect() -> sqlite3.Connection:
31
+ conn = sqlite3.connect(str(WORKFRAME_DB), timeout=10.0)
32
+ conn.row_factory = sqlite3.Row
33
+ conn.execute("PRAGMA journal_mode=WAL")
34
+ conn.execute("PRAGMA busy_timeout=5000")
35
+ return conn
36
+
37
+
38
+ def _utc_now() -> str:
39
+ return datetime.now(timezone.utc).isoformat()
40
+
41
+
42
+ def _iso_ts() -> str:
43
+ return str(int(time.time()))
44
+
45
+
46
+ def ensure_schema() -> None:
47
+ key = str(WORKFRAME_DB)
48
+ if key in _SCHEMA_READY:
49
+ return
50
+ conn = _connect()
51
+ try:
52
+ conn.executescript(
53
+ """
54
+ CREATE TABLE IF NOT EXISTS schema_migrations (
55
+ version TEXT PRIMARY KEY,
56
+ description TEXT,
57
+ applied_at TEXT
58
+ );
59
+
60
+ CREATE TABLE IF NOT EXISTS runs (
61
+ run_id TEXT PRIMARY KEY,
62
+ workspace_id TEXT NOT NULL,
63
+ surface TEXT NOT NULL,
64
+ actor_type TEXT NOT NULL,
65
+ actor_id TEXT NOT NULL,
66
+ triggering_user_id TEXT NOT NULL,
67
+ agent_id TEXT NOT NULL DEFAULT '',
68
+ runtime_binding_id TEXT NOT NULL DEFAULT '',
69
+ status TEXT NOT NULL,
70
+ payer_user_id TEXT NOT NULL,
71
+ funding_source TEXT NOT NULL,
72
+ room_id TEXT DEFAULT NULL,
73
+ card_id TEXT DEFAULT NULL,
74
+ engine TEXT NOT NULL DEFAULT 'hermes',
75
+ runtime TEXT NOT NULL DEFAULT 'hermes_managed',
76
+ risk_tier TEXT NOT NULL DEFAULT 'low',
77
+ budget_usd REAL DEFAULT NULL,
78
+ deny_reason TEXT DEFAULT NULL,
79
+ profile_slug TEXT DEFAULT NULL,
80
+ provider TEXT DEFAULT NULL,
81
+ credential_ref_id TEXT DEFAULT NULL,
82
+ credential_scope TEXT DEFAULT NULL,
83
+ session_id TEXT DEFAULT NULL,
84
+ created_at TEXT NOT NULL,
85
+ started_at TEXT DEFAULT NULL,
86
+ ended_at TEXT DEFAULT NULL
87
+ );
88
+
89
+ CREATE INDEX IF NOT EXISTS idx_runs_workspace
90
+ ON runs(workspace_id, created_at DESC);
91
+ CREATE INDEX IF NOT EXISTS idx_runs_room
92
+ ON runs(room_id, created_at DESC)
93
+ WHERE room_id IS NOT NULL;
94
+ CREATE INDEX IF NOT EXISTS idx_runs_status
95
+ ON runs(status, created_at DESC);
96
+
97
+ CREATE TABLE IF NOT EXISTS run_events (
98
+ event_id TEXT PRIMARY KEY,
99
+ run_id TEXT NOT NULL,
100
+ event_type TEXT NOT NULL,
101
+ payload_json TEXT NOT NULL DEFAULT '{}',
102
+ room_id TEXT DEFAULT NULL,
103
+ created_at TEXT NOT NULL,
104
+ FOREIGN KEY (run_id) REFERENCES runs (run_id)
105
+ );
106
+
107
+ CREATE INDEX IF NOT EXISTS idx_run_events_run
108
+ ON run_events(run_id, created_at ASC);
109
+ CREATE INDEX IF NOT EXISTS idx_run_events_room
110
+ ON run_events(room_id, created_at DESC)
111
+ WHERE room_id IS NOT NULL;
112
+
113
+ CREATE TABLE IF NOT EXISTS run_line_items (
114
+ line_item_id TEXT PRIMARY KEY,
115
+ run_id TEXT NOT NULL,
116
+ kind TEXT NOT NULL,
117
+ provider TEXT DEFAULT NULL,
118
+ payer_user_id TEXT NOT NULL,
119
+ funding_source TEXT NOT NULL,
120
+ amount_usd REAL DEFAULT NULL,
121
+ units REAL DEFAULT NULL,
122
+ unit_label TEXT DEFAULT NULL,
123
+ model TEXT DEFAULT NULL,
124
+ receipt_json TEXT NOT NULL DEFAULT '{}',
125
+ created_at TEXT NOT NULL,
126
+ FOREIGN KEY (run_id) REFERENCES runs (run_id)
127
+ );
128
+
129
+ CREATE INDEX IF NOT EXISTS idx_run_line_items_run
130
+ ON run_line_items(run_id, created_at ASC);
131
+
132
+ INSERT OR IGNORE INTO schema_migrations (version, description, applied_at)
133
+ VALUES ('14', 'runs, run_events, run_line_items ledger tables', datetime('now'));
134
+ """
135
+ )
136
+ conn.commit()
137
+ finally:
138
+ conn.close()
139
+ _SCHEMA_READY.add(key)
140
+
141
+
142
+ def _row_to_run(row: sqlite3.Row) -> Run:
143
+ return Run(
144
+ run_id=str(row["run_id"]),
145
+ workspace_id=str(row["workspace_id"]),
146
+ surface=RunSurface(str(row["surface"])),
147
+ actor_type=ActorType(str(row["actor_type"])),
148
+ actor_id=str(row["actor_id"]),
149
+ triggering_user_id=str(row["triggering_user_id"]),
150
+ agent_id=str(row["agent_id"] or ""),
151
+ runtime_binding_id=str(row["runtime_binding_id"] or ""),
152
+ status=RunStatus(str(row["status"])),
153
+ payer_user_id=str(row["payer_user_id"]),
154
+ funding_source=FundingSource(str(row["funding_source"])),
155
+ room_id=str(row["room_id"]) if row["room_id"] else None,
156
+ card_id=str(row["card_id"]) if row["card_id"] else None,
157
+ engine=str(row["engine"] or "hermes"),
158
+ runtime=str(row["runtime"] or "hermes_managed"),
159
+ risk_tier=str(row["risk_tier"] or "low"), # type: ignore[arg-type]
160
+ budget_usd=row["budget_usd"],
161
+ deny_reason=str(row["deny_reason"]) if row["deny_reason"] else None,
162
+ created_at=None,
163
+ started_at=None,
164
+ ended_at=None,
165
+ )
166
+
167
+
168
+ def insert_run(
169
+ conn: sqlite3.Connection,
170
+ *,
171
+ run_id: str,
172
+ workspace_id: str,
173
+ surface: RunSurface,
174
+ actor_type: ActorType,
175
+ actor_id: str,
176
+ triggering_user_id: str,
177
+ agent_id: str,
178
+ runtime_binding_id: str,
179
+ status: RunStatus,
180
+ payer_user_id: str,
181
+ funding_source: FundingSource,
182
+ room_id: str | None = None,
183
+ profile_slug: str | None = None,
184
+ provider: str | None = None,
185
+ deny_reason: str | None = None,
186
+ credential_ref_id: str | None = None,
187
+ credential_scope: str | None = None,
188
+ session_id: str | None = None,
189
+ ) -> None:
190
+ now = _utc_now()
191
+ conn.execute(
192
+ """
193
+ INSERT INTO runs (
194
+ run_id, workspace_id, surface, actor_type, actor_id, triggering_user_id,
195
+ agent_id, runtime_binding_id, status, payer_user_id, funding_source,
196
+ room_id, profile_slug, provider, deny_reason, credential_ref_id,
197
+ credential_scope, session_id, created_at, started_at
198
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
199
+ """,
200
+ (
201
+ run_id,
202
+ workspace_id,
203
+ surface.value,
204
+ actor_type.value,
205
+ actor_id,
206
+ triggering_user_id,
207
+ agent_id,
208
+ runtime_binding_id,
209
+ status.value,
210
+ payer_user_id,
211
+ funding_source.value,
212
+ room_id,
213
+ profile_slug,
214
+ provider,
215
+ deny_reason,
216
+ credential_ref_id,
217
+ credential_scope,
218
+ session_id,
219
+ now,
220
+ now if status in (RunStatus.RUNNING, RunStatus.AUTHORIZED) else None,
221
+ ),
222
+ )
223
+
224
+
225
+ def insert_run_event(
226
+ conn: sqlite3.Connection,
227
+ *,
228
+ run_id: str,
229
+ event_type: str,
230
+ payload: Mapping[str, Any] | None = None,
231
+ room_id: str | None = None,
232
+ event_id: str | None = None,
233
+ ) -> RunEvent:
234
+ eid = str(event_id or uuid.uuid4())
235
+ conn.execute(
236
+ """
237
+ INSERT INTO run_events (event_id, run_id, event_type, payload_json, room_id, created_at)
238
+ VALUES (?, ?, ?, ?, ?, ?)
239
+ """,
240
+ (eid, run_id, event_type, json.dumps(dict(payload or {})), room_id, _utc_now()),
241
+ )
242
+ return RunEvent(event_id=eid, run_id=run_id, event_type=event_type, payload=dict(payload or {}))
243
+
244
+
245
+ def update_run_status(
246
+ conn: sqlite3.Connection,
247
+ run_id: str,
248
+ status: RunStatus,
249
+ *,
250
+ deny_reason: str | None = None,
251
+ ) -> None:
252
+ ended = status in (RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED, RunStatus.DENIED)
253
+ conn.execute(
254
+ """
255
+ UPDATE runs
256
+ SET status = ?, deny_reason = COALESCE(?, deny_reason), ended_at = ?
257
+ WHERE run_id = ?
258
+ """,
259
+ (status.value, deny_reason, _utc_now() if ended else None, run_id),
260
+ )
261
+
262
+
263
+ def insert_line_item(
264
+ conn: sqlite3.Connection,
265
+ *,
266
+ run_id: str,
267
+ kind: str,
268
+ payer_user_id: str,
269
+ funding_source: FundingSource,
270
+ provider: str | None = None,
271
+ amount_usd: float | None = None,
272
+ units: float | None = None,
273
+ unit_label: str | None = None,
274
+ model: str | None = None,
275
+ receipt: Mapping[str, Any] | None = None,
276
+ line_item_id: str | None = None,
277
+ ) -> str:
278
+ lid = str(line_item_id or uuid.uuid4())
279
+ conn.execute(
280
+ """
281
+ INSERT INTO run_line_items (
282
+ line_item_id, run_id, kind, provider, payer_user_id, funding_source,
283
+ amount_usd, units, unit_label, model, receipt_json, created_at
284
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
285
+ """,
286
+ (
287
+ lid,
288
+ run_id,
289
+ kind,
290
+ provider,
291
+ payer_user_id,
292
+ funding_source.value,
293
+ amount_usd,
294
+ units,
295
+ unit_label,
296
+ model,
297
+ json.dumps(dict(receipt or {})),
298
+ _utc_now(),
299
+ ),
300
+ )
301
+ return lid
302
+
303
+
304
+ def record_authority_decision(
305
+ conn: sqlite3.Connection,
306
+ *,
307
+ run_id: str,
308
+ request_surface: RunSurface,
309
+ actor_type: ActorType,
310
+ actor_id: str,
311
+ triggering_user_id: str,
312
+ workspace_id: str,
313
+ agent_id: str,
314
+ runtime_binding_id: str,
315
+ profile_slug: str,
316
+ provider: str,
317
+ room_id: str | None,
318
+ session_id: str | None,
319
+ decision: RunAuthorityDecision,
320
+ ) -> None:
321
+ status = RunStatus.AUTHORIZED if decision.allowed else RunStatus.DENIED
322
+ if decision.allowed:
323
+ status = RunStatus.RUNNING
324
+ insert_run(
325
+ conn,
326
+ run_id=run_id,
327
+ workspace_id=workspace_id,
328
+ surface=request_surface,
329
+ actor_type=actor_type,
330
+ actor_id=actor_id,
331
+ triggering_user_id=triggering_user_id,
332
+ agent_id=agent_id,
333
+ runtime_binding_id=runtime_binding_id,
334
+ status=status,
335
+ payer_user_id=decision.payer_user_id or triggering_user_id,
336
+ funding_source=decision.funding_source,
337
+ room_id=room_id,
338
+ profile_slug=profile_slug,
339
+ provider=provider,
340
+ deny_reason=decision.deny_reason,
341
+ credential_ref_id=decision.credential_ref_id,
342
+ credential_scope=decision.credential_scope,
343
+ session_id=session_id,
344
+ )
345
+ event_type = "run.authorized" if decision.allowed else "run.denied"
346
+ insert_run_event(
347
+ conn,
348
+ run_id=run_id,
349
+ event_type=event_type,
350
+ payload={
351
+ "payer_user_id": decision.payer_user_id,
352
+ "funding_source": decision.funding_source.value,
353
+ "deny_reason": decision.deny_reason,
354
+ "profile_slug": profile_slug,
355
+ "provider": provider,
356
+ },
357
+ room_id=room_id,
358
+ )
359
+
360
+
361
+ def complete_run(
362
+ conn: sqlite3.Connection,
363
+ run_id: str,
364
+ *,
365
+ model: str = "",
366
+ provider: str = "",
367
+ funding_source: FundingSource,
368
+ payer_user_id: str,
369
+ receipt: Mapping[str, Any] | None = None,
370
+ ) -> None:
371
+ update_run_status(conn, run_id, RunStatus.COMPLETED)
372
+ insert_run_event(conn, run_id=run_id, event_type="run.completed", payload=dict(receipt or {}))
373
+ insert_line_item(
374
+ conn,
375
+ run_id=run_id,
376
+ kind="llm_turn",
377
+ payer_user_id=payer_user_id,
378
+ funding_source=funding_source,
379
+ provider=provider or None,
380
+ model=model or None,
381
+ unit_label="turn",
382
+ units=1.0,
383
+ receipt={
384
+ "run_id": run_id,
385
+ "model": model,
386
+ "provider": provider,
387
+ "payer_user_id": payer_user_id,
388
+ "funding_source": funding_source.value,
389
+ **dict(receipt or {}),
390
+ },
391
+ )
392
+
393
+
394
+ def fail_run(conn: sqlite3.Connection, run_id: str, *, reason: str = "") -> None:
395
+ update_run_status(conn, run_id, RunStatus.FAILED, deny_reason=reason or None)
396
+ insert_run_event(conn, run_id=run_id, event_type="run.failed", payload={"reason": reason})
397
+
398
+
399
+ def list_run_events_for_room(room_id: str, *, limit: int = 40) -> list[dict[str, Any]]:
400
+ room_id = str(room_id or "").strip()
401
+ if not room_id:
402
+ return []
403
+ ensure_schema()
404
+ conn = _connect()
405
+ try:
406
+ rows = conn.execute(
407
+ """
408
+ SELECT e.event_id, e.run_id, e.event_type, e.payload_json, e.created_at,
409
+ r.profile_slug, r.payer_user_id, r.funding_source, r.status AS run_status
410
+ FROM run_events e
411
+ JOIN runs r ON r.run_id = e.run_id
412
+ WHERE COALESCE(e.room_id, r.room_id) = ?
413
+ ORDER BY e.created_at DESC
414
+ LIMIT ?
415
+ """,
416
+ (room_id, max(1, limit)),
417
+ ).fetchall()
418
+ finally:
419
+ conn.close()
420
+ out: list[dict[str, Any]] = []
421
+ for row in rows:
422
+ try:
423
+ payload = json.loads(str(row["payload_json"] or "{}"))
424
+ except json.JSONDecodeError:
425
+ payload = {}
426
+ out.append(
427
+ {
428
+ "id": f"run_event:{row['event_id']}",
429
+ "kind": "run_event",
430
+ "event_type": str(row["event_type"]),
431
+ "run_id": str(row["run_id"]),
432
+ "run_status": str(row["run_status"]),
433
+ "profile": str(row["profile_slug"] or ""),
434
+ "payer_user_id": str(row["payer_user_id"] or ""),
435
+ "funding_source": str(row["funding_source"] or ""),
436
+ "task_description": _event_description(str(row["event_type"]), payload),
437
+ "status": str(row["run_status"]),
438
+ "created_at": str(row["created_at"]),
439
+ "source": "run_ledger",
440
+ "payload": payload,
441
+ }
442
+ )
443
+ return out
444
+
445
+
446
+ def _event_description(event_type: str, payload: Mapping[str, Any]) -> str:
447
+ if event_type == "run.authorized":
448
+ fs = str(payload.get("funding_source") or "")
449
+ return f"Run authorized ({fs})" if fs else "Run authorized"
450
+ if event_type == "run.denied":
451
+ return f"Run denied: {payload.get('deny_reason') or 'policy'}"
452
+ if event_type == "run.completed":
453
+ return "Chat turn completed"
454
+ if event_type == "run.failed":
455
+ return f"Run failed: {payload.get('reason') or 'error'}"
456
+ if event_type == "slash.command":
457
+ cmd = str(payload.get("command") or "")
458
+ return f"Slash /{cmd}" if cmd else "Slash command"
459
+ if event_type == "cron.triggered":
460
+ return f"Cron: {payload.get('job') or payload.get('actor_id') or 'job'}"
461
+ if event_type == "kanban.task_created":
462
+ return f"Kanban: {payload.get('title') or 'task'}"
463
+ return event_type
464
+
465
+
466
+ def get_run(conn: sqlite3.Connection, run_id: str) -> Run | None:
467
+ row = conn.execute("SELECT * FROM runs WHERE run_id = ?", (run_id,)).fetchone()
468
+ if not row:
469
+ return None
470
+ return _row_to_run(row)