create-workframe 0.1.5 → 0.1.6

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 (181) hide show
  1. package/.dockerignore +22 -22
  2. package/.gitignore +73 -73
  3. package/LICENSE +201 -201
  4. package/NOTICE +12 -12
  5. package/README.md +40 -40
  6. package/SECURITY.md +38 -38
  7. package/bin/create-workframe.js +2816 -2814
  8. package/bin/workframe.js +329 -329
  9. package/docs/workspace-instructions/WORKFRAME_DISCORD.md +20 -20
  10. package/docs/workspace-instructions/WORKFRAME_DOCUMENTS_AND_ARTIFACTS.md +20 -20
  11. package/docs/workspace-instructions/WORKFRAME_KANBAN.md +20 -20
  12. package/docs/workspace-instructions/WORKFRAME_ONBOARDING.md +21 -21
  13. package/docs/workspace-instructions/WORKFRAME_ROUTING.md +29 -29
  14. package/docs/workspace-instructions/WORKFRAME_TELEGRAM.md +19 -19
  15. package/package.json +64 -64
  16. package/profiles/README.md +15 -15
  17. package/profiles/architect/AGENTS.md +29 -29
  18. package/profiles/architect/SOUL.md +44 -44
  19. package/profiles/architect/skills/devops/kanban-worker/SKILL.md +27 -27
  20. package/profiles/designer/AGENTS.md +26 -26
  21. package/profiles/designer/SOUL.md +31 -31
  22. package/profiles/designer/skills/devops/kanban-worker/SKILL.md +27 -27
  23. package/profiles/dev/AGENTS.md +28 -28
  24. package/profiles/dev/SOUL.md +31 -31
  25. package/profiles/dev/skills/devops/kanban-worker/SKILL.md +27 -27
  26. package/profiles/docs/AGENTS.md +27 -27
  27. package/profiles/docs/SOUL.md +30 -30
  28. package/profiles/docs/skills/devops/kanban-worker/SKILL.md +27 -27
  29. package/profiles/research/AGENTS.md +26 -26
  30. package/profiles/research/SOUL.md +31 -31
  31. package/profiles/research/skills/devops/kanban-worker/SKILL.md +27 -27
  32. package/profiles/visionary/AGENTS.md +25 -25
  33. package/profiles/visionary/SOUL.md +31 -31
  34. package/profiles/visionary/skills/devops/kanban-worker/SKILL.md +27 -27
  35. package/profiles/workframe-agent/AGENTS.md +37 -37
  36. package/profiles/workframe-agent/SETUP.md +185 -185
  37. package/profiles/workframe-agent/SOUL.md +61 -61
  38. package/profiles/workframe-agent/skills/devops/botfather/SKILL.md +85 -85
  39. package/profiles/workframe-agent/skills/devops/kanban-handoff-pattern/SKILL.md +58 -58
  40. package/profiles/workframe-agent/skills/devops/workframe-cohort/SKILL.md +54 -54
  41. package/prompts/WORKFRAME_PROMPT_TEMPLATES.md +16 -16
  42. package/rules/.hermes.md +11 -11
  43. package/rules/AGENTS.md +22 -22
  44. package/rules/workspace-README.md +5 -5
  45. package/scripts/apply-update-workframe.sh +91 -21
  46. package/scripts/bundle-workframe-ui.mjs +83 -83
  47. package/scripts/create_workframe_scaffold.py +648 -648
  48. package/scripts/ensure-compose-host-paths.mjs +51 -51
  49. package/scripts/lib/install-identity.mjs +212 -212
  50. package/scripts/lib/workframe-registry.mjs +290 -290
  51. package/scripts/new-project.mjs +68 -68
  52. package/scripts/select_agent_pack.py +31 -31
  53. package/scripts/set-compose-public-url.mjs +92 -92
  54. package/scripts/sync-canonical-to-package.mjs +164 -164
  55. package/shared/WORKFRAME_AGENT_LIBRARY.md +31 -31
  56. package/shared/WORKFRAME_AGENT_OPERATIONS.md +29 -29
  57. package/shared/WORKFRAME_AGENT_PACKS.json +64 -64
  58. package/shared/WORKFRAME_AGENT_PACKS.yaml +20 -20
  59. package/shared/WORKFRAME_CHAT_PERMISSION_MODEL.md +20 -20
  60. package/shared/WORKFRAME_HANDOFF_SCHEMA.md +25 -25
  61. package/shared/WORKFRAME_SKILL_CURATION.md +27 -27
  62. package/workframe-api/Dockerfile +14 -14
  63. package/workframe-api/README.md +24 -24
  64. package/workframe-api/action_proxy.py +131 -131
  65. package/workframe-api/auth_rate_limit.py +49 -49
  66. package/workframe-api/credential_vault.py +445 -445
  67. package/workframe-api/data/avatar-catalog.json +41 -41
  68. package/workframe-api/data/logo-catalog.json +14 -14
  69. package/workframe-api/data/user-avatar-catalog.json +18 -18
  70. package/workframe-api/email_sender.py +220 -220
  71. package/workframe-api/google_auth.py +90 -90
  72. package/workframe-api/install_api.py +359 -359
  73. package/workframe-api/internal_proxy_auth.py +150 -150
  74. package/workframe-api/llm_proxy.py +280 -280
  75. package/workframe-api/oidc_jwt.py +108 -108
  76. package/workframe-api/package.json +12 -12
  77. package/workframe-api/platform_auth.py +194 -194
  78. package/workframe-api/profile_secret_policy.py +86 -86
  79. package/workframe-api/public/assets/index-DPXu_lGn.css +1 -1
  80. package/workframe-api/public/assets/index-DYnLrCZZ.js +8 -8
  81. package/workframe-api/public/assets/index-DglUqFB_.js +8 -8
  82. package/workframe-api/public/index.html +12 -12
  83. package/workframe-api/requirements.txt +2 -2
  84. package/workframe-api/server.py +19866 -19866
  85. package/workframe-api/site_meta.py +271 -271
  86. package/workframe-api/stack_config.py +427 -427
  87. package/workframe-api/time-bind-chat.py +99 -99
  88. package/workframe-api/turn_credentials.py +226 -226
  89. package/workframe-api/updates.py +506 -506
  90. package/workframe-api/vault_kek.py +159 -159
  91. package/workframe-api/zk_auth.py +633 -633
  92. package/workframe-supervisor/Dockerfile +11 -11
  93. package/workframe-supervisor/profile_secret_policy.py +76 -76
  94. package/workframe-supervisor/server.py +860 -860
  95. package/workframe-ui/docker/nginx.conf +85 -85
  96. package/workframe-ui/public/assets/{arc-CnriPN3G.js → arc-DoYr2T5c.js} +1 -1
  97. package/workframe-ui/public/assets/architecture-7EHR7CIX-CmDi28hh.js +1 -0
  98. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-DYYB1Ic2.js → architectureDiagram-3BPJPVTR-Clher08G.js} +1 -1
  99. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-Bn-fFOyj.js → blockDiagram-GPEHLZMM-Bdw9txrZ.js} +1 -1
  100. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BPPDtAnD.js → c4Diagram-AAUBKEIU-DlI_h_uo.js} +1 -1
  101. package/workframe-ui/public/assets/channel-DZYVoEdD.js +1 -0
  102. package/workframe-ui/public/assets/{chunk-2J33WTMH-Bz2CJiC3.js → chunk-2J33WTMH-ol-uwoeq.js} +1 -1
  103. package/workframe-ui/public/assets/{chunk-3OPIFGDE-B8aMeE1B.js → chunk-3OPIFGDE-7eDhBen0.js} +1 -1
  104. package/workframe-ui/public/assets/{chunk-4BX2VUAB-bLFHQTuT.js → chunk-4BX2VUAB-xPORD7Js.js} +1 -1
  105. package/workframe-ui/public/assets/{chunk-55IACEB6-BRxhx-lG.js → chunk-55IACEB6-Cwd7dr1z.js} +1 -1
  106. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BLyOgEJ8.js → chunk-5ZQYHXKU-Ckt4Sjy1.js} +1 -1
  107. package/workframe-ui/public/assets/{chunk-727SXJPM-DiuvBl7G.js → chunk-727SXJPM-sMk1op_G.js} +1 -1
  108. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-BE1_aBdC.js → chunk-AQP2D5EJ-CMgSvr6H.js} +1 -1
  109. package/workframe-ui/public/assets/{chunk-BSJP7CBP-BicGsmBs.js → chunk-BSJP7CBP-Dx7GN0T4.js} +1 -1
  110. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-C3VTns3d.js → chunk-CSCIHK7Q--OCu86u0.js} +1 -1
  111. package/workframe-ui/public/assets/{chunk-FMBD7UC4-DSiXY3bY.js → chunk-FMBD7UC4-CejmwJpz.js} +1 -1
  112. package/workframe-ui/public/assets/{chunk-KSCS5N6A-Cvw5s3fR.js → chunk-KSCS5N6A-CvOj2Yvp.js} +1 -1
  113. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-jJifGdIQ.js → chunk-L5ZTLDWV-B0nGBjLU.js} +1 -1
  114. package/workframe-ui/public/assets/{chunk-LZXEDZCA-Maee1aqo.js → chunk-LZXEDZCA-Bdzvu227.js} +2 -2
  115. package/workframe-ui/public/assets/{chunk-ND2GUHAM-C77N3VAM.js → chunk-ND2GUHAM-CGR07jTY.js} +1 -1
  116. package/workframe-ui/public/assets/{chunk-NZK2D7GU-BY68lrPB.js → chunk-NZK2D7GU-CBojDPNC.js} +1 -1
  117. package/workframe-ui/public/assets/{chunk-O5CBEL6O-D_c4kWzh.js → chunk-O5CBEL6O-DBin5MPF.js} +1 -1
  118. package/workframe-ui/public/assets/chunk-QZHKN3VN-DwiajAaK.js +1 -0
  119. package/workframe-ui/public/assets/chunk-WU5MYG2G-Bz8vA5s7.js +1 -0
  120. package/workframe-ui/public/assets/{chunk-XPW4576I-DlQgvqBp.js → chunk-XPW4576I-TD7I7pcb.js} +1 -1
  121. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-DQiq8od0.js +1 -0
  122. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-DQiq8od0.js +1 -0
  123. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-CHCOHDSd.js → cose-bilkent-S5V4N54A-Bmm5x5xm.js} +1 -1
  124. package/workframe-ui/public/assets/{dagre-BM42HDAG-CKPcSYwU.js → dagre-BM42HDAG-zU2r0aq2.js} +1 -1
  125. package/workframe-ui/public/assets/{diagram-2AECGRRQ-C6qe0jP-.js → diagram-2AECGRRQ-AL01qukm.js} +1 -1
  126. package/workframe-ui/public/assets/{diagram-5GNKFQAL-DhQhrieU.js → diagram-5GNKFQAL-DKi1xSMg.js} +1 -1
  127. package/workframe-ui/public/assets/{diagram-KO2AKTUF-Bqf6Iw1r.js → diagram-KO2AKTUF-DQJDQ-f-.js} +1 -1
  128. package/workframe-ui/public/assets/{diagram-LMA3HP47-Bt1ms2D2.js → diagram-LMA3HP47-B1oisGUj.js} +1 -1
  129. package/workframe-ui/public/assets/{diagram-OG6HWLK6-C0Rrxe1i.js → diagram-OG6HWLK6-DjpJKffk.js} +1 -1
  130. package/workframe-ui/public/assets/{dist-BLcLhTn6.js → dist-Jk20AnQG.js} +1 -1
  131. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-Cy1GbZVu.js → erDiagram-TEJ5UH35-Vu8RtLUf.js} +1 -1
  132. package/workframe-ui/public/assets/eventmodeling-FCH6USID-BXUnTe0c.js +1 -0
  133. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-DL3ALqDG.js → flowDiagram-I6XJVG4X-CHAXEhbR.js} +1 -1
  134. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-CMpwBhXQ.js → ganttDiagram-6RSMTGT7-DhiEkfH8.js} +1 -1
  135. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-C0mwHUXL.js → gitGraph-WXDBUCRP-Cuq44XvU.js} +1 -1
  136. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-6gzhWaF0.js → gitGraphDiagram-PVQCEYII-B7kgReRt.js} +1 -1
  137. package/workframe-ui/public/assets/index-72-j0sA5.js +129 -0
  138. package/workframe-ui/public/assets/index-auJ0bLMU.css +1 -0
  139. package/workframe-ui/public/assets/{info-J43DQDTF-Bb_wZTxU.js → info-J43DQDTF-oV4b3M4f.js} +1 -1
  140. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-601evYFV.js → infoDiagram-5YYISTIA-CthN4b6o.js} +1 -1
  141. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-BYtF-wHm.js → ishikawaDiagram-YF4QCWOH-oEGS3TKg.js} +1 -1
  142. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-BMizyHzC.js → journeyDiagram-JHISSGLW-CXJsWmKF.js} +1 -1
  143. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-CLLJsdnh.js → kanban-definition-UN3LZRKU-DTPo8yHW.js} +1 -1
  144. package/workframe-ui/public/assets/{line-BFiSvu-q.js → line-Bm9Pmcnz.js} +1 -1
  145. package/workframe-ui/public/assets/{linear-D6HYZ18_.js → linear-Cn-uD6eX.js} +1 -1
  146. package/workframe-ui/public/assets/{mermaid-parser.core-Cid5yDBQ.js → mermaid-parser.core-BBVSTQZ4.js} +2 -2
  147. package/workframe-ui/public/assets/{mermaid.core-DqIiju5Z.js → mermaid.core-DE668nUy.js} +3 -3
  148. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-Bg7xJdsz.js → mindmap-definition-RKZ34NQL-4qVsiwCf.js} +1 -1
  149. package/workframe-ui/public/assets/{packet-YPE3B663-1ebGt86D.js → packet-YPE3B663-CqJeoiFG.js} +1 -1
  150. package/workframe-ui/public/assets/{pie-LRSECV5Y-DJ9wOBC_.js → pie-LRSECV5Y-DcOqVssg.js} +1 -1
  151. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-BmK96Aus.js → pieDiagram-4H26LBE5-CosMnpAM.js} +1 -1
  152. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-ByUqtWPZ.js → quadrantDiagram-W4KKPZXB-DtsnIK98.js} +1 -1
  153. package/workframe-ui/public/assets/{radar-GUYGQ44K-DN-Hkt96.js → radar-GUYGQ44K-BqLpJuWc.js} +1 -1
  154. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-ByU2r8gb.js → requirementDiagram-4Y6WPE33-Pb7cuX8_.js} +1 -1
  155. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-elyWa0GK.js → sankeyDiagram-5OEKKPKP-BJBlMM1m.js} +1 -1
  156. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-CzZwsAiK.js → sequenceDiagram-3UESZ5HK-i4HnjP39.js} +1 -1
  157. package/workframe-ui/public/assets/{src-5vgsM_dl.js → src-DIhmc_XG.js} +1 -1
  158. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-B0O5w5lJ.js → stateDiagram-AJRCARHV-Ba2iClPT.js} +1 -1
  159. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-aUpqRAhy.js +1 -0
  160. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-oyqa5w5y.js → timeline-definition-PNZ67QCA-CL4X9vSF.js} +1 -1
  161. package/workframe-ui/public/assets/{treeView-BLDUP644-MCV12-eP.js → treeView-BLDUP644-BJrjnbMj.js} +1 -1
  162. package/workframe-ui/public/assets/{treemap-LRROVOQU-DiA1P9n5.js → treemap-LRROVOQU-CN6H54RF.js} +1 -1
  163. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CO_4x7OR.js → vennDiagram-CIIHVFJN-Cee6sbUy.js} +1 -1
  164. package/workframe-ui/public/assets/{wardley-L42UT6IY-CmQNv6Xv.js → wardley-L42UT6IY-D0cXq60c.js} +1 -1
  165. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-CJxDkbIj.js → wardleyDiagram-YWT4CUSO-CaH9XkCx.js} +1 -1
  166. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-B02MxgzG.js → xychartDiagram-2RQKCTM6-BEisgFoI.js} +1 -1
  167. package/workframe-ui/public/favicon.svg +7 -7
  168. package/workframe-ui/public/icons.svg +24 -24
  169. package/workframe-ui/public/index.html +47 -47
  170. package/workframe-ui/public/manifest.webmanifest +18 -18
  171. package/workframe-ui/public/workframe-config.json +3 -3
  172. package/workframe-ui/public/assets/architecture-7EHR7CIX-CFhr7xAe.js +0 -1
  173. package/workframe-ui/public/assets/channel-Dok_-T0o.js +0 -1
  174. package/workframe-ui/public/assets/chunk-QZHKN3VN-C3wwLrTa.js +0 -1
  175. package/workframe-ui/public/assets/chunk-WU5MYG2G-GFFSg2kf.js +0 -1
  176. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-DKbGfT5Z.js +0 -1
  177. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-DKbGfT5Z.js +0 -1
  178. package/workframe-ui/public/assets/eventmodeling-FCH6USID-C_PrceHe.js +0 -1
  179. package/workframe-ui/public/assets/index-DyO6K0TD.css +0 -1
  180. package/workframe-ui/public/assets/index-yvpSUs8T.js +0 -133
  181. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-RXZRaSgT.js +0 -1
@@ -1,99 +1,99 @@
1
- #!/usr/bin/env python3
2
- """Time bind + credential lease on running workframe-api (docker exec)."""
3
- from __future__ import annotations
4
-
5
- import sqlite3
6
- import sys
7
- import time
8
-
9
- import server
10
-
11
-
12
- def main() -> int:
13
- conn = sqlite3.connect(str(server.DATA_DIR / "workframe.db"))
14
- conn.row_factory = sqlite3.Row
15
- room = conn.execute(
16
- """
17
- SELECT r.id, r.workspace_id, r.agent_profile_id, ap.slug AS agent_slug
18
- FROM rooms r
19
- JOIN agent_profiles ap ON ap.id = r.agent_profile_id
20
- JOIN room_memberships rm ON rm.room_id = r.id AND rm.status = 'active'
21
- WHERE r.room_type = 'direct' AND r.deleted_at IS NULL
22
- AND ap.slug = 'workframe-agent'
23
- ORDER BY r.updated_at DESC
24
- LIMIT 1
25
- """,
26
- ).fetchone()
27
- if not room:
28
- print("no workframe-agent room", file=sys.stderr)
29
- return 1
30
- user = conn.execute(
31
- """
32
- SELECT u.id FROM users u
33
- JOIN room_memberships rm ON rm.user_id = u.id AND rm.room_id = ?
34
- WHERE u.status = 'active'
35
- LIMIT 1
36
- """,
37
- (str(room["id"]),),
38
- ).fetchone()
39
- conn.close()
40
- if not room or not user:
41
- print("no room/user", file=sys.stderr)
42
- return 1
43
-
44
- user_id = str(user["id"])
45
- room_id = str(room["id"])
46
- workspace_id = str(room["workspace_id"])
47
- payload = {
48
- "room_id": room_id,
49
- "workspace_id": workspace_id,
50
- "source_id": "timing",
51
- "client_id": "timing",
52
- "binding_version": 2,
53
- }
54
-
55
- t_rt = time.perf_counter()
56
- prof = server._resolve_chat_hermes_profile("workframe-agent", user_id, room_id, workspace_id)
57
- rt_ms = (time.perf_counter() - t_rt) * 1000
58
- print(f"resolve_runtime_ms={rt_ms:.0f} profile={prof}")
59
-
60
- provider = server._llm_billing_provider(prof)
61
- t_api = time.perf_counter()
62
- server.ensure_profile_api(prof, user_id, workspace_id)
63
- api_ms = (time.perf_counter() - t_api) * 1000
64
- t_llm = time.perf_counter()
65
- server._overlay_chat_llm_env(prof, user_id, workspace_id, provider)
66
- llm_ms = (time.perf_counter() - t_llm) * 1000
67
- print(f"ensure_profile_api_ms={api_ms:.0f} overlay_llm_ms={llm_ms:.0f}")
68
-
69
- t0 = time.perf_counter()
70
- session = server.profile_chat_session("workframe-agent", payload, user_id=user_id)
71
- session_ms = (time.perf_counter() - t0) * 1000
72
- sid = str(session.get("session_id") or "")
73
- print(f"session_ms={session_ms:.0f} session={sid[:40]}")
74
-
75
- t_hist = time.perf_counter()
76
- history = server.chat_messages(prof, sid)
77
- hist_ms = (time.perf_counter() - t_hist) * 1000
78
- print(f"history_ms={hist_ms:.0f} messages={len(history.get('messages') or [])}")
79
-
80
- healthy = server._profile_api_healthy(prof)
81
- print(f"profile_api_healthy={healthy} port={server._profile_api_port(prof)}")
82
-
83
- run_a = "timing-run-a"
84
- run_b = "timing-run-b"
85
- t1 = time.perf_counter()
86
- server._apply_turn_credential_lease(prof, user_id, workspace_id, provider, run_a)
87
- lease1_ms = (time.perf_counter() - t1) * 1000
88
- t2 = time.perf_counter()
89
- server._apply_turn_credential_lease(prof, user_id, workspace_id, provider, run_b)
90
- lease2_ms = (time.perf_counter() - t2) * 1000
91
- print(f"lease_cold_ms={lease1_ms:.0f} lease_reuse_ms={lease2_ms:.0f} provider={provider}")
92
-
93
- _, model = server._read_model_from_config(prof)
94
- print(f"model={model}")
95
- return 0
96
-
97
-
98
- if __name__ == "__main__":
99
- raise SystemExit(main())
1
+ #!/usr/bin/env python3
2
+ """Time bind + credential lease on running workframe-api (docker exec)."""
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ import sys
7
+ import time
8
+
9
+ import server
10
+
11
+
12
+ def main() -> int:
13
+ conn = sqlite3.connect(str(server.DATA_DIR / "workframe.db"))
14
+ conn.row_factory = sqlite3.Row
15
+ room = conn.execute(
16
+ """
17
+ SELECT r.id, r.workspace_id, r.agent_profile_id, ap.slug AS agent_slug
18
+ FROM rooms r
19
+ JOIN agent_profiles ap ON ap.id = r.agent_profile_id
20
+ JOIN room_memberships rm ON rm.room_id = r.id AND rm.status = 'active'
21
+ WHERE r.room_type = 'direct' AND r.deleted_at IS NULL
22
+ AND ap.slug = 'workframe-agent'
23
+ ORDER BY r.updated_at DESC
24
+ LIMIT 1
25
+ """,
26
+ ).fetchone()
27
+ if not room:
28
+ print("no workframe-agent room", file=sys.stderr)
29
+ return 1
30
+ user = conn.execute(
31
+ """
32
+ SELECT u.id FROM users u
33
+ JOIN room_memberships rm ON rm.user_id = u.id AND rm.room_id = ?
34
+ WHERE u.status = 'active'
35
+ LIMIT 1
36
+ """,
37
+ (str(room["id"]),),
38
+ ).fetchone()
39
+ conn.close()
40
+ if not room or not user:
41
+ print("no room/user", file=sys.stderr)
42
+ return 1
43
+
44
+ user_id = str(user["id"])
45
+ room_id = str(room["id"])
46
+ workspace_id = str(room["workspace_id"])
47
+ payload = {
48
+ "room_id": room_id,
49
+ "workspace_id": workspace_id,
50
+ "source_id": "timing",
51
+ "client_id": "timing",
52
+ "binding_version": 2,
53
+ }
54
+
55
+ t_rt = time.perf_counter()
56
+ prof = server._resolve_chat_hermes_profile("workframe-agent", user_id, room_id, workspace_id)
57
+ rt_ms = (time.perf_counter() - t_rt) * 1000
58
+ print(f"resolve_runtime_ms={rt_ms:.0f} profile={prof}")
59
+
60
+ provider = server._llm_billing_provider(prof)
61
+ t_api = time.perf_counter()
62
+ server.ensure_profile_api(prof, user_id, workspace_id)
63
+ api_ms = (time.perf_counter() - t_api) * 1000
64
+ t_llm = time.perf_counter()
65
+ server._overlay_chat_llm_env(prof, user_id, workspace_id, provider)
66
+ llm_ms = (time.perf_counter() - t_llm) * 1000
67
+ print(f"ensure_profile_api_ms={api_ms:.0f} overlay_llm_ms={llm_ms:.0f}")
68
+
69
+ t0 = time.perf_counter()
70
+ session = server.profile_chat_session("workframe-agent", payload, user_id=user_id)
71
+ session_ms = (time.perf_counter() - t0) * 1000
72
+ sid = str(session.get("session_id") or "")
73
+ print(f"session_ms={session_ms:.0f} session={sid[:40]}")
74
+
75
+ t_hist = time.perf_counter()
76
+ history = server.chat_messages(prof, sid)
77
+ hist_ms = (time.perf_counter() - t_hist) * 1000
78
+ print(f"history_ms={hist_ms:.0f} messages={len(history.get('messages') or [])}")
79
+
80
+ healthy = server._profile_api_healthy(prof)
81
+ print(f"profile_api_healthy={healthy} port={server._profile_api_port(prof)}")
82
+
83
+ run_a = "timing-run-a"
84
+ run_b = "timing-run-b"
85
+ t1 = time.perf_counter()
86
+ server._apply_turn_credential_lease(prof, user_id, workspace_id, provider, run_a)
87
+ lease1_ms = (time.perf_counter() - t1) * 1000
88
+ t2 = time.perf_counter()
89
+ server._apply_turn_credential_lease(prof, user_id, workspace_id, provider, run_b)
90
+ lease2_ms = (time.perf_counter() - t2) * 1000
91
+ print(f"lease_cold_ms={lease1_ms:.0f} lease_reuse_ms={lease2_ms:.0f} provider={provider}")
92
+
93
+ _, model = server._read_model_from_config(prof)
94
+ print(f"model={model}")
95
+ return 0
96
+
97
+
98
+ if __name__ == "__main__":
99
+ raise SystemExit(main())
@@ -1,226 +1,226 @@
1
- """Per-run credential leases — opaque tokens for Hermes; real secrets stay in API vault."""
2
-
3
- from __future__ import annotations
4
-
5
- import hashlib
6
- import os
7
- import secrets
8
- import sqlite3
9
- import time
10
- from datetime import datetime, timedelta, timezone
11
- from pathlib import Path
12
- from typing import Any
13
-
14
- import credential_vault
15
-
16
- DATA_DIR = Path(os.environ.get("WORKFRAME_API_DATA_DIR", "/app/data"))
17
- WORKFRAME_DB = DATA_DIR / "workframe.db"
18
- LEASE_PREFIX = "wf_rt_"
19
- DEFAULT_TTL = int(os.environ.get("WORKFRAME_TURN_LEASE_TTL", "900"))
20
-
21
-
22
- def _connect() -> sqlite3.Connection:
23
- conn = sqlite3.connect(str(WORKFRAME_DB), timeout=5.0)
24
- conn.row_factory = sqlite3.Row
25
- return conn
26
-
27
-
28
- # ponytail: schema DDL ran on every validate_lease/issue — ~38ms/call on bind-mounted
29
- # sqlite. Guard by DB path so it runs once per process (tests reassign WORKFRAME_DB → re-run).
30
- _SCHEMA_READY: set[str] = set()
31
-
32
-
33
- def ensure_schema() -> None:
34
- credential_vault.ensure_schema()
35
- key = str(WORKFRAME_DB)
36
- if key in _SCHEMA_READY:
37
- return
38
- conn = _connect()
39
- try:
40
- conn.execute(
41
- """
42
- CREATE TABLE IF NOT EXISTS turn_credential_leases (
43
- run_id TEXT PRIMARY KEY,
44
- token_hash TEXT NOT NULL UNIQUE,
45
- payer_user_id TEXT NOT NULL,
46
- workspace_id TEXT NOT NULL,
47
- provider TEXT NOT NULL,
48
- credential_binding_id TEXT DEFAULT NULL,
49
- profile_slug TEXT NOT NULL,
50
- expires_at TEXT NOT NULL,
51
- revoked_at TEXT DEFAULT NULL,
52
- created_at TEXT NOT NULL
53
- )
54
- """
55
- )
56
- conn.execute(
57
- "CREATE INDEX IF NOT EXISTS idx_turn_leases_token "
58
- "ON turn_credential_leases(token_hash)"
59
- )
60
- conn.execute(
61
- "CREATE INDEX IF NOT EXISTS idx_turn_leases_expiry "
62
- "ON turn_credential_leases(expires_at)"
63
- )
64
- conn.commit()
65
- finally:
66
- conn.close()
67
- _SCHEMA_READY.add(key)
68
-
69
-
70
- def _hash_token(token: str) -> str:
71
- return hashlib.sha256(str(token or "").encode("utf-8")).hexdigest()
72
-
73
-
74
- def _expired(expires_at: str) -> bool:
75
- value = str(expires_at or "").strip()
76
- if not value:
77
- return True
78
- if value.isdigit():
79
- return int(value) < int(time.time())
80
- try:
81
- return datetime.fromisoformat(value.replace("Z", "+00:00")) < datetime.now(timezone.utc)
82
- except ValueError:
83
- return True
84
-
85
-
86
- def issue_lease(
87
- run_id: str,
88
- payer_user_id: str,
89
- workspace_id: str,
90
- provider: str,
91
- profile_slug: str,
92
- credential_binding_id: str | None,
93
- *,
94
- ttl_seconds: int = DEFAULT_TTL,
95
- ) -> str:
96
- """Create lease; return full token value for profile env (wf_rt_…)."""
97
- run_id = str(run_id or "").strip()
98
- payer_user_id = str(payer_user_id or "").strip()
99
- workspace_id = str(workspace_id or "").strip()
100
- provider = str(provider or "openrouter").strip().lower()
101
- profile_slug = str(profile_slug or "").strip()
102
- if not run_id or not payer_user_id or not profile_slug:
103
- raise ValueError("run_id, payer_user_id, and profile_slug required")
104
- ttl = int(ttl_seconds or DEFAULT_TTL)
105
- if ttl <= 0:
106
- raise ValueError("ttl_seconds must be positive")
107
-
108
- ensure_schema()
109
- raw = secrets.token_hex(32)
110
- token = f"{LEASE_PREFIX}{raw}"
111
- now = datetime.now(timezone.utc)
112
- expires_at = (now + timedelta(seconds=ttl)).isoformat()
113
- created_at = now.isoformat()
114
- conn = _connect()
115
- try:
116
- conn.execute(
117
- """
118
- INSERT INTO turn_credential_leases (
119
- run_id, token_hash, payer_user_id, workspace_id, provider,
120
- credential_binding_id, profile_slug, expires_at, created_at
121
- ) VALUES (?,?,?,?,?,?,?,?,?)
122
- ON CONFLICT(run_id) DO UPDATE SET
123
- token_hash = excluded.token_hash,
124
- payer_user_id = excluded.payer_user_id,
125
- workspace_id = excluded.workspace_id,
126
- provider = excluded.provider,
127
- credential_binding_id = excluded.credential_binding_id,
128
- profile_slug = excluded.profile_slug,
129
- expires_at = excluded.expires_at,
130
- revoked_at = NULL,
131
- created_at = excluded.created_at
132
- """,
133
- (
134
- run_id,
135
- _hash_token(token),
136
- payer_user_id,
137
- workspace_id,
138
- provider,
139
- str(credential_binding_id or "") or None,
140
- profile_slug,
141
- expires_at,
142
- created_at,
143
- ),
144
- )
145
- conn.commit()
146
- finally:
147
- conn.close()
148
- return token
149
-
150
-
151
- def parse_lease_token(value: str) -> str:
152
- raw = str(value or "").strip()
153
- if raw.startswith(LEASE_PREFIX):
154
- return raw
155
- if raw and not raw.startswith(LEASE_PREFIX):
156
- return f"{LEASE_PREFIX}{raw}"
157
- return ""
158
-
159
-
160
- def validate_lease(token: str) -> dict[str, Any] | None:
161
- """Return lease metadata if token is active (not revoked/expired)."""
162
- token = parse_lease_token(token)
163
- if not token:
164
- return None
165
- ensure_schema()
166
- conn = _connect()
167
- try:
168
- row = conn.execute(
169
- """
170
- SELECT run_id, payer_user_id, workspace_id, provider,
171
- credential_binding_id, profile_slug, expires_at, revoked_at
172
- FROM turn_credential_leases
173
- WHERE token_hash = ?
174
- """,
175
- (_hash_token(token),),
176
- ).fetchone()
177
- finally:
178
- 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
- }
189
-
190
-
191
- def revoke_lease(run_id: str) -> None:
192
- run_id = str(run_id or "").strip()
193
- if not run_id:
194
- return
195
- ensure_schema()
196
- now = datetime.now(timezone.utc).isoformat()
197
- conn = _connect()
198
- try:
199
- conn.execute(
200
- "UPDATE turn_credential_leases SET revoked_at = ? WHERE run_id = ? AND revoked_at IS NULL",
201
- (now, run_id),
202
- )
203
- conn.commit()
204
- finally:
205
- conn.close()
206
-
207
-
208
- def resolve_lease_secret(lease: dict[str, Any], resolve_secret_fn) -> tuple[str, str]:
209
- """resolve_secret_fn(user_id, workspace_id, provider, binding_id) -> (env_var, secret)."""
210
- binding_id = str(lease.get("credential_binding_id") or "").strip()
211
- provider = str(lease.get("provider") or "openrouter")
212
- payer = str(lease.get("payer_user_id") or "")
213
- workspace = str(lease.get("workspace_id") or "")
214
- return resolve_secret_fn(payer, workspace, provider, binding_id)
215
-
216
-
217
- if __name__ == "__main__":
218
- ensure_schema()
219
- rid = "run-selfcheck"
220
- tok = issue_lease(rid, "user-a", "ws-1", "openrouter", "u-user-a-architect", "bind-1")
221
- assert tok.startswith(LEASE_PREFIX)
222
- meta = validate_lease(tok)
223
- assert meta and meta["run_id"] == rid
224
- revoke_lease(rid)
225
- assert validate_lease(tok) is None
226
- print("turn_credentials ok")
1
+ """Per-run credential leases — opaque tokens for Hermes; real secrets stay in API vault."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ import secrets
8
+ import sqlite3
9
+ import time
10
+ from datetime import datetime, timedelta, timezone
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import credential_vault
15
+
16
+ DATA_DIR = Path(os.environ.get("WORKFRAME_API_DATA_DIR", "/app/data"))
17
+ WORKFRAME_DB = DATA_DIR / "workframe.db"
18
+ LEASE_PREFIX = "wf_rt_"
19
+ DEFAULT_TTL = int(os.environ.get("WORKFRAME_TURN_LEASE_TTL", "900"))
20
+
21
+
22
+ def _connect() -> sqlite3.Connection:
23
+ conn = sqlite3.connect(str(WORKFRAME_DB), timeout=5.0)
24
+ conn.row_factory = sqlite3.Row
25
+ return conn
26
+
27
+
28
+ # ponytail: schema DDL ran on every validate_lease/issue — ~38ms/call on bind-mounted
29
+ # sqlite. Guard by DB path so it runs once per process (tests reassign WORKFRAME_DB → re-run).
30
+ _SCHEMA_READY: set[str] = set()
31
+
32
+
33
+ def ensure_schema() -> None:
34
+ credential_vault.ensure_schema()
35
+ key = str(WORKFRAME_DB)
36
+ if key in _SCHEMA_READY:
37
+ return
38
+ conn = _connect()
39
+ try:
40
+ conn.execute(
41
+ """
42
+ CREATE TABLE IF NOT EXISTS turn_credential_leases (
43
+ run_id TEXT PRIMARY KEY,
44
+ token_hash TEXT NOT NULL UNIQUE,
45
+ payer_user_id TEXT NOT NULL,
46
+ workspace_id TEXT NOT NULL,
47
+ provider TEXT NOT NULL,
48
+ credential_binding_id TEXT DEFAULT NULL,
49
+ profile_slug TEXT NOT NULL,
50
+ expires_at TEXT NOT NULL,
51
+ revoked_at TEXT DEFAULT NULL,
52
+ created_at TEXT NOT NULL
53
+ )
54
+ """
55
+ )
56
+ conn.execute(
57
+ "CREATE INDEX IF NOT EXISTS idx_turn_leases_token "
58
+ "ON turn_credential_leases(token_hash)"
59
+ )
60
+ conn.execute(
61
+ "CREATE INDEX IF NOT EXISTS idx_turn_leases_expiry "
62
+ "ON turn_credential_leases(expires_at)"
63
+ )
64
+ conn.commit()
65
+ finally:
66
+ conn.close()
67
+ _SCHEMA_READY.add(key)
68
+
69
+
70
+ def _hash_token(token: str) -> str:
71
+ return hashlib.sha256(str(token or "").encode("utf-8")).hexdigest()
72
+
73
+
74
+ def _expired(expires_at: str) -> bool:
75
+ value = str(expires_at or "").strip()
76
+ if not value:
77
+ return True
78
+ if value.isdigit():
79
+ return int(value) < int(time.time())
80
+ try:
81
+ return datetime.fromisoformat(value.replace("Z", "+00:00")) < datetime.now(timezone.utc)
82
+ except ValueError:
83
+ return True
84
+
85
+
86
+ def issue_lease(
87
+ run_id: str,
88
+ payer_user_id: str,
89
+ workspace_id: str,
90
+ provider: str,
91
+ profile_slug: str,
92
+ credential_binding_id: str | None,
93
+ *,
94
+ ttl_seconds: int = DEFAULT_TTL,
95
+ ) -> str:
96
+ """Create lease; return full token value for profile env (wf_rt_…)."""
97
+ run_id = str(run_id or "").strip()
98
+ payer_user_id = str(payer_user_id or "").strip()
99
+ workspace_id = str(workspace_id or "").strip()
100
+ provider = str(provider or "openrouter").strip().lower()
101
+ profile_slug = str(profile_slug or "").strip()
102
+ if not run_id or not payer_user_id or not profile_slug:
103
+ raise ValueError("run_id, payer_user_id, and profile_slug required")
104
+ ttl = int(ttl_seconds or DEFAULT_TTL)
105
+ if ttl <= 0:
106
+ raise ValueError("ttl_seconds must be positive")
107
+
108
+ ensure_schema()
109
+ raw = secrets.token_hex(32)
110
+ token = f"{LEASE_PREFIX}{raw}"
111
+ now = datetime.now(timezone.utc)
112
+ expires_at = (now + timedelta(seconds=ttl)).isoformat()
113
+ created_at = now.isoformat()
114
+ conn = _connect()
115
+ try:
116
+ conn.execute(
117
+ """
118
+ INSERT INTO turn_credential_leases (
119
+ run_id, token_hash, payer_user_id, workspace_id, provider,
120
+ credential_binding_id, profile_slug, expires_at, created_at
121
+ ) VALUES (?,?,?,?,?,?,?,?,?)
122
+ ON CONFLICT(run_id) DO UPDATE SET
123
+ token_hash = excluded.token_hash,
124
+ payer_user_id = excluded.payer_user_id,
125
+ workspace_id = excluded.workspace_id,
126
+ provider = excluded.provider,
127
+ credential_binding_id = excluded.credential_binding_id,
128
+ profile_slug = excluded.profile_slug,
129
+ expires_at = excluded.expires_at,
130
+ revoked_at = NULL,
131
+ created_at = excluded.created_at
132
+ """,
133
+ (
134
+ run_id,
135
+ _hash_token(token),
136
+ payer_user_id,
137
+ workspace_id,
138
+ provider,
139
+ str(credential_binding_id or "") or None,
140
+ profile_slug,
141
+ expires_at,
142
+ created_at,
143
+ ),
144
+ )
145
+ conn.commit()
146
+ finally:
147
+ conn.close()
148
+ return token
149
+
150
+
151
+ def parse_lease_token(value: str) -> str:
152
+ raw = str(value or "").strip()
153
+ if raw.startswith(LEASE_PREFIX):
154
+ return raw
155
+ if raw and not raw.startswith(LEASE_PREFIX):
156
+ return f"{LEASE_PREFIX}{raw}"
157
+ return ""
158
+
159
+
160
+ def validate_lease(token: str) -> dict[str, Any] | None:
161
+ """Return lease metadata if token is active (not revoked/expired)."""
162
+ token = parse_lease_token(token)
163
+ if not token:
164
+ return None
165
+ ensure_schema()
166
+ conn = _connect()
167
+ try:
168
+ row = conn.execute(
169
+ """
170
+ SELECT run_id, payer_user_id, workspace_id, provider,
171
+ credential_binding_id, profile_slug, expires_at, revoked_at
172
+ FROM turn_credential_leases
173
+ WHERE token_hash = ?
174
+ """,
175
+ (_hash_token(token),),
176
+ ).fetchone()
177
+ finally:
178
+ 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
+ }
189
+
190
+
191
+ def revoke_lease(run_id: str) -> None:
192
+ run_id = str(run_id or "").strip()
193
+ if not run_id:
194
+ return
195
+ ensure_schema()
196
+ now = datetime.now(timezone.utc).isoformat()
197
+ conn = _connect()
198
+ try:
199
+ conn.execute(
200
+ "UPDATE turn_credential_leases SET revoked_at = ? WHERE run_id = ? AND revoked_at IS NULL",
201
+ (now, run_id),
202
+ )
203
+ conn.commit()
204
+ finally:
205
+ conn.close()
206
+
207
+
208
+ def resolve_lease_secret(lease: dict[str, Any], resolve_secret_fn) -> tuple[str, str]:
209
+ """resolve_secret_fn(user_id, workspace_id, provider, binding_id) -> (env_var, secret)."""
210
+ binding_id = str(lease.get("credential_binding_id") or "").strip()
211
+ provider = str(lease.get("provider") or "openrouter")
212
+ payer = str(lease.get("payer_user_id") or "")
213
+ workspace = str(lease.get("workspace_id") or "")
214
+ return resolve_secret_fn(payer, workspace, provider, binding_id)
215
+
216
+
217
+ if __name__ == "__main__":
218
+ ensure_schema()
219
+ rid = "run-selfcheck"
220
+ tok = issue_lease(rid, "user-a", "ws-1", "openrouter", "u-user-a-architect", "bind-1")
221
+ assert tok.startswith(LEASE_PREFIX)
222
+ meta = validate_lease(tok)
223
+ assert meta and meta["run_id"] == rid
224
+ revoke_lease(rid)
225
+ assert validate_lease(tok) is None
226
+ print("turn_credentials ok")