create-workframe 0.1.5 → 0.1.7

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 (185) 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 +20295 -19866
  85. package/workframe-api/site_meta.py +271 -271
  86. package/workframe-api/stack_config.py +435 -427
  87. package/workframe-api/test_ensure_native_profile.py +29 -0
  88. package/workframe-api/test_oauth_llm.py +58 -0
  89. package/workframe-api/test_provider_model_surface.py +31 -0
  90. package/workframe-api/test_stack_config_install_smtp.py +51 -0
  91. package/workframe-api/time-bind-chat.py +99 -99
  92. package/workframe-api/turn_credentials.py +226 -226
  93. package/workframe-api/updates.py +506 -506
  94. package/workframe-api/vault_kek.py +159 -159
  95. package/workframe-api/zk_auth.py +633 -633
  96. package/workframe-supervisor/Dockerfile +11 -11
  97. package/workframe-supervisor/profile_secret_policy.py +76 -76
  98. package/workframe-supervisor/server.py +860 -860
  99. package/workframe-ui/docker/nginx.conf +85 -85
  100. package/workframe-ui/public/assets/{arc-CnriPN3G.js → arc-Cn-lt9fT.js} +1 -1
  101. package/workframe-ui/public/assets/architecture-7EHR7CIX-CCMNXHod.js +1 -0
  102. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-DYYB1Ic2.js → architectureDiagram-3BPJPVTR-qXoGCqfH.js} +1 -1
  103. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-Bn-fFOyj.js → blockDiagram-GPEHLZMM-DoopFlWj.js} +1 -1
  104. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BPPDtAnD.js → c4Diagram-AAUBKEIU-BRa7hfzE.js} +1 -1
  105. package/workframe-ui/public/assets/channel-CtFV3sE3.js +1 -0
  106. package/workframe-ui/public/assets/{chunk-2J33WTMH-Bz2CJiC3.js → chunk-2J33WTMH-Cl33E2hh.js} +1 -1
  107. package/workframe-ui/public/assets/{chunk-3OPIFGDE-B8aMeE1B.js → chunk-3OPIFGDE-BRywYZQO.js} +1 -1
  108. package/workframe-ui/public/assets/{chunk-4BX2VUAB-bLFHQTuT.js → chunk-4BX2VUAB-CI3xzp-0.js} +1 -1
  109. package/workframe-ui/public/assets/{chunk-55IACEB6-BRxhx-lG.js → chunk-55IACEB6-Bkf3Ye3F.js} +1 -1
  110. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BLyOgEJ8.js → chunk-5ZQYHXKU-C5RN3hUT.js} +1 -1
  111. package/workframe-ui/public/assets/{chunk-727SXJPM-DiuvBl7G.js → chunk-727SXJPM-BaY0yDwt.js} +1 -1
  112. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-BE1_aBdC.js → chunk-AQP2D5EJ-VMMBaDqd.js} +1 -1
  113. package/workframe-ui/public/assets/{chunk-BSJP7CBP-BicGsmBs.js → chunk-BSJP7CBP-DlNZ1UV3.js} +1 -1
  114. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-C3VTns3d.js → chunk-CSCIHK7Q-tT2HyVy0.js} +1 -1
  115. package/workframe-ui/public/assets/{chunk-FMBD7UC4-DSiXY3bY.js → chunk-FMBD7UC4-CyACRaUT.js} +1 -1
  116. package/workframe-ui/public/assets/{chunk-KSCS5N6A-Cvw5s3fR.js → chunk-KSCS5N6A-seK5WAZs.js} +1 -1
  117. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-jJifGdIQ.js → chunk-L5ZTLDWV-r5cyG5AH.js} +1 -1
  118. package/workframe-ui/public/assets/{chunk-LZXEDZCA-Maee1aqo.js → chunk-LZXEDZCA-SnbeVxsJ.js} +2 -2
  119. package/workframe-ui/public/assets/{chunk-ND2GUHAM-C77N3VAM.js → chunk-ND2GUHAM-zbFgEXSJ.js} +1 -1
  120. package/workframe-ui/public/assets/{chunk-NZK2D7GU-BY68lrPB.js → chunk-NZK2D7GU-UhZc8kOk.js} +1 -1
  121. package/workframe-ui/public/assets/{chunk-O5CBEL6O-D_c4kWzh.js → chunk-O5CBEL6O-DHvu2iZ-.js} +1 -1
  122. package/workframe-ui/public/assets/chunk-QZHKN3VN-B1g7l1_B.js +1 -0
  123. package/workframe-ui/public/assets/chunk-WU5MYG2G-B_qfZ6QP.js +1 -0
  124. package/workframe-ui/public/assets/{chunk-XPW4576I-DlQgvqBp.js → chunk-XPW4576I-CCPNKbzG.js} +1 -1
  125. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CJoeCNq6.js +1 -0
  126. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CJoeCNq6.js +1 -0
  127. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-CHCOHDSd.js → cose-bilkent-S5V4N54A-D1CLe6Y0.js} +1 -1
  128. package/workframe-ui/public/assets/{dagre-BM42HDAG-CKPcSYwU.js → dagre-BM42HDAG-DYHcGVs_.js} +1 -1
  129. package/workframe-ui/public/assets/{diagram-2AECGRRQ-C6qe0jP-.js → diagram-2AECGRRQ-DVUkJ-II.js} +1 -1
  130. package/workframe-ui/public/assets/{diagram-5GNKFQAL-DhQhrieU.js → diagram-5GNKFQAL-C9Y1Acc2.js} +1 -1
  131. package/workframe-ui/public/assets/{diagram-KO2AKTUF-Bqf6Iw1r.js → diagram-KO2AKTUF-Db3zez-n.js} +1 -1
  132. package/workframe-ui/public/assets/{diagram-LMA3HP47-Bt1ms2D2.js → diagram-LMA3HP47-BaOR5sIr.js} +1 -1
  133. package/workframe-ui/public/assets/{diagram-OG6HWLK6-C0Rrxe1i.js → diagram-OG6HWLK6-fLBzMY5W.js} +1 -1
  134. package/workframe-ui/public/assets/{dist-BLcLhTn6.js → dist-uZYeD3o1.js} +1 -1
  135. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-Cy1GbZVu.js → erDiagram-TEJ5UH35-BoCxDSyE.js} +1 -1
  136. package/workframe-ui/public/assets/eventmodeling-FCH6USID-Bw910jho.js +1 -0
  137. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-DL3ALqDG.js → flowDiagram-I6XJVG4X-BnYa3CAj.js} +1 -1
  138. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-CMpwBhXQ.js → ganttDiagram-6RSMTGT7-Dm_e_Kaa.js} +1 -1
  139. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-C0mwHUXL.js → gitGraph-WXDBUCRP-CBAj8ntv.js} +1 -1
  140. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-6gzhWaF0.js → gitGraphDiagram-PVQCEYII-D5auJjJh.js} +1 -1
  141. package/workframe-ui/public/assets/index-DHLQNAXd.js +129 -0
  142. package/workframe-ui/public/assets/index-auJ0bLMU.css +1 -0
  143. package/workframe-ui/public/assets/{info-J43DQDTF-Bb_wZTxU.js → info-J43DQDTF-BNMADxE2.js} +1 -1
  144. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-601evYFV.js → infoDiagram-5YYISTIA-C8c45T4J.js} +1 -1
  145. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-BYtF-wHm.js → ishikawaDiagram-YF4QCWOH-CyWuHB4e.js} +1 -1
  146. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-BMizyHzC.js → journeyDiagram-JHISSGLW-B43pCuDT.js} +1 -1
  147. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-CLLJsdnh.js → kanban-definition-UN3LZRKU-Dyn480BI.js} +1 -1
  148. package/workframe-ui/public/assets/{line-BFiSvu-q.js → line-CNtIbnmt.js} +1 -1
  149. package/workframe-ui/public/assets/{linear-D6HYZ18_.js → linear-pmSg_4uV.js} +1 -1
  150. package/workframe-ui/public/assets/{mermaid-parser.core-Cid5yDBQ.js → mermaid-parser.core-BX4tnojE.js} +2 -2
  151. package/workframe-ui/public/assets/{mermaid.core-DqIiju5Z.js → mermaid.core-BtpBva5o.js} +3 -3
  152. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-Bg7xJdsz.js → mindmap-definition-RKZ34NQL-DcMcB_69.js} +1 -1
  153. package/workframe-ui/public/assets/{packet-YPE3B663-1ebGt86D.js → packet-YPE3B663-KSzK9WlD.js} +1 -1
  154. package/workframe-ui/public/assets/{pie-LRSECV5Y-DJ9wOBC_.js → pie-LRSECV5Y-DLOH6Mg8.js} +1 -1
  155. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-BmK96Aus.js → pieDiagram-4H26LBE5-BhrX2aGk.js} +1 -1
  156. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-ByUqtWPZ.js → quadrantDiagram-W4KKPZXB-B5dDjbTX.js} +1 -1
  157. package/workframe-ui/public/assets/{radar-GUYGQ44K-DN-Hkt96.js → radar-GUYGQ44K-BPdjqZiu.js} +1 -1
  158. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-ByU2r8gb.js → requirementDiagram-4Y6WPE33-21Dq5e0N.js} +1 -1
  159. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-elyWa0GK.js → sankeyDiagram-5OEKKPKP-CiA99dbO.js} +1 -1
  160. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-CzZwsAiK.js → sequenceDiagram-3UESZ5HK-Bwpui7_x.js} +1 -1
  161. package/workframe-ui/public/assets/{src-5vgsM_dl.js → src-B3Axsi1U.js} +1 -1
  162. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-B0O5w5lJ.js → stateDiagram-AJRCARHV-C76qwtE8.js} +1 -1
  163. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-Dn0CDWJD.js +1 -0
  164. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-oyqa5w5y.js → timeline-definition-PNZ67QCA-CufInOyk.js} +1 -1
  165. package/workframe-ui/public/assets/{treeView-BLDUP644-MCV12-eP.js → treeView-BLDUP644-C4sxh0R-.js} +1 -1
  166. package/workframe-ui/public/assets/{treemap-LRROVOQU-DiA1P9n5.js → treemap-LRROVOQU-BjV72ANV.js} +1 -1
  167. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CO_4x7OR.js → vennDiagram-CIIHVFJN-DgsdR0Rl.js} +1 -1
  168. package/workframe-ui/public/assets/{wardley-L42UT6IY-CmQNv6Xv.js → wardley-L42UT6IY-DZ279G_t.js} +1 -1
  169. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-CJxDkbIj.js → wardleyDiagram-YWT4CUSO-Bde25kvz.js} +1 -1
  170. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-B02MxgzG.js → xychartDiagram-2RQKCTM6-B8VnS2Pb.js} +1 -1
  171. package/workframe-ui/public/favicon.svg +7 -7
  172. package/workframe-ui/public/icons.svg +24 -24
  173. package/workframe-ui/public/index.html +47 -47
  174. package/workframe-ui/public/manifest.webmanifest +18 -18
  175. package/workframe-ui/public/workframe-config.json +3 -3
  176. package/workframe-ui/public/assets/architecture-7EHR7CIX-CFhr7xAe.js +0 -1
  177. package/workframe-ui/public/assets/channel-Dok_-T0o.js +0 -1
  178. package/workframe-ui/public/assets/chunk-QZHKN3VN-C3wwLrTa.js +0 -1
  179. package/workframe-ui/public/assets/chunk-WU5MYG2G-GFFSg2kf.js +0 -1
  180. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-DKbGfT5Z.js +0 -1
  181. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-DKbGfT5Z.js +0 -1
  182. package/workframe-ui/public/assets/eventmodeling-FCH6USID-C_PrceHe.js +0 -1
  183. package/workframe-ui/public/assets/index-DyO6K0TD.css +0 -1
  184. package/workframe-ui/public/assets/index-yvpSUs8T.js +0 -133
  185. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-RXZRaSgT.js +0 -1
@@ -0,0 +1,29 @@
1
+ """ponytail self-check: empty Hermes data must seed workframe-agent for gateway boot.
2
+
3
+ Run: python services/workframe-api/test_ensure_native_profile.py
4
+ """
5
+ import json
6
+ import os
7
+ import sys
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
12
+
13
+ import server # noqa: E402
14
+
15
+ td = Path(tempfile.mkdtemp())
16
+ os.environ["HERMES_DATA"] = str(td)
17
+ server.HERMES_DATA = td
18
+ server.ROUTES_JSON = td / "workframe" / "routes.json"
19
+
20
+ slug = server._native_profile_slug()
21
+ assert slug == "workframe-agent"
22
+ assert not server._native_profile_present()
23
+ assert server._seed_native_profile_on_disk(slug)
24
+ assert server._native_profile_present()
25
+ assert (td / "profiles" / slug / "config.yaml").is_file()
26
+ routes = json.loads((td / "workframe" / "routes.json").read_text(encoding="utf-8"))
27
+ assert any(row.get("profile") == slug for row in routes.get("routes", []))
28
+
29
+ print("ensure native profile self-check ok")
@@ -0,0 +1,58 @@
1
+ """ponytail self-check: OAuth LLM providers (Codex/Nous) must pass chat gates without API keys.
2
+
3
+ Run: python services/workframe-api/test_oauth_llm.py
4
+ """
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
9
+
10
+ import server # noqa: E402
11
+
12
+ keys = server._hermes_oauth_auth_keys("openai-codex")
13
+ assert "openai-codex" in keys
14
+ assert "codex" in keys
15
+ assert "openai_codex" in keys
16
+
17
+ pool_only = {
18
+ "version": 1,
19
+ "providers": {},
20
+ "credential_pool": {"openai-codex": [{"id": "x", "auth_type": "oauth"}]},
21
+ }
22
+ assert server._auth_json_has_oauth_material(pool_only)
23
+
24
+ # credential_pool-only auth must merge into profile auth
25
+ merged: dict = {"version": 1, "providers": {}, "credential_pool": {}}
26
+ assert server._merge_oauth_auth_into_profile(merged, pool_only, "openai-codex")
27
+ assert merged["credential_pool"].get("openai-codex")
28
+
29
+ # Without live tokens require path must fail
30
+ try:
31
+ server._require_runtime_owner_provider("user-1", "ws-1", "codex")
32
+ raise AssertionError("expected ValueError without oauth tokens")
33
+ except ValueError:
34
+ pass
35
+
36
+ _orig = server._hermes_oauth_tokens_present
37
+
38
+
39
+ def _fake_present(_user: str, _auth_id: str) -> bool:
40
+ return True
41
+
42
+
43
+ server._hermes_oauth_tokens_present = _fake_present
44
+ try:
45
+ resolved = server._require_runtime_owner_provider(
46
+ "550e8400-e29b-41d4-a716-446655440000",
47
+ "ws-1",
48
+ "codex",
49
+ )
50
+ assert resolved.get("credential_type") == "oauth"
51
+ assert resolved.get("credential_ref") == "oauth:openai-codex"
52
+ finally:
53
+ server._hermes_oauth_tokens_present = _orig
54
+
55
+ mvp = server.PROVIDER_MVP_MODELS["codex"]["primary"]
56
+ assert mvp == "gpt-5.4-medium"
57
+
58
+ print("oauth llm self-check ok")
@@ -0,0 +1,31 @@
1
+ """ponytail: connected providers → model offers → billing alignment.
2
+
3
+ Run: python services/workframe-api/test_provider_model_surface.py
4
+ """
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
9
+
10
+ import server # noqa: E402
11
+
12
+ codex_rows = server._model_catalog_rows_for_provider("codex")
13
+ codex_models = {row["model"] for row in codex_rows}
14
+ assert "gpt-5.4-medium" in codex_models, codex_models
15
+ assert "gpt-5.4-mini" in codex_models, codex_models
16
+ assert all("gemini" not in m for m in codex_models)
17
+
18
+ assert server._resolve_billing_provider_for_model("gpt-5.4-mini", {"codex"}) == "codex"
19
+ assert server._resolve_billing_provider_for_model("google/gemini-2.5-flash", {"codex"}) == ""
20
+ assert server._resolve_billing_provider_for_model(
21
+ "google/gemini-2.5-flash", {"openrouter"}
22
+ ) == "openrouter"
23
+
24
+ stack = server._suggestions_for_connected_llm_providers({"codex", "openrouter"})
25
+ models = {row["model"] for row in stack}
26
+ assert "gpt-5.4-medium" in models
27
+ assert any(m.startswith("google/") for m in models)
28
+
29
+ assert server._billing_provider_id_from_hermes_config("openai-codex") == "codex"
30
+
31
+ print("provider model surface self-check ok")
@@ -0,0 +1,51 @@
1
+ """ponytail: install window must ignore compose SMTP env (clean install, not migration).
2
+
3
+ Run: python services/workframe-api/test_stack_config_install_smtp.py
4
+ """
5
+ import os
6
+ import sys
7
+ import tempfile
8
+ from pathlib import Path
9
+
10
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
11
+
12
+ import stack_config # noqa: E402
13
+
14
+ td = Path(tempfile.mkdtemp())
15
+ stack_config.DATA_DIR = td
16
+ stack_config.CONFIG_PATH = td / "stack_config.json"
17
+
18
+ os.environ["SMTP_HOST"] = "smtp.ghost.example"
19
+ os.environ["SMTP_USER"] = "ghost@example.com"
20
+ os.environ["SMTP_PASS"] = "secret"
21
+
22
+ # Fresh install: no stack file, install window open → env ignored
23
+ assert stack_config._install_window_open()
24
+ resolved = stack_config.resolved_smtp()
25
+ assert resolved.get("source") == "none", resolved
26
+ assert not stack_config.smtp_configured()
27
+
28
+ # Wizard saves SMTP to stack file
29
+ stack_config.patch_stack_config(
30
+ {
31
+ "smtp": {
32
+ "host": "smtp.wizard.example",
33
+ "port": 587,
34
+ "user": "admin@wizard.example",
35
+ "password": "wizard-pass",
36
+ "from": "admin@wizard.example",
37
+ }
38
+ }
39
+ )
40
+ resolved = stack_config.resolved_smtp()
41
+ assert resolved.get("host") == "smtp.wizard.example", resolved
42
+ assert resolved.get("source") == "stack_config", resolved
43
+
44
+ # After install complete, env wins for ops overrides
45
+ stack_config.patch_stack_config({"install_complete": True})
46
+ assert not stack_config._install_window_open()
47
+ resolved = stack_config.resolved_smtp()
48
+ assert resolved.get("host") == "smtp.ghost.example", resolved
49
+ assert resolved.get("source") == "env", resolved
50
+
51
+ print("stack_config install-window smtp self-check ok")
@@ -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())