create-workframe 0.1.0 → 0.1.2

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