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,150 +1,150 @@
1
- """Shared auth for /internal/llm and /internal/action — network + optional proxy token."""
2
-
3
- from __future__ import annotations
4
-
5
- import hmac
6
- import ipaddress
7
- import os
8
- import secrets
9
- from http.server import BaseHTTPRequestHandler
10
- from pathlib import Path
11
-
12
- PROXY_TOKEN_ENV = "WORKFRAME_PROXY_TOKEN"
13
- PROXY_TOKEN_HEADER = "X-Workframe-Proxy-Token"
14
- PROFILE_HEADER = "X-Workframe-Profile"
15
- PROXY_TOKEN_FILE = ".proxy_token"
16
- SHARED_PROXY_TOKEN_PATH = Path("/run/workframe-proxy/token")
17
-
18
- _TOKEN: str | None = None
19
- _TOKEN_PATH: Path | None = None
20
-
21
-
22
- def _data_dir() -> Path:
23
- return Path(
24
- os.environ.get("WORKFRAME_API_DATA_DIR")
25
- or os.environ.get("MISSION_DATA_DIR")
26
- or Path(__file__).resolve().parent / "data"
27
- )
28
-
29
-
30
- def _deployment_mode() -> str:
31
- return (os.environ.get("WORKFRAME_DEPLOYMENT_MODE") or "trusted_team").strip().lower()
32
-
33
-
34
- def proxy_token_configured() -> bool:
35
- return bool(_loaded_proxy_token())
36
-
37
-
38
- def _loaded_proxy_token() -> str:
39
- global _TOKEN, _TOKEN_PATH
40
- if _TOKEN is not None:
41
- return _TOKEN
42
- env = str(os.environ.get(PROXY_TOKEN_ENV) or "").strip()
43
- if env:
44
- _TOKEN = env
45
- return _TOKEN
46
- path = _data_dir() / PROXY_TOKEN_FILE
47
- _TOKEN_PATH = path
48
- if path.is_file():
49
- _TOKEN = path.read_text(encoding="utf-8").strip()
50
- return _TOKEN
51
- _TOKEN = ""
52
- return ""
53
-
54
-
55
- def _sync_shared_proxy_token(token: str) -> None:
56
- value = str(token or "").strip()
57
- if not value:
58
- return
59
- try:
60
- SHARED_PROXY_TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)
61
- SHARED_PROXY_TOKEN_PATH.write_text(value + "\n", encoding="utf-8")
62
- except OSError:
63
- pass
64
-
65
-
66
- def bootstrap_proxy_token(*, allow_generate_file: bool = True) -> str:
67
- """Load proxy token from env or data dir; optionally create file for local dogfood."""
68
- global _TOKEN
69
- existing = str(os.environ.get(PROXY_TOKEN_ENV) or "").strip()
70
- if existing:
71
- _TOKEN = existing
72
- _sync_shared_proxy_token(existing)
73
- return existing
74
- path = _data_dir() / PROXY_TOKEN_FILE
75
- if path.is_file():
76
- _TOKEN = path.read_text(encoding="utf-8").strip()
77
- _sync_shared_proxy_token(_TOKEN)
78
- return _TOKEN
79
- if not allow_generate_file or _deployment_mode() == "public_multi_user":
80
- _TOKEN = ""
81
- return ""
82
- token = secrets.token_urlsafe(32)
83
- path.parent.mkdir(parents=True, exist_ok=True)
84
- path.write_text(token + "\n", encoding="utf-8")
85
- # ponytail: gateway reads same token from shared compose volume when env unset
86
- try:
87
- SHARED_PROXY_TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)
88
- SHARED_PROXY_TOKEN_PATH.write_text(token + "\n", encoding="utf-8")
89
- except OSError:
90
- pass
91
- _TOKEN = token
92
- return token
93
-
94
-
95
- def reset_proxy_token_for_tests() -> None:
96
- global _TOKEN, _TOKEN_PATH
97
- _TOKEN = None
98
- _TOKEN_PATH = None
99
-
100
-
101
- def _client_ip(handler: BaseHTTPRequestHandler) -> str:
102
- return str(handler.client_address[0] if handler.client_address else "").strip()
103
-
104
-
105
- def is_internal_client(host: str) -> bool:
106
- """Private/docker callers only — not routable public addresses."""
107
- addr = str(host or "").strip()
108
- if not addr:
109
- return False
110
- if addr in {"127.0.0.1", "::1", "localhost"}:
111
- return True
112
- try:
113
- ip = ipaddress.ip_address(addr.split("%", 1)[0])
114
- except ValueError:
115
- return False
116
- return ip.is_private or ip.is_loopback
117
-
118
-
119
- def _header_token(handler: BaseHTTPRequestHandler) -> str:
120
- return str(handler.headers.get(PROXY_TOKEN_HEADER) or "").strip()
121
-
122
-
123
- def authorize_internal_proxy(handler: BaseHTTPRequestHandler) -> tuple[bool, str]:
124
- """Return (ok, error_code)."""
125
- if not is_internal_client(_client_ip(handler)):
126
- return False, "internal only"
127
- expected = _loaded_proxy_token()
128
- if not expected:
129
- return True, ""
130
- got = _header_token(handler)
131
- if not got or not hmac.compare_digest(got, expected):
132
- return False, "proxy token required"
133
- return True, ""
134
-
135
-
136
- if __name__ == "__main__":
137
- reset_proxy_token_for_tests()
138
- os.environ[PROXY_TOKEN_ENV] = "test-proxy-token"
139
- assert proxy_token_configured()
140
-
141
- class _H:
142
- client_address = ("172.19.0.2", 1234)
143
- headers = {PROXY_TOKEN_HEADER: "test-proxy-token"}
144
-
145
- ok, err = authorize_internal_proxy(_H()) # type: ignore[arg-type]
146
- assert ok and not err
147
- _H.headers = {}
148
- ok, err = authorize_internal_proxy(_H()) # type: ignore[arg-type]
149
- assert not ok and err == "proxy token required"
150
- print("internal_proxy_auth ok")
1
+ """Shared auth for /internal/llm and /internal/action — network + optional proxy token."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hmac
6
+ import ipaddress
7
+ import os
8
+ import secrets
9
+ from http.server import BaseHTTPRequestHandler
10
+ from pathlib import Path
11
+
12
+ PROXY_TOKEN_ENV = "WORKFRAME_PROXY_TOKEN"
13
+ PROXY_TOKEN_HEADER = "X-Workframe-Proxy-Token"
14
+ PROFILE_HEADER = "X-Workframe-Profile"
15
+ PROXY_TOKEN_FILE = ".proxy_token"
16
+ SHARED_PROXY_TOKEN_PATH = Path("/run/workframe-proxy/token")
17
+
18
+ _TOKEN: str | None = None
19
+ _TOKEN_PATH: Path | None = None
20
+
21
+
22
+ def _data_dir() -> Path:
23
+ return Path(
24
+ os.environ.get("WORKFRAME_API_DATA_DIR")
25
+ or os.environ.get("MISSION_DATA_DIR")
26
+ or Path(__file__).resolve().parent / "data"
27
+ )
28
+
29
+
30
+ def _deployment_mode() -> str:
31
+ return (os.environ.get("WORKFRAME_DEPLOYMENT_MODE") or "trusted_team").strip().lower()
32
+
33
+
34
+ def proxy_token_configured() -> bool:
35
+ return bool(_loaded_proxy_token())
36
+
37
+
38
+ def _loaded_proxy_token() -> str:
39
+ global _TOKEN, _TOKEN_PATH
40
+ if _TOKEN is not None:
41
+ return _TOKEN
42
+ env = str(os.environ.get(PROXY_TOKEN_ENV) or "").strip()
43
+ if env:
44
+ _TOKEN = env
45
+ return _TOKEN
46
+ path = _data_dir() / PROXY_TOKEN_FILE
47
+ _TOKEN_PATH = path
48
+ if path.is_file():
49
+ _TOKEN = path.read_text(encoding="utf-8").strip()
50
+ return _TOKEN
51
+ _TOKEN = ""
52
+ return ""
53
+
54
+
55
+ def _sync_shared_proxy_token(token: str) -> None:
56
+ value = str(token or "").strip()
57
+ if not value:
58
+ return
59
+ try:
60
+ SHARED_PROXY_TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)
61
+ SHARED_PROXY_TOKEN_PATH.write_text(value + "\n", encoding="utf-8")
62
+ except OSError:
63
+ pass
64
+
65
+
66
+ def bootstrap_proxy_token(*, allow_generate_file: bool = True) -> str:
67
+ """Load proxy token from env or data dir; optionally create file for local dogfood."""
68
+ global _TOKEN
69
+ existing = str(os.environ.get(PROXY_TOKEN_ENV) or "").strip()
70
+ if existing:
71
+ _TOKEN = existing
72
+ _sync_shared_proxy_token(existing)
73
+ return existing
74
+ path = _data_dir() / PROXY_TOKEN_FILE
75
+ if path.is_file():
76
+ _TOKEN = path.read_text(encoding="utf-8").strip()
77
+ _sync_shared_proxy_token(_TOKEN)
78
+ return _TOKEN
79
+ if not allow_generate_file or _deployment_mode() == "public_multi_user":
80
+ _TOKEN = ""
81
+ return ""
82
+ token = secrets.token_urlsafe(32)
83
+ path.parent.mkdir(parents=True, exist_ok=True)
84
+ path.write_text(token + "\n", encoding="utf-8")
85
+ # ponytail: gateway reads same token from shared compose volume when env unset
86
+ try:
87
+ SHARED_PROXY_TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)
88
+ SHARED_PROXY_TOKEN_PATH.write_text(token + "\n", encoding="utf-8")
89
+ except OSError:
90
+ pass
91
+ _TOKEN = token
92
+ return token
93
+
94
+
95
+ def reset_proxy_token_for_tests() -> None:
96
+ global _TOKEN, _TOKEN_PATH
97
+ _TOKEN = None
98
+ _TOKEN_PATH = None
99
+
100
+
101
+ def _client_ip(handler: BaseHTTPRequestHandler) -> str:
102
+ return str(handler.client_address[0] if handler.client_address else "").strip()
103
+
104
+
105
+ def is_internal_client(host: str) -> bool:
106
+ """Private/docker callers only — not routable public addresses."""
107
+ addr = str(host or "").strip()
108
+ if not addr:
109
+ return False
110
+ if addr in {"127.0.0.1", "::1", "localhost"}:
111
+ return True
112
+ try:
113
+ ip = ipaddress.ip_address(addr.split("%", 1)[0])
114
+ except ValueError:
115
+ return False
116
+ return ip.is_private or ip.is_loopback
117
+
118
+
119
+ def _header_token(handler: BaseHTTPRequestHandler) -> str:
120
+ return str(handler.headers.get(PROXY_TOKEN_HEADER) or "").strip()
121
+
122
+
123
+ def authorize_internal_proxy(handler: BaseHTTPRequestHandler) -> tuple[bool, str]:
124
+ """Return (ok, error_code)."""
125
+ if not is_internal_client(_client_ip(handler)):
126
+ return False, "internal only"
127
+ expected = _loaded_proxy_token()
128
+ if not expected:
129
+ return True, ""
130
+ got = _header_token(handler)
131
+ if not got or not hmac.compare_digest(got, expected):
132
+ return False, "proxy token required"
133
+ return True, ""
134
+
135
+
136
+ if __name__ == "__main__":
137
+ reset_proxy_token_for_tests()
138
+ os.environ[PROXY_TOKEN_ENV] = "test-proxy-token"
139
+ assert proxy_token_configured()
140
+
141
+ class _H:
142
+ client_address = ("172.19.0.2", 1234)
143
+ headers = {PROXY_TOKEN_HEADER: "test-proxy-token"}
144
+
145
+ ok, err = authorize_internal_proxy(_H()) # type: ignore[arg-type]
146
+ assert ok and not err
147
+ _H.headers = {}
148
+ ok, err = authorize_internal_proxy(_H()) # type: ignore[arg-type]
149
+ assert not ok and err == "proxy token required"
150
+ print("internal_proxy_auth ok")