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,131 +1,131 @@
1
- """Internal action proxy — dev/tool PATs stay in vault; Hermes sends lease tokens."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import re
7
- import urllib.error
8
- import urllib.request
9
- from http.server import BaseHTTPRequestHandler
10
- from typing import Any, Callable
11
-
12
- import llm_proxy
13
- import turn_credentials
14
-
15
- UPSTREAM_BASE: dict[str, str] = {
16
- "github": "https://api.github.com",
17
- "vercel": "https://api.vercel.com",
18
- "netlify": "https://api.netlify.com/api/v1",
19
- }
20
-
21
- PROXY_PATH_RE = re.compile(r"^/internal/action/([a-z0-9_-]+)(/.*)?$", re.IGNORECASE)
22
-
23
-
24
- def upstream_auth_header(provider: str, secret: str) -> dict[str, str]:
25
- provider = str(provider or "").strip().lower()
26
- secret = str(secret or "").strip()
27
- if provider == "github":
28
- return {"Authorization": f"Bearer {secret}", "Accept": "application/vnd.github+json"}
29
- if provider == "netlify":
30
- return {"Authorization": f"Bearer {secret}"}
31
- return {"Authorization": f"Bearer {secret}"}
32
-
33
-
34
- def forward_request(
35
- provider: str,
36
- subpath: str,
37
- method: str,
38
- headers: dict[str, str],
39
- body: bytes | None,
40
- *,
41
- resolve_secret: Callable[[str, str, str, str], tuple[str, str]],
42
- ) -> tuple[int, dict[str, str], bytes]:
43
- provider = str(provider or "").strip().lower()
44
- base = UPSTREAM_BASE.get(provider)
45
- if not base:
46
- return 404, {"Content-Type": "application/json"}, json.dumps({"error": "unknown provider"}).encode()
47
-
48
- token = llm_proxy.extract_bearer(headers)
49
- lease = turn_credentials.validate_lease(token)
50
- if not lease:
51
- return 401, {"Content-Type": "application/json"}, json.dumps({"error": "invalid lease"}).encode()
52
- if str(lease.get("provider") or "").lower() != provider:
53
- return 403, {"Content-Type": "application/json"}, json.dumps({"error": "provider mismatch"}).encode()
54
-
55
- ok_profile, profile_err, profile_status = llm_proxy.validate_lease_profile(lease, headers)
56
- if not ok_profile:
57
- return (
58
- profile_status,
59
- {"Content-Type": "application/json"},
60
- json.dumps({"error": profile_err}).encode(),
61
- )
62
-
63
- _env_var, secret = turn_credentials.resolve_lease_secret(lease, resolve_secret)
64
- if not secret:
65
- return 402, {"Content-Type": "application/json"}, json.dumps({"error": "no credential"}).encode()
66
-
67
- path = subpath if subpath.startswith("/") else f"/{subpath}"
68
- url = f"{base.rstrip('/')}{path}"
69
- upstream_headers = {
70
- k: v
71
- for k, v in headers.items()
72
- if k.lower()
73
- not in {
74
- "host",
75
- "connection",
76
- "content-length",
77
- "authorization",
78
- "x-api-key",
79
- }
80
- }
81
- upstream_headers.update(upstream_auth_header(provider, secret))
82
-
83
- req = urllib.request.Request(url, data=body, headers=upstream_headers, method=method.upper())
84
- try:
85
- with urllib.request.urlopen(req, timeout=600) as resp:
86
- resp_body = resp.read()
87
- out_headers = {"Content-Type": resp.headers.get("Content-Type", "application/octet-stream")}
88
- return resp.status, out_headers, resp_body
89
- except urllib.error.HTTPError as exc:
90
- raw = exc.read()
91
- out_headers = {"Content-Type": exc.headers.get("Content-Type", "application/json")}
92
- return exc.code, out_headers, raw
93
-
94
-
95
- def handle_proxy_request(
96
- handler: BaseHTTPRequestHandler,
97
- path: str,
98
- method: str,
99
- body: bytes | None,
100
- *,
101
- resolve_secret: Callable[[str, str, str, str], tuple[str, str]],
102
- ) -> bool:
103
- ok, err = llm_proxy.authorize_internal_proxy(handler)
104
- if not ok:
105
- handler.send_response(403)
106
- handler.send_header("Content-Type", "application/json")
107
- handler.end_headers()
108
- handler.wfile.write(json.dumps({"error": err or "internal only"}).encode())
109
- return True
110
-
111
- match = PROXY_PATH_RE.match(path)
112
- if not match:
113
- return False
114
-
115
- provider = match.group(1).lower()
116
- subpath = match.group(2) or "/"
117
- headers = {k: v for k, v in handler.headers.items()}
118
- status, out_headers, resp_body = forward_request(
119
- provider,
120
- subpath,
121
- method,
122
- headers,
123
- body,
124
- resolve_secret=resolve_secret,
125
- )
126
- handler.send_response(status)
127
- for key, value in out_headers.items():
128
- handler.send_header(key, value)
129
- handler.end_headers()
130
- handler.wfile.write(resp_body)
131
- return True
1
+ """Internal action proxy — dev/tool PATs stay in vault; Hermes sends lease tokens."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import urllib.error
8
+ import urllib.request
9
+ from http.server import BaseHTTPRequestHandler
10
+ from typing import Any, Callable
11
+
12
+ import llm_proxy
13
+ import turn_credentials
14
+
15
+ UPSTREAM_BASE: dict[str, str] = {
16
+ "github": "https://api.github.com",
17
+ "vercel": "https://api.vercel.com",
18
+ "netlify": "https://api.netlify.com/api/v1",
19
+ }
20
+
21
+ PROXY_PATH_RE = re.compile(r"^/internal/action/([a-z0-9_-]+)(/.*)?$", re.IGNORECASE)
22
+
23
+
24
+ def upstream_auth_header(provider: str, secret: str) -> dict[str, str]:
25
+ provider = str(provider or "").strip().lower()
26
+ secret = str(secret or "").strip()
27
+ if provider == "github":
28
+ return {"Authorization": f"Bearer {secret}", "Accept": "application/vnd.github+json"}
29
+ if provider == "netlify":
30
+ return {"Authorization": f"Bearer {secret}"}
31
+ return {"Authorization": f"Bearer {secret}"}
32
+
33
+
34
+ def forward_request(
35
+ provider: str,
36
+ subpath: str,
37
+ method: str,
38
+ headers: dict[str, str],
39
+ body: bytes | None,
40
+ *,
41
+ resolve_secret: Callable[[str, str, str, str], tuple[str, str]],
42
+ ) -> tuple[int, dict[str, str], bytes]:
43
+ provider = str(provider or "").strip().lower()
44
+ base = UPSTREAM_BASE.get(provider)
45
+ if not base:
46
+ return 404, {"Content-Type": "application/json"}, json.dumps({"error": "unknown provider"}).encode()
47
+
48
+ token = llm_proxy.extract_bearer(headers)
49
+ lease = turn_credentials.validate_lease(token)
50
+ if not lease:
51
+ return 401, {"Content-Type": "application/json"}, json.dumps({"error": "invalid lease"}).encode()
52
+ if str(lease.get("provider") or "").lower() != provider:
53
+ return 403, {"Content-Type": "application/json"}, json.dumps({"error": "provider mismatch"}).encode()
54
+
55
+ ok_profile, profile_err, profile_status = llm_proxy.validate_lease_profile(lease, headers)
56
+ if not ok_profile:
57
+ return (
58
+ profile_status,
59
+ {"Content-Type": "application/json"},
60
+ json.dumps({"error": profile_err}).encode(),
61
+ )
62
+
63
+ _env_var, secret = turn_credentials.resolve_lease_secret(lease, resolve_secret)
64
+ if not secret:
65
+ return 402, {"Content-Type": "application/json"}, json.dumps({"error": "no credential"}).encode()
66
+
67
+ path = subpath if subpath.startswith("/") else f"/{subpath}"
68
+ url = f"{base.rstrip('/')}{path}"
69
+ upstream_headers = {
70
+ k: v
71
+ for k, v in headers.items()
72
+ if k.lower()
73
+ not in {
74
+ "host",
75
+ "connection",
76
+ "content-length",
77
+ "authorization",
78
+ "x-api-key",
79
+ }
80
+ }
81
+ upstream_headers.update(upstream_auth_header(provider, secret))
82
+
83
+ req = urllib.request.Request(url, data=body, headers=upstream_headers, method=method.upper())
84
+ try:
85
+ with urllib.request.urlopen(req, timeout=600) as resp:
86
+ resp_body = resp.read()
87
+ out_headers = {"Content-Type": resp.headers.get("Content-Type", "application/octet-stream")}
88
+ return resp.status, out_headers, resp_body
89
+ except urllib.error.HTTPError as exc:
90
+ raw = exc.read()
91
+ out_headers = {"Content-Type": exc.headers.get("Content-Type", "application/json")}
92
+ return exc.code, out_headers, raw
93
+
94
+
95
+ def handle_proxy_request(
96
+ handler: BaseHTTPRequestHandler,
97
+ path: str,
98
+ method: str,
99
+ body: bytes | None,
100
+ *,
101
+ resolve_secret: Callable[[str, str, str, str], tuple[str, str]],
102
+ ) -> bool:
103
+ ok, err = llm_proxy.authorize_internal_proxy(handler)
104
+ if not ok:
105
+ handler.send_response(403)
106
+ handler.send_header("Content-Type", "application/json")
107
+ handler.end_headers()
108
+ handler.wfile.write(json.dumps({"error": err or "internal only"}).encode())
109
+ return True
110
+
111
+ match = PROXY_PATH_RE.match(path)
112
+ if not match:
113
+ return False
114
+
115
+ provider = match.group(1).lower()
116
+ subpath = match.group(2) or "/"
117
+ headers = {k: v for k, v in handler.headers.items()}
118
+ status, out_headers, resp_body = forward_request(
119
+ provider,
120
+ subpath,
121
+ method,
122
+ headers,
123
+ body,
124
+ resolve_secret=resolve_secret,
125
+ )
126
+ handler.send_response(status)
127
+ for key, value in out_headers.items():
128
+ handler.send_header(key, value)
129
+ handler.end_headers()
130
+ handler.wfile.write(resp_body)
131
+ return True
@@ -1,49 +1,49 @@
1
- """In-process auth endpoint throttle — trust boundary (0022 N5)."""
2
- from __future__ import annotations
3
-
4
- import time
5
- from collections import deque
6
-
7
- _BUCKETS: dict[str, deque[float]] = {}
8
- _MINUTE_WINDOW = 60.0
9
- _MINUTE_MAX = 5
10
- _HOUR_WINDOW = 3600.0
11
- _HOUR_MAX = 20
12
-
13
-
14
- def _bucket_key(kind: str, client_ip: str, email: str) -> str:
15
- return f"{kind}:{client_ip}:{email.strip().lower()}"
16
-
17
-
18
- def _prune(queue: deque[float], now: float, window: float) -> None:
19
- while queue and now - queue[0] > window:
20
- queue.popleft()
21
-
22
-
23
- def allow_auth_request(kind: str, client_ip: str, email: str) -> bool:
24
- now = time.time()
25
- key = _bucket_key(kind, client_ip, email)
26
- queue = _BUCKETS.setdefault(key, deque())
27
- _prune(queue, now, _HOUR_WINDOW)
28
- if len(queue) >= _HOUR_MAX:
29
- return False
30
- recent = deque(queue)
31
- _prune(recent, now, _MINUTE_WINDOW)
32
- if len(recent) >= _MINUTE_MAX:
33
- return False
34
- queue.append(now)
35
- return True
36
-
37
-
38
- def reset_for_tests() -> None:
39
- _BUCKETS.clear()
40
-
41
-
42
- if __name__ == "__main__":
43
- reset_for_tests()
44
- ip = "203.0.113.1"
45
- email = "a@example.com"
46
- for _ in range(_MINUTE_MAX):
47
- assert allow_auth_request("start", ip, email)
48
- assert not allow_auth_request("start", ip, email)
49
- print("auth_rate_limit ok")
1
+ """In-process auth endpoint throttle — trust boundary (0022 N5)."""
2
+ from __future__ import annotations
3
+
4
+ import time
5
+ from collections import deque
6
+
7
+ _BUCKETS: dict[str, deque[float]] = {}
8
+ _MINUTE_WINDOW = 60.0
9
+ _MINUTE_MAX = 5
10
+ _HOUR_WINDOW = 3600.0
11
+ _HOUR_MAX = 20
12
+
13
+
14
+ def _bucket_key(kind: str, client_ip: str, email: str) -> str:
15
+ return f"{kind}:{client_ip}:{email.strip().lower()}"
16
+
17
+
18
+ def _prune(queue: deque[float], now: float, window: float) -> None:
19
+ while queue and now - queue[0] > window:
20
+ queue.popleft()
21
+
22
+
23
+ def allow_auth_request(kind: str, client_ip: str, email: str) -> bool:
24
+ now = time.time()
25
+ key = _bucket_key(kind, client_ip, email)
26
+ queue = _BUCKETS.setdefault(key, deque())
27
+ _prune(queue, now, _HOUR_WINDOW)
28
+ if len(queue) >= _HOUR_MAX:
29
+ return False
30
+ recent = deque(queue)
31
+ _prune(recent, now, _MINUTE_WINDOW)
32
+ if len(recent) >= _MINUTE_MAX:
33
+ return False
34
+ queue.append(now)
35
+ return True
36
+
37
+
38
+ def reset_for_tests() -> None:
39
+ _BUCKETS.clear()
40
+
41
+
42
+ if __name__ == "__main__":
43
+ reset_for_tests()
44
+ ip = "203.0.113.1"
45
+ email = "a@example.com"
46
+ for _ in range(_MINUTE_MAX):
47
+ assert allow_auth_request("start", ip, email)
48
+ assert not allow_auth_request("start", ip, email)
49
+ print("auth_rate_limit ok")