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,157 @@
1
+ """WF-NS-P2 run ledger self-check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sqlite3
8
+ import sys
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
13
+
14
+ import run_ledger # noqa: E402
15
+ from domain.entities import ( # noqa: E402
16
+ ActorType,
17
+ FundingSource,
18
+ RunStatus,
19
+ RunSurface,
20
+ )
21
+ from run_authority import RunAuthorityDecision, chat_run_request # noqa: E402
22
+
23
+
24
+ def test_schema_and_round_trip() -> None:
25
+ with tempfile.TemporaryDirectory() as tmp:
26
+ db_path = Path(tmp) / "workframe.db"
27
+ os.environ["WORKFRAME_API_DATA_DIR"] = tmp
28
+ run_ledger.WORKFRAME_DB = db_path
29
+ run_ledger._SCHEMA_READY.clear()
30
+ run_ledger.ensure_schema()
31
+
32
+ conn = sqlite3.connect(str(db_path))
33
+ conn.row_factory = sqlite3.Row
34
+ try:
35
+ decision = RunAuthorityDecision(
36
+ allowed=True,
37
+ deny_reason=None,
38
+ payer_user_id="user-1",
39
+ funding_source=FundingSource.BYOK,
40
+ credential_ref_id=None,
41
+ credential_scope="user",
42
+ grants=(),
43
+ )
44
+ run_ledger.record_authority_decision(
45
+ conn,
46
+ run_id="run-test-1",
47
+ request_surface=RunSurface.CHAT,
48
+ actor_type=ActorType.USER,
49
+ actor_id="user-1",
50
+ triggering_user_id="user-1",
51
+ workspace_id="ws-1",
52
+ agent_id="agent-1",
53
+ runtime_binding_id="bind-1",
54
+ profile_slug="u-user-1-native",
55
+ provider="openrouter",
56
+ room_id="room-1",
57
+ session_id="sess-1",
58
+ decision=decision,
59
+ )
60
+ conn.commit()
61
+
62
+ run = run_ledger.get_run(conn, "run-test-1")
63
+ assert run is not None
64
+ assert run.status == RunStatus.RUNNING
65
+ assert run.funding_source == FundingSource.BYOK
66
+
67
+ run_ledger.complete_run(
68
+ conn,
69
+ "run-test-1",
70
+ model="test/model",
71
+ provider="openrouter",
72
+ funding_source=FundingSource.BYOK,
73
+ payer_user_id="user-1",
74
+ )
75
+ conn.commit()
76
+
77
+ items = conn.execute(
78
+ "SELECT kind, funding_source, amount_usd, receipt_json FROM run_line_items WHERE run_id = ?",
79
+ ("run-test-1",),
80
+ ).fetchall()
81
+ assert len(items) == 1
82
+ assert str(items[0]["kind"]) == "llm_turn"
83
+ assert items[0]["amount_usd"] is None
84
+ receipt = json.loads(str(items[0]["receipt_json"]))
85
+ assert receipt["run_id"] == "run-test-1"
86
+ assert receipt["payer_user_id"] == "user-1"
87
+ finally:
88
+ conn.close()
89
+
90
+ events = run_ledger.list_run_events_for_room("room-1")
91
+ assert len(events) >= 2
92
+ assert events[0]["source"] == "run_ledger"
93
+
94
+
95
+ def test_dm_room_chat_run_recorded() -> None:
96
+ """WF-NS-P2: DM rooms (room_type=direct) share chat_stream run path."""
97
+ dm_room_id = "550e8400-e29b-41d4-a716-446655440099"
98
+ req = chat_run_request(
99
+ triggering_user_id="user-a",
100
+ profile_slug="u-user-a-native",
101
+ workspace_id="ws-1",
102
+ provider="openrouter",
103
+ room_id=dm_room_id,
104
+ )
105
+ assert req.surface == RunSurface.CHAT
106
+
107
+ with tempfile.TemporaryDirectory() as tmp:
108
+ db_path = Path(tmp) / "workframe.db"
109
+ os.environ["WORKFRAME_API_DATA_DIR"] = tmp
110
+ run_ledger.WORKFRAME_DB = db_path
111
+ run_ledger._SCHEMA_READY.clear()
112
+ run_ledger.ensure_schema()
113
+
114
+ conn = sqlite3.connect(str(db_path))
115
+ conn.row_factory = sqlite3.Row
116
+ try:
117
+ decision = RunAuthorityDecision(
118
+ allowed=True,
119
+ deny_reason=None,
120
+ payer_user_id="user-a",
121
+ funding_source=FundingSource.BYOK,
122
+ credential_ref_id=None,
123
+ credential_scope="user",
124
+ grants=(),
125
+ )
126
+ run_ledger.record_authority_decision(
127
+ conn,
128
+ run_id="run-dm-1",
129
+ request_surface=req.surface,
130
+ actor_type=req.actor_type,
131
+ actor_id=req.actor_id,
132
+ triggering_user_id="user-a",
133
+ workspace_id="ws-1",
134
+ agent_id=req.agent_id,
135
+ runtime_binding_id=req.runtime_binding_id,
136
+ profile_slug=req.profile_slug,
137
+ provider="openrouter",
138
+ room_id=dm_room_id,
139
+ session_id="sess-dm-1",
140
+ decision=decision,
141
+ )
142
+ conn.commit()
143
+ row = conn.execute(
144
+ "SELECT surface, room_id FROM runs WHERE run_id = ?",
145
+ ("run-dm-1",),
146
+ ).fetchone()
147
+ assert row is not None
148
+ assert row["surface"] == RunSurface.CHAT.value
149
+ assert row["room_id"] == dm_room_id
150
+ finally:
151
+ conn.close()
152
+
153
+
154
+ if __name__ == "__main__":
155
+ test_schema_and_round_trip()
156
+ test_dm_room_chat_run_recorded()
157
+ print("test_run_ledger: ok")
@@ -0,0 +1,130 @@
1
+ """WF-NS-P2 run surface wiring self-check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sqlite3
7
+ import sys
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
12
+
13
+ import run_ledger # noqa: E402
14
+ import run_surface_wiring # noqa: E402
15
+ import server # noqa: E402
16
+ from domain.entities import ActorType, RunSurface # noqa: E402
17
+
18
+
19
+ def _use_tmp_db(tmp: str) -> Path:
20
+ db_path = Path(tmp) / "workframe.db"
21
+ data = Path(tmp)
22
+ os.environ["WORKFRAME_API_DATA_DIR"] = tmp
23
+ server.DATA_DIR = data
24
+ server.AUTH_DB_PATH = data / "auth.db"
25
+ run_ledger.DATA_DIR = data
26
+ run_ledger.WORKFRAME_DB = db_path
27
+ run_ledger._SCHEMA_READY.clear()
28
+ return db_path
29
+
30
+
31
+ def _line_items_for_run(db_path: Path, run_id: str) -> list[sqlite3.Row]:
32
+ conn = sqlite3.connect(str(db_path))
33
+ conn.row_factory = sqlite3.Row
34
+ try:
35
+ return conn.execute(
36
+ "SELECT kind, amount_usd, receipt_json FROM run_line_items WHERE run_id = ?",
37
+ (run_id,),
38
+ ).fetchall()
39
+ finally:
40
+ conn.close()
41
+
42
+
43
+ def test_slash_audit_run_no_gate() -> None:
44
+ with tempfile.TemporaryDirectory() as tmp:
45
+ _use_tmp_db(tmp)
46
+ assert run_surface_wiring.slash_requires_authority("help") is False
47
+ assert run_surface_wiring.slash_requires_authority("chat") is True
48
+
49
+
50
+ def test_record_automated_cron() -> None:
51
+ with tempfile.TemporaryDirectory() as tmp:
52
+ db_path = _use_tmp_db(tmp)
53
+ run_ledger.ensure_schema()
54
+ result = run_surface_wiring.record_automated_surface_run(
55
+ {
56
+ "surface": "cron",
57
+ "profile_slug": "u-test-dev",
58
+ "triggering_user_id": "user-1",
59
+ "workspace_id": "ws-1",
60
+ "actor_id": "cron:daily",
61
+ "provider": "openrouter",
62
+ "event_type": "cron.triggered",
63
+ "payload": {"job": "daily"},
64
+ }
65
+ )
66
+ assert result["ok"] is True
67
+ rid = str(result["run_id"])
68
+ conn = sqlite3.connect(str(db_path))
69
+ conn.row_factory = sqlite3.Row
70
+ row = conn.execute("SELECT surface, status FROM runs WHERE run_id = ?", (rid,)).fetchone()
71
+ conn.close()
72
+ assert row is not None
73
+ assert row["surface"] == RunSurface.CRON.value
74
+ assert row["status"] == "completed"
75
+ items = _line_items_for_run(db_path, rid)
76
+ assert len(items) == 1
77
+ assert items[0]["amount_usd"] is None
78
+
79
+
80
+ def test_finish_surface_run_emits_receipt() -> None:
81
+ with tempfile.TemporaryDirectory() as tmp:
82
+ db_path = _use_tmp_db(tmp)
83
+ run_ledger.ensure_schema()
84
+ rid = run_surface_wiring.record_audit_surface_run(
85
+ surface=RunSurface.SLASH,
86
+ actor_type=ActorType.USER,
87
+ actor_id="user-1",
88
+ triggering_user_id="user-1",
89
+ profile_slug="u-test-dev",
90
+ workspace_id="ws-1",
91
+ event_type="slash.command",
92
+ payload={"command": "help"},
93
+ provider="openrouter",
94
+ )
95
+ run_surface_wiring.finish_surface_run(rid, ok=True, detail="ok")
96
+ items = _line_items_for_run(db_path, rid)
97
+ assert len(items) == 1
98
+ assert items[0]["amount_usd"] is None
99
+
100
+
101
+ def test_record_automated_webhook() -> None:
102
+ with tempfile.TemporaryDirectory() as tmp:
103
+ _use_tmp_db(tmp)
104
+ run_ledger.ensure_schema()
105
+ result = run_surface_wiring.record_automated_surface_run(
106
+ {
107
+ "surface": "webhook",
108
+ "profile_slug": "u-test-dev",
109
+ "triggering_user_id": "user-1",
110
+ "workspace_id": "ws-1",
111
+ "actor_id": "webhook:github",
112
+ "event_type": "webhook.received",
113
+ "provider": "openrouter",
114
+ "payload": {"source": "github"},
115
+ }
116
+ )
117
+ assert result["ok"] is True
118
+ assert result["surface"] == RunSurface.WEBHOOK.value
119
+ rid = str(result["run_id"])
120
+ items = _line_items_for_run(Path(tmp) / "workframe.db", rid)
121
+ assert len(items) == 1
122
+ assert items[0]["amount_usd"] is None
123
+
124
+
125
+ if __name__ == "__main__":
126
+ test_slash_audit_run_no_gate()
127
+ test_record_automated_cron()
128
+ test_record_automated_webhook()
129
+ test_finish_surface_run_emits_receipt()
130
+ print("test_run_surface_wiring ok")
@@ -0,0 +1,85 @@
1
+ """WF-032 runtime_cohort self-check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ import tempfile
8
+ import uuid
9
+ from pathlib import Path
10
+
11
+ API_DIR = Path(__file__).resolve().parent
12
+ if str(API_DIR) not in sys.path:
13
+ sys.path.insert(0, str(API_DIR))
14
+
15
+ os.environ.setdefault("WORKFRAME_API_DATA_DIR", str(API_DIR / ".tmp-test-data"))
16
+ os.environ.setdefault("HERMES_DATA", str(API_DIR / ".tmp-test-hermes"))
17
+ os.environ.setdefault("DEV_LOCAL_UNSAFE", "true")
18
+
19
+ import runtime_cohort # noqa: E402
20
+ import server # noqa: E402
21
+
22
+
23
+ def test_runtime_profile_slug_format() -> None:
24
+ slug = runtime_cohort._runtime_profile_slug("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "architect")
25
+ assert slug.startswith("u-")
26
+ assert slug.endswith("-architect")
27
+ assert server._runtime_profile_slug("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "architect") == slug
28
+
29
+
30
+ def test_self_grant_forbidden() -> None:
31
+ try:
32
+ runtime_cohort.create_delegation_grant("ws-1", "user-1", "user-1")
33
+ raise AssertionError("expected ValueError")
34
+ except ValueError as exc:
35
+ assert str(exc) == "self_grant_forbidden"
36
+
37
+
38
+ def test_delegation_grant_round_trip() -> None:
39
+ with tempfile.TemporaryDirectory() as tmp:
40
+ os.environ["WORKFRAME_API_DATA_DIR"] = tmp
41
+ server.DATA_DIR = Path(tmp)
42
+ server.AUTH_DB_PATH = Path(tmp) / "auth.db"
43
+ server._ensure_workframe_db_schema()
44
+ ws_id = str(uuid.uuid4())
45
+ grantor = str(uuid.uuid4())
46
+ grantee = str(uuid.uuid4())
47
+ now = server._utc_now()
48
+ conn = server._workframe_db()
49
+ try:
50
+ conn.execute(
51
+ "INSERT INTO workspaces (id, slug, display_name, owner_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
52
+ (ws_id, "test-ws", "Test", grantor, now, now),
53
+ )
54
+ for uid, email in ((grantor, "a@test.local"), (grantee, "b@test.local")):
55
+ conn.execute(
56
+ "INSERT INTO users (id, email, display_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
57
+ (uid, email, email.split("@")[0], now, now),
58
+ )
59
+ conn.execute(
60
+ """
61
+ INSERT INTO workspace_memberships (
62
+ id, workspace_id, user_id, role, status, created_at, updated_at
63
+ ) VALUES (?, ?, ?, 'member', 'active', ?, ?)
64
+ """,
65
+ (str(uuid.uuid4()), ws_id, uid, now, now),
66
+ )
67
+ conn.commit()
68
+ finally:
69
+ conn.close()
70
+ created = runtime_cohort.create_delegation_grant(ws_id, grantor, grantee)
71
+ assert created["ok"] is True
72
+ grant_id = created["grant"]["id"]
73
+ listed = runtime_cohort.list_delegation_grants(ws_id, grantor)
74
+ assert any(row["id"] == grant_id for row in listed["grants"])
75
+ revoked = runtime_cohort.revoke_delegation_grant(ws_id, grant_id, grantor)
76
+ assert revoked["ok"] is True
77
+ grantors = runtime_cohort._delegation_grantor_ids_for_grantee(grantee, ws_id)
78
+ assert grantor not in grantors
79
+
80
+
81
+ if __name__ == "__main__":
82
+ test_runtime_profile_slug_format()
83
+ test_self_grant_forbidden()
84
+ test_delegation_grant_round_trip()
85
+ print("test_runtime_cohort: ok")
@@ -0,0 +1,39 @@
1
+ """WF-NS-P1 secure-mode docker socket boundary self-check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
9
+
10
+ import server # noqa: E402
11
+
12
+
13
+ def test_docker_request_blocked_in_secure_mode() -> None:
14
+ prev = server.SECURE_MODE
15
+ try:
16
+ server.SECURE_MODE = True
17
+ try:
18
+ server._docker_request("GET", "/containers/json")
19
+ raise AssertionError("expected RuntimeError in SECURE_MODE")
20
+ except RuntimeError as exc:
21
+ assert "SECURE_MODE" in str(exc)
22
+ finally:
23
+ server.SECURE_MODE = prev
24
+
25
+
26
+ def test_public_compose_api_has_no_docker_sock() -> None:
27
+ """Compose config assertion — complements WF-011 supervisor negatives."""
28
+ repo = Path(__file__).resolve().parents[2]
29
+ public = (repo / "infra" / "compose" / "workframe" / "docker-compose.public.yml").read_text(
30
+ encoding="utf-8"
31
+ )
32
+ api_block = public.split("workframe-api:")[1].split("workframe-supervisor:")[0]
33
+ assert "/var/run/docker.sock" not in api_block
34
+
35
+
36
+ if __name__ == "__main__":
37
+ test_docker_request_blocked_in_secure_mode()
38
+ test_public_compose_api_has_no_docker_sock()
39
+ print("test_secure_mode_docker_boundary: ok")
@@ -0,0 +1,99 @@
1
+ """WF-032 guard: server.py must re-export symbols still called from extracted modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ API_DIR = Path(__file__).resolve().parent
10
+ if str(API_DIR) not in sys.path:
11
+ sys.path.insert(0, str(API_DIR))
12
+
13
+ os.environ.setdefault("WORKFRAME_API_DATA_DIR", str(API_DIR / ".tmp-test-data"))
14
+ os.environ.setdefault("HERMES_DATA", str(API_DIR / ".tmp-test-hermes"))
15
+ os.environ.setdefault("DEV_LOCAL_UNSAFE", "true")
16
+
17
+ import server # noqa: E402
18
+
19
+
20
+ _REQUIRED = (
21
+ "_parse_workspace_settings",
22
+ "_strip_profile_action_env",
23
+ "_strip_profile_llm_env",
24
+ "_user_action_env_specs",
25
+ "_overlay_turn_user_env",
26
+ "_overlay_turn_provider_env",
27
+ "_llm_proxy_base_url",
28
+ "_require_user_provider",
29
+ "_ensure_profile_llm_proxy",
30
+ "_revoke_turn_credential_lease",
31
+ "_sync_profile_provider_env",
32
+ "_session_info",
33
+ "chat_messages",
34
+ "chat_bootstrap",
35
+ "_gateway_container_exec",
36
+ "_gateway_container_exec_detached",
37
+ "_docker_exec",
38
+ "_gateway_exec",
39
+ "_hermes_agent_version",
40
+ "ensure_profile_api",
41
+ "hermes_commands_catalog",
42
+ "hermes_commands_exec",
43
+ "hermes_usage",
44
+ "_runtime_profile_slug",
45
+ "ensure_runtime_profile",
46
+ "ensure_user_agent_cohort",
47
+ "resolve_runtime_assignee",
48
+ "cohort_runtime_slugs",
49
+ "_prepare_runtime_profile_credentials",
50
+ "purge_stale_runtime_profiles",
51
+ "list_delegation_grants",
52
+ "create_delegation_grant",
53
+ "revoke_delegation_grant",
54
+ "_delegation_grantor_ids_for_grantee",
55
+ "_runtime_profile_on_disk",
56
+ "_invalidate_gateway_registered_cache",
57
+ "_normalize_user_avatar_url",
58
+ "_normalize_logo_url",
59
+ "_normalize_agent_avatar_patch",
60
+ "_resolve_avatar_fields",
61
+ "_upsert_agent_registry_row",
62
+ "_assign_agent_avatar",
63
+ "_avatar_id_for_display_name",
64
+ "_pick_logo_url",
65
+ "_supervisor_ready",
66
+ "_supervisor_request",
67
+ "_maybe_sync_compose_public_url",
68
+ "_supervisor_gateway_exec",
69
+ "_supervisor_container_exec",
70
+ "_supervisor_profile_lifecycle",
71
+ "_parse_messaging_settings_patch",
72
+ "_workspace_messaging_integrations_payload",
73
+ "_sync_workspace_messaging_gateway",
74
+ "room_chat_bind",
75
+ "profile_chat_bind",
76
+ "list_room_sessions",
77
+ "profile_chat_activate_room_session",
78
+ "profile_chat_message",
79
+ "_enrich_room_chat_payload",
80
+ "_room_session_rows",
81
+ "_extract_title",
82
+ "bootstrap_agent_dm_lane",
83
+ "_provision_agent_dm_runtimes",
84
+ "_ensure_default_workspace",
85
+ "_bootstrap_after_setup",
86
+ "_promote_workspace_owner_if_unclaimed",
87
+ "_sync_workspace_home_room",
88
+ "_onboard_workspace_member_rooms",
89
+ )
90
+
91
+
92
+ def test_server_wf032_reexports_present() -> None:
93
+ missing = [name for name in _REQUIRED if not hasattr(server, name)]
94
+ assert not missing, f"missing server re-exports: {missing}"
95
+
96
+
97
+ if __name__ == "__main__":
98
+ test_server_wf032_reexports_present()
99
+ print("ok")
@@ -53,4 +53,11 @@ os.environ["WORKFRAME_DEPLOYMENT_MODE"] = "trusted_team"
53
53
  stack_config.patch_stack_config({"deployment_mode": "single_user_local"})
54
54
  assert stack_config.resolve_deployment_mode() == "single_user_local"
55
55
 
56
+ # Admin verify survives reload
57
+ stack_config.patch_stack_config({"install_complete": False})
58
+ stack_config.mark_install_admin_verified("owner@example.com")
59
+ assert stack_config.install_admin_verified()
60
+ cfg = stack_config.get_stack_config()
61
+ assert cfg["smtp"]["admin_email"] == "owner@example.com"
62
+
56
63
  print("stack_config install-window smtp self-check ok")
@@ -157,11 +157,22 @@ def parse_lease_token(value: str) -> str:
157
157
  return ""
158
158
 
159
159
 
160
- def validate_lease(token: str) -> dict[str, Any] | None:
161
- """Return lease metadata if token is active (not revoked/expired)."""
160
+ def _lease_row_to_meta(row: sqlite3.Row) -> dict[str, Any]:
161
+ return {
162
+ "run_id": str(row["run_id"]),
163
+ "payer_user_id": str(row["payer_user_id"]),
164
+ "workspace_id": str(row["workspace_id"]),
165
+ "provider": str(row["provider"]),
166
+ "credential_binding_id": str(row["credential_binding_id"] or ""),
167
+ "profile_slug": str(row["profile_slug"]),
168
+ }
169
+
170
+
171
+ def inspect_lease(token: str) -> tuple[str | None, dict[str, Any] | None]:
172
+ """Return (deny_reason, lease_meta). deny_reason is None when the lease is active."""
162
173
  token = parse_lease_token(token)
163
174
  if not token:
164
- return None
175
+ return "missing_token", None
165
176
  ensure_schema()
166
177
  conn = _connect()
167
178
  try:
@@ -176,16 +187,20 @@ def validate_lease(token: str) -> dict[str, Any] | None:
176
187
  ).fetchone()
177
188
  finally:
178
189
  conn.close()
179
- if not row or row["revoked_at"] or _expired(str(row["expires_at"] or "")):
180
- return None
181
- return {
182
- "run_id": str(row["run_id"]),
183
- "payer_user_id": str(row["payer_user_id"]),
184
- "workspace_id": str(row["workspace_id"]),
185
- "provider": str(row["provider"]),
186
- "credential_binding_id": str(row["credential_binding_id"] or ""),
187
- "profile_slug": str(row["profile_slug"]),
188
- }
190
+ if not row:
191
+ return "not_found", None
192
+ meta = _lease_row_to_meta(row)
193
+ if row["revoked_at"]:
194
+ return "revoked", meta
195
+ if _expired(str(row["expires_at"] or "")):
196
+ return "expired", meta
197
+ return None, meta
198
+
199
+
200
+ def validate_lease(token: str) -> dict[str, Any] | None:
201
+ """Return lease metadata if token is active (not revoked/expired)."""
202
+ deny_reason, lease = inspect_lease(token)
203
+ return lease if deny_reason is None else None
189
204
 
190
205
 
191
206
  def revoke_lease(run_id: str) -> None: