create-workframe 0.1.0 → 0.1.1

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