create-workframe 0.1.0 → 0.1.2

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 (198) hide show
  1. package/LICENSE +201 -201
  2. package/NOTICE +12 -12
  3. package/README.md +8 -92
  4. package/SECURITY.md +38 -40
  5. package/bin/workframe.js +329 -329
  6. package/docs/workspace-instructions/WORKFRAME_ONBOARDING.md +1 -1
  7. package/docs/workspace-instructions/WORKFRAME_ROUTING.md +8 -8
  8. package/package.json +3 -6
  9. package/profiles/architect/AGENTS.md +29 -29
  10. package/profiles/architect/SOUL.md +2 -2
  11. package/profiles/architect/skills/devops/kanban-worker/SKILL.md +27 -27
  12. package/profiles/designer/AGENTS.md +26 -26
  13. package/profiles/designer/skills/devops/kanban-worker/SKILL.md +27 -27
  14. package/profiles/dev/AGENTS.md +28 -28
  15. package/profiles/dev/skills/devops/kanban-worker/SKILL.md +27 -27
  16. package/profiles/docs/AGENTS.md +27 -27
  17. package/profiles/docs/skills/devops/kanban-worker/SKILL.md +27 -27
  18. package/profiles/research/AGENTS.md +26 -26
  19. package/profiles/research/skills/devops/kanban-worker/SKILL.md +27 -27
  20. package/profiles/visionary/AGENTS.md +25 -25
  21. package/profiles/visionary/skills/devops/kanban-worker/SKILL.md +27 -27
  22. package/profiles/workframe-agent/AGENTS.md +37 -37
  23. package/profiles/workframe-agent/skills/devops/botfather/SKILL.md +85 -85
  24. package/profiles/workframe-agent/skills/devops/kanban-handoff-pattern/SKILL.md +58 -58
  25. package/profiles/workframe-agent/skills/devops/workframe-cohort/SKILL.md +54 -54
  26. package/rules/workspace-README.md +5 -5
  27. package/scripts/bundle-workframe-ui.mjs +3 -3
  28. package/scripts/ensure-compose-host-paths.mjs +51 -51
  29. package/scripts/lib/install-identity.mjs +212 -212
  30. package/scripts/set-compose-public-url.mjs +92 -92
  31. package/scripts/sync-canonical-to-package.mjs +27 -9
  32. package/shared/WORKFRAME_AGENT_LIBRARY.md +17 -17
  33. package/shared/WORKFRAME_AGENT_OPERATIONS.md +15 -15
  34. package/shared/WORKFRAME_AGENT_PACKS.json +18 -18
  35. package/shared/WORKFRAME_AGENT_PACKS.yaml +8 -8
  36. package/shared/WORKFRAME_SKILL_CURATION.md +4 -4
  37. package/workframe-api/README.md +26 -28
  38. package/workframe-api/action_proxy.py +131 -131
  39. package/workframe-api/auth_rate_limit.py +49 -49
  40. package/workframe-api/credential_vault.py +445 -445
  41. package/workframe-api/data/avatar-catalog.json +41 -41
  42. package/workframe-api/email_sender.py +220 -220
  43. package/workframe-api/google_auth.py +90 -90
  44. package/workframe-api/install_api.py +359 -359
  45. package/workframe-api/internal_proxy_auth.py +150 -150
  46. package/workframe-api/llm_proxy.py +277 -277
  47. package/workframe-api/oidc_jwt.py +108 -108
  48. package/workframe-api/package.json +12 -13
  49. package/workframe-api/public/assets/index-DPXu_lGn.css +1 -1
  50. package/workframe-api/public/assets/index-DYnLrCZZ.js +8 -8
  51. package/workframe-api/requirements.txt +2 -2
  52. package/workframe-api/site_meta.py +271 -271
  53. package/workframe-api/stack_config.py +427 -427
  54. package/workframe-api/time-bind-chat.py +99 -99
  55. package/workframe-api/turn_credentials.py +226 -226
  56. package/workframe-api/updates.py +417 -417
  57. package/workframe-api/vault_kek.py +159 -159
  58. package/workframe-api/zk_auth.py +633 -633
  59. package/workframe-supervisor/Dockerfile +11 -11
  60. package/workframe-supervisor/server.py +787 -787
  61. package/workframe-ui/docker/nginx.conf +85 -85
  62. package/workframe-ui/public/assets/{arc-CBDYvkAF.js → arc-COAT3laO.js} +1 -1
  63. package/workframe-ui/public/assets/architecture-7EHR7CIX-DUyH3hWG.js +1 -0
  64. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-XnBRKeW0.js → architectureDiagram-3BPJPVTR-BFjWV24l.js} +1 -1
  65. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-VYHUfVhd.js → blockDiagram-GPEHLZMM-DSQLPfrj.js} +1 -1
  66. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BTjUcJpm.js → c4Diagram-AAUBKEIU-DKEHv1t2.js} +1 -1
  67. package/workframe-ui/public/assets/channel-g7r_RGaY.js +1 -0
  68. package/workframe-ui/public/assets/{chunk-2J33WTMH-w7uu7R-b.js → chunk-2J33WTMH-DHZg-DUi.js} +1 -1
  69. package/workframe-ui/public/assets/{chunk-3OPIFGDE-Cb9LtnDX.js → chunk-3OPIFGDE-BB-OYTfp.js} +1 -1
  70. package/workframe-ui/public/assets/{chunk-4BX2VUAB-DiQ-qCwH.js → chunk-4BX2VUAB-C93q0YIm.js} +1 -1
  71. package/workframe-ui/public/assets/{chunk-55IACEB6-C-mLFr7z.js → chunk-55IACEB6-MAYniqik.js} +1 -1
  72. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-DOesfiCI.js → chunk-5ZQYHXKU-ChgN6YJs.js} +1 -1
  73. package/workframe-ui/public/assets/{chunk-727SXJPM-BJ3oBZuz.js → chunk-727SXJPM-B_FYwdAv.js} +1 -1
  74. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-CCA6xpGs.js → chunk-AQP2D5EJ-1_Hw_h1A.js} +1 -1
  75. package/workframe-ui/public/assets/{chunk-BSJP7CBP-a0cMNFb2.js → chunk-BSJP7CBP-CFiDQ1Rv.js} +1 -1
  76. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-kuqN8EIY.js → chunk-CSCIHK7Q-DZ9UMTlB.js} +1 -1
  77. package/workframe-ui/public/assets/{chunk-FMBD7UC4-DyPgYHCg.js → chunk-FMBD7UC4-DlMlyFgw.js} +1 -1
  78. package/workframe-ui/public/assets/{chunk-KSCS5N6A-CdUuvR0V.js → chunk-KSCS5N6A-DHXtQ_Hf.js} +1 -1
  79. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-Dq9NoWmK.js → chunk-L5ZTLDWV-CuQzg-QG.js} +1 -1
  80. package/workframe-ui/public/assets/{chunk-LZXEDZCA-p74rddlO.js → chunk-LZXEDZCA-BHzjzCGg.js} +2 -2
  81. package/workframe-ui/public/assets/{chunk-ND2GUHAM-DBD2u1Gz.js → chunk-ND2GUHAM-DHXx05n2.js} +1 -1
  82. package/workframe-ui/public/assets/{chunk-NZK2D7GU-BeIeYFnd.js → chunk-NZK2D7GU-CV5pmDM_.js} +1 -1
  83. package/workframe-ui/public/assets/{chunk-O5CBEL6O-ClHc56ib.js → chunk-O5CBEL6O-6tkCHxsV.js} +1 -1
  84. package/workframe-ui/public/assets/chunk-QZHKN3VN-C5UQehWY.js +1 -0
  85. package/workframe-ui/public/assets/chunk-WU5MYG2G-DhWllrI8.js +1 -0
  86. package/workframe-ui/public/assets/{chunk-XPW4576I-EFr8R_1p.js → chunk-XPW4576I-BClwIiCp.js} +1 -1
  87. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BBM_8T8E.js +1 -0
  88. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BBM_8T8E.js +1 -0
  89. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-C7aPBODd.js → cose-bilkent-S5V4N54A-DOrGV6DQ.js} +1 -1
  90. package/workframe-ui/public/assets/{dagre-BM42HDAG-BdU1Rv-H.js → dagre-BM42HDAG-DXTPvJkX.js} +1 -1
  91. package/workframe-ui/public/assets/{diagram-2AECGRRQ-DWowSo85.js → diagram-2AECGRRQ-xX_v-pbf.js} +1 -1
  92. package/workframe-ui/public/assets/{diagram-5GNKFQAL-MnxBbceO.js → diagram-5GNKFQAL-Cd2pXbBe.js} +1 -1
  93. package/workframe-ui/public/assets/{diagram-KO2AKTUF-DQaLRXFf.js → diagram-KO2AKTUF-Df3XvUtk.js} +1 -1
  94. package/workframe-ui/public/assets/{diagram-LMA3HP47-CQaBud9k.js → diagram-LMA3HP47-CsijIPaD.js} +1 -1
  95. package/workframe-ui/public/assets/{diagram-OG6HWLK6-D8bAXbY9.js → diagram-OG6HWLK6-aq5fmfHd.js} +1 -1
  96. package/workframe-ui/public/assets/{dist-DGpTLHr_.js → dist-D1c0mkbB.js} +1 -1
  97. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-1E-xSvBK.js → erDiagram-TEJ5UH35-DnFysVRY.js} +1 -1
  98. package/workframe-ui/public/assets/eventmodeling-FCH6USID-Ci8mdb44.js +1 -0
  99. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-CgOVD5hu.js → flowDiagram-I6XJVG4X-C6Ebi3su.js} +1 -1
  100. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-JFYAIauo.js → ganttDiagram-6RSMTGT7-BQXQtUpa.js} +1 -1
  101. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-B9REenIl.js → gitGraph-WXDBUCRP-Dt0zIs_M.js} +1 -1
  102. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-BQ7NcMSn.js → gitGraphDiagram-PVQCEYII-BF8gHzRn.js} +1 -1
  103. package/workframe-ui/public/assets/index-DpoUZAxh.css +1 -0
  104. package/workframe-ui/public/assets/{index-Dnw6vjqb.js → index-lRpzpNPT.js} +2 -2
  105. package/workframe-ui/public/assets/{info-J43DQDTF-CL6-eTjH.js → info-J43DQDTF-CSmszQJT.js} +1 -1
  106. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-LJTODW4W.js → infoDiagram-5YYISTIA-CVTKGW6p.js} +1 -1
  107. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-bchrQVuo.js → ishikawaDiagram-YF4QCWOH-Z8pT09Lv.js} +1 -1
  108. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-DkrvYuxP.js → journeyDiagram-JHISSGLW-r3wD68_T.js} +1 -1
  109. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-DFRbj0IG.js → kanban-definition-UN3LZRKU-Il8VglqN.js} +1 -1
  110. package/workframe-ui/public/assets/{line-Vd48P7-O.js → line-oyjpfz2A.js} +1 -1
  111. package/workframe-ui/public/assets/{linear-Ckizh2G7.js → linear-Cf7p5tVp.js} +1 -1
  112. package/workframe-ui/public/assets/{mermaid-parser.core-Bkimsnqj.js → mermaid-parser.core-YmbZ-AfY.js} +2 -2
  113. package/workframe-ui/public/assets/{mermaid.core-x0TvVuPo.js → mermaid.core-BFdCAqCo.js} +3 -3
  114. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-6ykAFPEz.js → mindmap-definition-RKZ34NQL-Cy2iCtEl.js} +1 -1
  115. package/workframe-ui/public/assets/{packet-YPE3B663-Dw3xgMDt.js → packet-YPE3B663-DwOBZL6K.js} +1 -1
  116. package/workframe-ui/public/assets/{pie-LRSECV5Y-DATysawG.js → pie-LRSECV5Y-04PPhnKK.js} +1 -1
  117. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-SJKD1S0S.js → pieDiagram-4H26LBE5-LxIpgHqi.js} +1 -1
  118. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-BrYDZX8q.js → quadrantDiagram-W4KKPZXB-0nBYfYm4.js} +1 -1
  119. package/workframe-ui/public/assets/{radar-GUYGQ44K-BmWYPCds.js → radar-GUYGQ44K-D2-vBqps.js} +1 -1
  120. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DwL9Mc8e.js → requirementDiagram-4Y6WPE33-DbuU0nlu.js} +1 -1
  121. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DYIFsL8h.js → sankeyDiagram-5OEKKPKP-B2hQ6B2x.js} +1 -1
  122. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-0-FPkFk8.js → sequenceDiagram-3UESZ5HK-BBrU30e1.js} +1 -1
  123. package/workframe-ui/public/assets/{src-B_od6b6h.js → src-BJEDmV70.js} +1 -1
  124. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BQCiBk6u.js → stateDiagram-AJRCARHV-7FGO4kkH.js} +1 -1
  125. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-DLTSizMg.js +1 -0
  126. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DS3tFcXj.js → timeline-definition-PNZ67QCA-ptDm4rCN.js} +1 -1
  127. package/workframe-ui/public/assets/{treeView-BLDUP644-DSyUCKLY.js → treeView-BLDUP644-CS6Z-0q8.js} +1 -1
  128. package/workframe-ui/public/assets/{treemap-LRROVOQU-CEZaNh5Y.js → treemap-LRROVOQU-DqV4Y2VA.js} +1 -1
  129. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CD-Vc9NF.js → vennDiagram-CIIHVFJN-C0UrZJYt.js} +1 -1
  130. package/workframe-ui/public/assets/{wardley-L42UT6IY-Drq5w1Mc.js → wardley-L42UT6IY-bNDN3_Sa.js} +1 -1
  131. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DouXDJoF.js → wardleyDiagram-YWT4CUSO-jWiJsefM.js} +1 -1
  132. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DDf_Lol5.js → xychartDiagram-2RQKCTM6-Dsh_fLCy.js} +1 -1
  133. package/workframe-ui/public/favicon.svg +7 -7
  134. package/workframe-ui/public/index.html +50 -50
  135. package/workframe-ui/public/workframe-config.json +3 -3
  136. package/scripts/security_audit.py +0 -156
  137. package/scripts/test-scaffold.mjs +0 -390
  138. package/workframe-api/tests/__init__.py +0 -0
  139. package/workframe-api/tests/db_setup.py +0 -13
  140. package/workframe-api/tests/test_admin_updates_gated.py +0 -30
  141. package/workframe-api/tests/test_agent_dm_bootstrap.py +0 -196
  142. package/workframe-api/tests/test_agent_profile_sync.py +0 -76
  143. package/workframe-api/tests/test_auth_email.py +0 -222
  144. package/workframe-api/tests/test_auth_hole_fix_selfcheck.py +0 -99
  145. package/workframe-api/tests/test_auth_rate_limit.py +0 -19
  146. package/workframe-api/tests/test_avatar_resolve.py +0 -77
  147. package/workframe-api/tests/test_child_soul_template.py +0 -71
  148. package/workframe-api/tests/test_credential_canary.py +0 -135
  149. package/workframe-api/tests/test_credential_isolation.py +0 -448
  150. package/workframe-api/tests/test_credential_resolution.py +0 -206
  151. package/workframe-api/tests/test_device_oauth.py +0 -108
  152. package/workframe-api/tests/test_doctor_repair.py +0 -103
  153. package/workframe-api/tests/test_ensure_profile_api.py +0 -77
  154. package/workframe-api/tests/test_gateway_compose_security.py +0 -136
  155. package/workframe-api/tests/test_install_secure_host.py +0 -39
  156. package/workframe-api/tests/test_internal_proxy_auth.py +0 -125
  157. package/workframe-api/tests/test_invite_runtime_bootstrap.py +0 -72
  158. package/workframe-api/tests/test_kanban_delegation.py +0 -185
  159. package/workframe-api/tests/test_llm_proxy.py +0 -155
  160. package/workframe-api/tests/test_login_access_policy.py +0 -183
  161. package/workframe-api/tests/test_mvp_model_bootstrap.py +0 -75
  162. package/workframe-api/tests/test_onboarding_bootstrap.py +0 -248
  163. package/workframe-api/tests/test_platform_auth.py +0 -47
  164. package/workframe-api/tests/test_profile_config_path.py +0 -56
  165. package/workframe-api/tests/test_profile_config_yaml_repair.py +0 -63
  166. package/workframe-api/tests/test_profile_create.py +0 -72
  167. package/workframe-api/tests/test_profile_identity_overlay.py +0 -61
  168. package/workframe-api/tests/test_profile_install_health.py +0 -45
  169. package/workframe-api/tests/test_profile_secret_policy.py +0 -57
  170. package/workframe-api/tests/test_profile_workspace_cwd.py +0 -34
  171. package/workframe-api/tests/test_provider_bootstrap.py +0 -75
  172. package/workframe-api/tests/test_provider_connect.py +0 -54
  173. package/workframe-api/tests/test_room_crud.py +0 -192
  174. package/workframe-api/tests/test_room_tenancy.py +0 -701
  175. package/workframe-api/tests/test_runtime_identity_backfill.py +0 -34
  176. package/workframe-api/tests/test_site_meta.py +0 -81
  177. package/workframe-api/tests/test_soul_stub.py +0 -42
  178. package/workframe-api/tests/test_space_member_sync.py +0 -99
  179. package/workframe-api/tests/test_stripe_stack_config.py +0 -37
  180. package/workframe-api/tests/test_supervisor_lifecycle.py +0 -52
  181. package/workframe-api/tests/test_turn_credential_vault.py +0 -125
  182. package/workframe-api/tests/test_updates.py +0 -176
  183. package/workframe-api/tests/test_user_cohort.py +0 -113
  184. package/workframe-api/tests/test_vault_envelope.py +0 -110
  185. package/workframe-api/tests/test_workspace_members.py +0 -183
  186. package/workframe-api/tests/test_workspace_messaging_sync.py +0 -125
  187. package/workframe-api/tests/test_workspace_provider_list.py +0 -57
  188. package/workframe-supervisor/tests/test_exec_guard.py +0 -42
  189. package/workframe-supervisor/tests/test_server_import.py +0 -21
  190. package/workframe-ui/public/assets/architecture-7EHR7CIX-CtbQKTuT.js +0 -1
  191. package/workframe-ui/public/assets/channel-Dy4Z4-jn.js +0 -1
  192. package/workframe-ui/public/assets/chunk-QZHKN3VN-CtBEchFK.js +0 -1
  193. package/workframe-ui/public/assets/chunk-WU5MYG2G-B9pBtriN.js +0 -1
  194. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-BMAEA8jI.js +0 -1
  195. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-BMAEA8jI.js +0 -1
  196. package/workframe-ui/public/assets/eventmodeling-FCH6USID-D75cstNT.js +0 -1
  197. package/workframe-ui/public/assets/index-DpAGxump.css +0 -1
  198. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-B89jAMFF.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")