create-workframe 0.1.12 → 0.1.13

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 (188) hide show
  1. package/README.md +1 -1
  2. package/bin/create-workframe.js +2 -2
  3. package/bin/workframe.js +130 -2
  4. package/docs/security.md +2 -2
  5. package/package.json +1 -1
  6. package/scripts/bundle-workframe-ui.mjs +9 -0
  7. package/scripts/sync-canonical-to-package.mjs +1 -0
  8. package/scripts/verify-public-deploy.sh +65 -3
  9. package/workframe-api/action_proxy.py +23 -17
  10. package/workframe-api/activity_feed.py +798 -0
  11. package/workframe-api/api_meta.py +157 -0
  12. package/workframe-api/auth_gate.py +566 -0
  13. package/workframe-api/avatar_registry.py +343 -0
  14. package/workframe-api/broker_audit.py +164 -0
  15. package/workframe-api/cell_authority.py +231 -0
  16. package/workframe-api/chat_bind.py +319 -0
  17. package/workframe-api/chat_sessions.py +509 -0
  18. package/workframe-api/chat_stream.py +694 -0
  19. package/workframe-api/cleanup_dogfood_smoke.py +252 -0
  20. package/workframe-api/credential_broker.py +162 -0
  21. package/workframe-api/credential_resolve.py +202 -0
  22. package/workframe-api/credential_store.py +245 -0
  23. package/workframe-api/crew_registry.py +250 -0
  24. package/workframe-api/db_schema.py +755 -0
  25. package/workframe-api/docker_gateway.py +166 -0
  26. package/workframe-api/doctor_runtime.py +109 -0
  27. package/workframe-api/domain/__init__.py +47 -0
  28. package/workframe-api/domain/entities.py +252 -0
  29. package/workframe-api/domain/schema/workframe-domain.schema.json +278 -0
  30. package/workframe-api/egress_policy.py +42 -0
  31. package/workframe-api/handler_modules/__init__.py +17 -0
  32. package/workframe-api/handler_modules/handler_admin.py +542 -0
  33. package/workframe-api/handler_modules/handler_auth.py +512 -0
  34. package/workframe-api/handler_modules/handler_chat.py +641 -0
  35. package/workframe-api/handler_modules/handler_install.py +178 -0
  36. package/workframe-api/handler_modules/handler_provider.py +325 -0
  37. package/workframe-api/handler_modules/handler_workspace.py +1420 -0
  38. package/workframe-api/health_monitor.py +80 -0
  39. package/workframe-api/hermes_admin.py +756 -0
  40. package/workframe-api/hermes_profiles.py +1328 -0
  41. package/workframe-api/install_api.py +123 -0
  42. package/workframe-api/kanban_cron.py +473 -0
  43. package/workframe-api/lane_bindings.py +423 -0
  44. package/workframe-api/llm_proxy.py +17 -43
  45. package/workframe-api/mention_helpers.py +180 -0
  46. package/workframe-api/mention_invoke.py +431 -0
  47. package/workframe-api/model_surface.py +1139 -0
  48. package/workframe-api/oauth_pending.py +85 -0
  49. package/workframe-api/oauth_redirect.py +381 -0
  50. package/workframe-api/package.json +2 -2
  51. package/workframe-api/profile_api_lifecycle.py +66 -0
  52. package/workframe-api/profile_gateway.py +436 -0
  53. package/workframe-api/provider_bindings.py +673 -0
  54. package/workframe-api/provider_bootstrap.py +347 -0
  55. package/workframe-api/provider_catalog.py +180 -0
  56. package/workframe-api/rooms.py +1613 -0
  57. package/workframe-api/route_registry.py +331 -191
  58. package/workframe-api/run-typecheck.mjs +2 -1
  59. package/workframe-api/run_authority.py +287 -0
  60. package/workframe-api/run_ledger.py +470 -0
  61. package/workframe-api/run_surface_wiring.py +344 -0
  62. package/workframe-api/runtime_cohort.py +752 -0
  63. package/workframe-api/runtime_tokens.py +127 -0
  64. package/workframe-api/server.py +1453 -19513
  65. package/workframe-api/snapshot_feed.py +49 -0
  66. package/workframe-api/stack_config.py +33 -1
  67. package/workframe-api/supervisor_client.py +154 -0
  68. package/workframe-api/test_api_meta_build_stamp.py +34 -0
  69. package/workframe-api/test_cell_authority.py +79 -0
  70. package/workframe-api/test_chat_bind.py +48 -0
  71. package/workframe-api/test_credential_lease_matrix.py +171 -0
  72. package/workframe-api/test_credential_resolve.py +51 -0
  73. package/workframe-api/test_credential_store.py +27 -0
  74. package/workframe-api/test_dogfood_flows.py +346 -0
  75. package/workframe-api/test_domain_entities.py +152 -0
  76. package/workframe-api/test_exception_hygiene.py +12 -0
  77. package/workframe-api/test_hermes_admin.py +72 -0
  78. package/workframe-api/test_install_wizard_resume.py +78 -0
  79. package/workframe-api/test_local_bootstrap.py +67 -0
  80. package/workframe-api/test_mention_helpers.py +35 -0
  81. package/workframe-api/test_mention_invoke.py +26 -0
  82. package/workframe-api/test_model_surface_consistency.py +1 -0
  83. package/workframe-api/test_oauth_pending.py +41 -0
  84. package/workframe-api/test_provider_bindings.py +52 -0
  85. package/workframe-api/test_provider_catalog.py +28 -0
  86. package/workframe-api/test_route_registry.py +171 -35
  87. package/workframe-api/test_run_authority.py +112 -0
  88. package/workframe-api/test_run_ledger.py +157 -0
  89. package/workframe-api/test_run_surface_wiring.py +130 -0
  90. package/workframe-api/test_runtime_cohort.py +85 -0
  91. package/workframe-api/test_secure_mode_docker_boundary.py +39 -0
  92. package/workframe-api/test_server_reexports.py +99 -0
  93. package/workframe-api/test_stack_config_install_smtp.py +7 -0
  94. package/workframe-api/turn_credentials.py +28 -13
  95. package/workframe-api/turn_overlay.py +715 -0
  96. package/workframe-api/updates.py +16 -1
  97. package/workframe-api/user_prefs.py +387 -0
  98. package/workframe-api/wf032_extract_remaining_handlers.py +417 -0
  99. package/workframe-api/workspace_bootstrap.py +536 -0
  100. package/workframe-api/workspace_files.py +344 -0
  101. package/workframe-api/workspace_messaging.py +206 -0
  102. package/workframe-api/zk_auth.py +3 -2
  103. package/workframe-supervisor/server.py +10 -1
  104. package/workframe-supervisor/test_supervisor_negative.py +75 -0
  105. package/workframe-ui/public/assets/{arc-B0OFRGmJ.js → arc-aUUffclm.js} +1 -1
  106. package/workframe-ui/public/assets/architecture-7EHR7CIX-BSUP4S8J.js +1 -0
  107. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-DeBNltHS.js → architectureDiagram-3BPJPVTR-DU6oaqIb.js} +1 -1
  108. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-BDhCHu7I.js → blockDiagram-GPEHLZMM-D2p8BU6-.js} +1 -1
  109. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-olcDYvtI.js → c4Diagram-AAUBKEIU-ouwyPmTJ.js} +1 -1
  110. package/workframe-ui/public/assets/channel-DOunZZ0X.js +1 -0
  111. package/workframe-ui/public/assets/{chunk-2J33WTMH-D0obCBn_.js → chunk-2J33WTMH-DsE7U5dj.js} +1 -1
  112. package/workframe-ui/public/assets/{chunk-3OPIFGDE-DX93f-ZZ.js → chunk-3OPIFGDE-DM0kkZ9h.js} +1 -1
  113. package/workframe-ui/public/assets/{chunk-4BX2VUAB-BX9B5wUs.js → chunk-4BX2VUAB-CQ8mAyXp.js} +1 -1
  114. package/workframe-ui/public/assets/{chunk-55IACEB6-C4DGc6RO.js → chunk-55IACEB6-DN1BDtbH.js} +1 -1
  115. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-Crlja9Lf.js → chunk-5ZQYHXKU-BzK2KqOt.js} +1 -1
  116. package/workframe-ui/public/assets/{chunk-727SXJPM-JYSm3szI.js → chunk-727SXJPM-C3AxtOi9.js} +1 -1
  117. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-_KslxVEl.js → chunk-AQP2D5EJ-4mVwEFuD.js} +1 -1
  118. package/workframe-ui/public/assets/{chunk-BSJP7CBP-CpTO-jU8.js → chunk-BSJP7CBP-23kN2q0A.js} +1 -1
  119. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-JGUkCmbI.js → chunk-CSCIHK7Q-CPBKVW-L.js} +2 -2
  120. package/workframe-ui/public/assets/{chunk-FMBD7UC4-BscBwGaC.js → chunk-FMBD7UC4-CKqcYq7K.js} +1 -1
  121. package/workframe-ui/public/assets/{chunk-KSCS5N6A-5ZTZ0bpX.js → chunk-KSCS5N6A-JRQnhxQE.js} +1 -1
  122. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-DxvitYbW.js → chunk-L5ZTLDWV-BLZrdIwa.js} +1 -1
  123. package/workframe-ui/public/assets/chunk-LZXEDZCA-DisG0XJT.js +2 -0
  124. package/workframe-ui/public/assets/{chunk-ND2GUHAM-Cayl0iV9.js → chunk-ND2GUHAM-ClAOxArS.js} +1 -1
  125. package/workframe-ui/public/assets/{chunk-NZK2D7GU-_7dyBR5G.js → chunk-NZK2D7GU-XUE1aNkm.js} +1 -1
  126. package/workframe-ui/public/assets/{chunk-O5CBEL6O--xTpD0yi.js → chunk-O5CBEL6O-DDN-Ifon.js} +1 -1
  127. package/workframe-ui/public/assets/chunk-QZHKN3VN-EVQ0VMd5.js +1 -0
  128. package/workframe-ui/public/assets/chunk-WU5MYG2G-k_-ZsLOS.js +1 -0
  129. package/workframe-ui/public/assets/{chunk-XPW4576I-CwxJQaeK.js → chunk-XPW4576I-B4_iYZ6r.js} +1 -1
  130. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-W9WVQsby.js +1 -0
  131. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-W9WVQsby.js +1 -0
  132. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-CCspENYv.js → cose-bilkent-S5V4N54A-Cy1HhS7j.js} +1 -1
  133. package/workframe-ui/public/assets/{dagre-BM42HDAG-Dv8V6R60.js → dagre-BM42HDAG-Bjal81Ie.js} +1 -1
  134. package/workframe-ui/public/assets/{diagram-2AECGRRQ-Da48hFPT.js → diagram-2AECGRRQ-v5Gdvzp_.js} +1 -1
  135. package/workframe-ui/public/assets/{diagram-5GNKFQAL-dTihc7-3.js → diagram-5GNKFQAL-DuyRdfmY.js} +1 -1
  136. package/workframe-ui/public/assets/{diagram-KO2AKTUF-D_Jqu699.js → diagram-KO2AKTUF-WwWCGbIW.js} +1 -1
  137. package/workframe-ui/public/assets/{diagram-LMA3HP47-Bt8PtEaZ.js → diagram-LMA3HP47-DTYwZALT.js} +1 -1
  138. package/workframe-ui/public/assets/{diagram-OG6HWLK6-BtmPjlh0.js → diagram-OG6HWLK6-B46kBIqE.js} +1 -1
  139. package/workframe-ui/public/assets/{dist-Cz2turIT.js → dist-mQWmB3z6.js} +1 -1
  140. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-BxMGCflX.js → erDiagram-TEJ5UH35-__40xO-y.js} +1 -1
  141. package/workframe-ui/public/assets/eventmodeling-FCH6USID-Cm1uRTxU.js +1 -0
  142. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-ivyroIZt.js → flowDiagram-I6XJVG4X-Cwj6dVUz.js} +1 -1
  143. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-B9llwShx.js → ganttDiagram-6RSMTGT7-D_IOSwAS.js} +1 -1
  144. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BFQHk4bw.js → gitGraph-WXDBUCRP-C1vGecee.js} +1 -1
  145. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-ursegLbc.js → gitGraphDiagram-PVQCEYII-DMTxFJxn.js} +1 -1
  146. package/workframe-ui/public/assets/index-BdiM4-lI.js +130 -0
  147. package/workframe-ui/public/assets/index-Fnad47vV.css +1 -0
  148. package/workframe-ui/public/assets/{info-J43DQDTF-DQpifAsB.js → info-J43DQDTF-CwjmyPmD.js} +1 -1
  149. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-C16XP2Q7.js → infoDiagram-5YYISTIA-BGzh7ZL2.js} +1 -1
  150. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-CiJoz7rP.js → ishikawaDiagram-YF4QCWOH-BLXdnR_5.js} +1 -1
  151. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CVwEvvUL.js → journeyDiagram-JHISSGLW-BK3behMe.js} +1 -1
  152. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-CWQ6hX5i.js → kanban-definition-UN3LZRKU-Bx172cCl.js} +1 -1
  153. package/workframe-ui/public/assets/{line-CgQsmU35.js → line-BTCEHb7l.js} +1 -1
  154. package/workframe-ui/public/assets/{linear-Bxbc77dL.js → linear-CkbydpnK.js} +1 -1
  155. package/workframe-ui/public/assets/{mermaid-parser.core-C_jMeF0a.js → mermaid-parser.core-DvrtArpk.js} +2 -2
  156. package/workframe-ui/public/assets/{mermaid.core-4RKV9MZP.js → mermaid.core-yj_1x_qj.js} +3 -3
  157. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-DZ7y7Sz0.js → mindmap-definition-RKZ34NQL-BXbMltQ0.js} +1 -1
  158. package/workframe-ui/public/assets/{packet-YPE3B663-B9h2I7Vq.js → packet-YPE3B663-9pVCBQ7S.js} +1 -1
  159. package/workframe-ui/public/assets/{pie-LRSECV5Y-B2mP5uHm.js → pie-LRSECV5Y-CdqoWesP.js} +1 -1
  160. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-DyYKyKiP.js → pieDiagram-4H26LBE5-kGs-VfXd.js} +1 -1
  161. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-CL_6nej-.js → quadrantDiagram-W4KKPZXB-D9D8gQw5.js} +1 -1
  162. package/workframe-ui/public/assets/{radar-GUYGQ44K-CNKNDQ7S.js → radar-GUYGQ44K-d4zMZpQS.js} +1 -1
  163. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-eWU_mhFa.js → requirementDiagram-4Y6WPE33-BI7EQ9zc.js} +1 -1
  164. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-DrjcXvEQ.js → sankeyDiagram-5OEKKPKP-Bcz19jQB.js} +1 -1
  165. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-BHWR1xTJ.js → sequenceDiagram-3UESZ5HK-iQiCEfYp.js} +1 -1
  166. package/workframe-ui/public/assets/{src-DfJB8kV1.js → src-D6mvlP96.js} +1 -1
  167. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BGJbSd3I.js → stateDiagram-AJRCARHV-CxaTXs02.js} +1 -1
  168. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CXikj9xi.js +1 -0
  169. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-DkwoCjiZ.js → timeline-definition-PNZ67QCA-Bej-EAnf.js} +1 -1
  170. package/workframe-ui/public/assets/{treeView-BLDUP644-GxfhiqoW.js → treeView-BLDUP644-C7PnnzwX.js} +1 -1
  171. package/workframe-ui/public/assets/{treemap-LRROVOQU-DxrOsars.js → treemap-LRROVOQU-CqISLmkO.js} +1 -1
  172. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-BsWhpUfr.js → vennDiagram-CIIHVFJN-eJMTTEW-.js} +1 -1
  173. package/workframe-ui/public/assets/{wardley-L42UT6IY-CrFZWMHS.js → wardley-L42UT6IY-D4sMroBF.js} +1 -1
  174. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DV8Xpf5I.js → wardleyDiagram-YWT4CUSO-DC3DCUs7.js} +1 -1
  175. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-DqMqnwdZ.js → xychartDiagram-2RQKCTM6-BeMaM6Aq.js} +1 -1
  176. package/workframe-ui/public/index.html +7 -5
  177. package/workframe-ui/public/workframe-build.json +5 -0
  178. package/workframe-ui/public/assets/architecture-7EHR7CIX-Dwwi9yA0.js +0 -1
  179. package/workframe-ui/public/assets/channel-fNHbDpV4.js +0 -1
  180. package/workframe-ui/public/assets/chunk-LZXEDZCA-DMfdMUwz.js +0 -2
  181. package/workframe-ui/public/assets/chunk-QZHKN3VN-ewTgEQK8.js +0 -1
  182. package/workframe-ui/public/assets/chunk-WU5MYG2G-D4Tg-A0l.js +0 -1
  183. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CFAgBpEl.js +0 -1
  184. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CFAgBpEl.js +0 -1
  185. package/workframe-ui/public/assets/eventmodeling-FCH6USID-BIJTP5k-.js +0 -1
  186. package/workframe-ui/public/assets/index-8CuZDEIG.css +0 -1
  187. package/workframe-ui/public/assets/index-xS9lFekI.js +0 -129
  188. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CMLuNyeC.js +0 -1
@@ -1,9 +1,11 @@
1
1
  """Declarative API route registry (WF-037).
2
2
 
3
3
  Deny-by-default auth tiers plus batched handler dispatch; handler bodies live on server.Handler.
4
+ Unknown /api/* paths pass auth and return 404 at dispatch; registered paths without auth → 401.
4
5
  """
5
6
  from __future__ import annotations
6
7
 
8
+ import re
7
9
  from dataclasses import dataclass
8
10
  from enum import Enum
9
11
  from typing import Any
@@ -11,178 +13,276 @@ from typing import Any
11
13
 
12
14
  class AuthLevel(str, Enum):
13
15
  PUBLIC = "public"
14
- SINGLE_USER_GET = "single_user_get"
15
- AUTH_FLOW = "auth_flow"
16
- INSTALL = "install"
17
16
  SESSION = "session"
17
+ ROLE_OWNER_ADMIN = "role_owner_admin"
18
+ INTERNAL = "internal"
18
19
 
19
20
 
20
- GET_ALWAYS_PUBLIC_ROUTES = frozenset({
21
- "/api/meta",
22
- "/api/health",
23
- "/api/setup/status",
24
- "/api/install/status",
25
- "/api/public/site-meta",
26
- "/api/public/link-preview",
27
- "/api/public/manifest.webmanifest",
28
- "/api/auth/google/callback",
29
- })
30
-
31
- GET_SINGLE_USER_PUBLIC_ROUTES = frozenset({
32
- "/api/files/read",
33
- "/api/files/raw",
34
- "/api/files/tree",
35
- "/api/files/list",
36
- "/api/files/state",
37
- "/api/board",
38
- "/api/content",
39
- "/api/content/get",
40
- "/api/agents",
41
- "/api/snapshot",
42
- "/api/hermes/usage",
43
- "/api/hermes/profile",
44
- "/api/hermes/debug",
45
- "/api/hermes/insights",
46
- "/api/hermes/gquota",
47
- "/api/hermes/models",
48
- "/api/hermes/skills",
49
- "/api/hermes/commands",
50
- "/api/routes",
51
- "/api/chat/session",
52
- "/api/chat/resolve",
53
- "/api/chat/messages",
54
- "/api/profiles",
55
- })
56
-
57
- AUTH_FLOW_POST_ROUTES = frozenset({
58
- "/api/auth/start",
59
- "/api/auth/verify",
60
- "/api/auth/logout",
61
- "/api/auth/refresh",
62
- "/api/auth/bootstrap",
63
- "/api/setup",
64
- "/api/auth/google/start",
65
- "/api/auth/local-bootstrap",
66
- })
67
-
68
21
  @dataclass(frozen=True, slots=True)
69
22
  class Route:
70
23
  method: str
71
24
  path: str
72
- handler: str
73
25
  auth: AuthLevel
26
+ handler: str | None = None
27
+ sessionless_ok: bool = False
28
+ install_window: bool = False
29
+
74
30
 
31
+ @dataclass(frozen=True, slots=True)
32
+ class RoutePattern:
33
+ method: str
34
+ pattern: re.Pattern[str]
35
+ label: str
36
+ auth: AuthLevel
37
+ sessionless_ok: bool = False
38
+ install_window: bool = False
39
+ handler: str | None = None
75
40
 
76
- # Batch: data-read GET surfaces (handler methods on server.Handler).
41
+
42
+ # Exact routes — handler set when dispatched via registry; auth always required.
77
43
  ROUTES: tuple[Route, ...] = (
78
- Route("GET", "/api/meta", "_route_get_meta", AuthLevel.PUBLIC),
79
- Route("GET", "/api/health", "_route_get_health", AuthLevel.PUBLIC),
80
- Route("GET", "/api/setup/status", "_route_get_setup_status", AuthLevel.PUBLIC),
81
- Route("GET", "/api/install/status", "_route_get_install_status", AuthLevel.PUBLIC),
82
- Route("GET", "/api/public/site-meta", "_route_get_public_site_meta", AuthLevel.PUBLIC),
83
- Route("GET", "/api/public/link-preview", "_route_get_public_link_preview", AuthLevel.PUBLIC),
84
- Route("GET", "/api/public/manifest.webmanifest", "_route_get_public_manifest", AuthLevel.PUBLIC),
85
- Route("GET", "/api/agents", "_route_get_agents", AuthLevel.SINGLE_USER_GET),
86
- Route("GET", "/api/snapshot", "_route_get_snapshot", AuthLevel.SINGLE_USER_GET),
87
- Route("GET", "/api/activity/detail", "_route_get_activity_detail", AuthLevel.SESSION),
88
- Route("GET", "/api/board", "_route_get_board", AuthLevel.SINGLE_USER_GET),
89
- Route("GET", "/api/content", "_route_get_content", AuthLevel.SINGLE_USER_GET),
90
- Route("GET", "/api/content/get", "_route_get_content_get", AuthLevel.SINGLE_USER_GET),
91
- Route("GET", "/api/files/tree", "_route_get_files_tree", AuthLevel.SINGLE_USER_GET),
92
- Route("GET", "/api/files/list", "_route_get_files_list", AuthLevel.SINGLE_USER_GET),
93
- Route("GET", "/api/files/state", "_route_get_files_state", AuthLevel.SINGLE_USER_GET),
94
- Route("GET", "/api/files/read", "_route_get_files_read", AuthLevel.SINGLE_USER_GET),
95
- Route("GET", "/api/files/raw", "_route_get_files_raw", AuthLevel.SINGLE_USER_GET),
96
- Route("GET", "/api/routes", "_route_get_routes", AuthLevel.SINGLE_USER_GET),
97
- Route("GET", "/api/chat/messages", "_route_get_chat_messages", AuthLevel.SINGLE_USER_GET),
98
- Route("GET", "/api/chat/session", "_route_get_chat_session", AuthLevel.SINGLE_USER_GET),
99
- Route("GET", "/api/chat/resolve", "_route_get_chat_resolve", AuthLevel.SINGLE_USER_GET),
100
- Route("GET", "/api/hermes/skills", "_route_get_hermes_skills", AuthLevel.SINGLE_USER_GET),
101
- Route("GET", "/api/hermes/commands", "_route_get_hermes_commands", AuthLevel.SINGLE_USER_GET),
102
- Route("GET", "/api/hermes/usage", "_route_get_hermes_usage", AuthLevel.SINGLE_USER_GET),
103
- Route("GET", "/api/hermes/profile", "_route_get_hermes_profile", AuthLevel.SINGLE_USER_GET),
104
- # Batch 3: session /me + hermes catalog GET surfaces
105
- Route("GET", "/api/me", "_route_get_me", AuthLevel.SESSION),
106
- Route("GET", "/api/me/onboarding", "_route_get_me_onboarding", AuthLevel.SESSION),
107
- Route("GET", "/api/me/credentials", "_route_get_me_credentials", AuthLevel.SESSION),
108
- Route("GET", "/api/me/providers", "_route_get_me_providers", AuthLevel.SESSION),
109
- Route("GET", "/api/hermes/models", "_route_get_hermes_models", AuthLevel.SINGLE_USER_GET),
110
- Route("GET", "/api/hermes/debug", "_route_get_hermes_debug", AuthLevel.SINGLE_USER_GET),
111
- Route("GET", "/api/hermes/insights", "_route_get_hermes_insights", AuthLevel.SINGLE_USER_GET),
112
- Route("GET", "/api/hermes/gquota", "_route_get_hermes_gquota", AuthLevel.SINGLE_USER_GET),
113
- # Batch 4: session cohort + legacy user credentials
114
- Route("GET", "/api/me/cohort", "_route_get_me_cohort", AuthLevel.SESSION),
115
- Route("GET", "/api/user/credentials", "_route_get_user_credentials", AuthLevel.SESSION),
116
- # Batch 2: auth-flow + hermes/chat + me credentials POST surfaces
117
- Route("POST", "/api/auth/start", "_route_post_auth_start", AuthLevel.AUTH_FLOW),
118
- Route("POST", "/api/auth/verify", "_route_post_auth_verify", AuthLevel.AUTH_FLOW),
119
- Route("POST", "/api/auth/logout", "_route_post_auth_logout", AuthLevel.AUTH_FLOW),
120
- Route("POST", "/api/auth/refresh", "_route_post_auth_refresh", AuthLevel.AUTH_FLOW),
121
- Route("POST", "/api/auth/bootstrap", "_route_post_auth_bootstrap", AuthLevel.AUTH_FLOW),
122
- Route("POST", "/api/auth/google/start", "_route_post_auth_google_start", AuthLevel.AUTH_FLOW),
123
- Route("POST", "/api/auth/local-bootstrap", "_route_post_auth_local_bootstrap", AuthLevel.AUTH_FLOW),
124
- Route("POST", "/api/setup", "_route_post_setup", AuthLevel.AUTH_FLOW),
125
- Route("POST", "/api/hermes/model", "_route_post_hermes_model", AuthLevel.SESSION),
126
- Route("POST", "/api/hermes/model/apply-default", "_route_post_hermes_model_apply_default", AuthLevel.SESSION),
127
- Route("POST", "/api/hermes/fallback-chain", "_route_post_hermes_fallback_chain", AuthLevel.SESSION),
128
- Route("POST", "/api/hermes/commands/exec", "_route_post_hermes_commands_exec", AuthLevel.SESSION),
129
- Route("POST", "/api/hermes/gateway/exec", "_route_post_hermes_gateway_exec", AuthLevel.SESSION),
130
- Route("POST", "/api/chat/stop", "_route_post_chat_stop", AuthLevel.SESSION),
131
- Route("POST", "/api/chat/steer", "_route_post_chat_steer", AuthLevel.SESSION),
132
- Route("POST", "/api/me/credentials", "_route_post_me_credentials", AuthLevel.SESSION),
133
- Route("POST", "/api/me/telegram/link", "_route_post_me_telegram_link", AuthLevel.SESSION),
134
- Route("POST", "/api/board", "_route_post_board", AuthLevel.SESSION),
135
- # Batch 5: workspace mutation POST surfaces
136
- Route("POST", "/api/board/update", "_route_post_board_update", AuthLevel.SESSION),
137
- Route("POST", "/api/board/delete", "_route_post_board_delete", AuthLevel.SESSION),
138
- Route("POST", "/api/content/save", "_route_post_content_save", AuthLevel.SESSION),
139
- Route("POST", "/api/content/delete", "_route_post_content_delete", AuthLevel.SESSION),
140
- Route("POST", "/api/files/write", "_route_post_files_write", AuthLevel.SESSION),
141
- Route("POST", "/api/files/upload", "_route_post_files_upload", AuthLevel.SESSION),
142
- Route("POST", "/api/chat/dispatch", "_route_post_chat_dispatch", AuthLevel.SESSION),
44
+ # --- public GET ---
45
+ Route("GET", "/api/meta", AuthLevel.PUBLIC, "_route_get_meta"),
46
+ Route("GET", "/api/health", AuthLevel.PUBLIC, "_route_get_health"),
47
+ Route("GET", "/api/setup/status", AuthLevel.PUBLIC, "_route_get_setup_status"),
48
+ Route("GET", "/api/install/status", AuthLevel.PUBLIC, "_route_get_install_status"),
49
+ Route("GET", "/api/public/site-meta", AuthLevel.PUBLIC, "_route_get_public_site_meta"),
50
+ Route("GET", "/api/public/link-preview", AuthLevel.PUBLIC, "_route_get_public_link_preview"),
51
+ Route("GET", "/api/public/manifest.webmanifest", AuthLevel.PUBLIC, "_route_get_public_manifest"),
52
+ Route("GET", "/api/auth/hermes-dashboard-gate", AuthLevel.PUBLIC, "_route_get_auth_hermes_dashboard_gate"),
53
+ Route("GET", "/api/auth/google/callback", AuthLevel.PUBLIC, "_route_get_auth_google_callback"),
54
+ # --- sessionless_ok GET (single_user_local / DEV_LOCAL_UNSAFE) ---
55
+ Route("GET", "/api/agents", AuthLevel.SESSION, "_route_get_agents", sessionless_ok=True),
56
+ Route("GET", "/api/snapshot", AuthLevel.SESSION, "_route_get_snapshot", sessionless_ok=True),
57
+ Route("GET", "/api/board", AuthLevel.SESSION, "_route_get_board", sessionless_ok=True),
58
+ Route("GET", "/api/content", AuthLevel.SESSION, "_route_get_content", sessionless_ok=True),
59
+ Route("GET", "/api/content/get", AuthLevel.SESSION, "_route_get_content_get", sessionless_ok=True),
60
+ Route("GET", "/api/files/tree", AuthLevel.SESSION, "_route_get_files_tree", sessionless_ok=True),
61
+ Route("GET", "/api/files/list", AuthLevel.SESSION, "_route_get_files_list", sessionless_ok=True),
62
+ Route("GET", "/api/files/state", AuthLevel.SESSION, "_route_get_files_state", sessionless_ok=True),
63
+ Route("GET", "/api/files/read", AuthLevel.SESSION, "_route_get_files_read", sessionless_ok=True),
64
+ Route("GET", "/api/files/raw", AuthLevel.SESSION, "_route_get_files_raw", sessionless_ok=True),
65
+ Route("GET", "/api/routes", AuthLevel.SESSION, "_route_get_routes", sessionless_ok=True),
66
+ Route("GET", "/api/chat/messages", AuthLevel.SESSION, "_route_get_chat_messages", sessionless_ok=True),
67
+ Route("GET", "/api/chat/session", AuthLevel.SESSION, "_route_get_chat_session", sessionless_ok=True),
68
+ Route("GET", "/api/chat/resolve", AuthLevel.SESSION, "_route_get_chat_resolve", sessionless_ok=True),
69
+ Route("GET", "/api/hermes/skills", AuthLevel.SESSION, "_route_get_hermes_skills", sessionless_ok=True),
70
+ Route("GET", "/api/hermes/commands", AuthLevel.SESSION, "_route_get_hermes_commands", sessionless_ok=True),
71
+ Route("GET", "/api/hermes/usage", AuthLevel.SESSION, "_route_get_hermes_usage", sessionless_ok=True),
72
+ Route("GET", "/api/hermes/profile", AuthLevel.SESSION, "_route_get_hermes_profile", sessionless_ok=True),
73
+ Route("GET", "/api/hermes/models", AuthLevel.SESSION, "_route_get_hermes_models", sessionless_ok=True),
74
+ Route("GET", "/api/hermes/debug", AuthLevel.SESSION, "_route_get_hermes_debug", sessionless_ok=True),
75
+ Route("GET", "/api/hermes/insights", AuthLevel.SESSION, "_route_get_hermes_insights", sessionless_ok=True),
76
+ Route("GET", "/api/hermes/gquota", AuthLevel.SESSION, "_route_get_hermes_gquota", sessionless_ok=True),
77
+ # --- session GET ---
78
+ Route("GET", "/api/activity/detail", AuthLevel.SESSION, "_route_get_activity_detail"),
79
+ Route("GET", "/api/me", AuthLevel.SESSION, "_route_get_me"),
80
+ Route("GET", "/api/me/onboarding", AuthLevel.SESSION, "_route_get_me_onboarding"),
81
+ Route("GET", "/api/me/credentials", AuthLevel.SESSION, "_route_get_me_credentials"),
82
+ Route("GET", "/api/me/providers", AuthLevel.SESSION, "_route_get_me_providers"),
83
+ Route("GET", "/api/me/cohort", AuthLevel.SESSION, "_route_get_me_cohort"),
84
+ Route("GET", "/api/user/credentials", AuthLevel.SESSION, "_route_get_user_credentials"),
85
+ Route("GET", "/api/hermes/profiles/status", AuthLevel.SESSION, "_route_get_hermes_profiles_status"),
86
+ Route("GET", "/api/oauth/github/callback", AuthLevel.SESSION, "_route_get_oauth_github_callback"),
87
+ Route("GET", "/api/oauth/discord/callback", AuthLevel.SESSION, "_route_get_oauth_discord_callback"),
88
+ Route("GET", "/api/oauth/stripe/callback", AuthLevel.SESSION, "_route_get_oauth_stripe_callback"),
89
+ Route("GET", "/api/events", AuthLevel.SESSION, "_route_get_events"),
90
+ Route("GET", "/events", AuthLevel.SESSION, "_route_get_events"),
91
+ # --- owner/admin GET ---
92
+ Route("GET", "/api/chat/bootstrap", AuthLevel.ROLE_OWNER_ADMIN, "_route_get_chat_bootstrap"),
93
+ Route("GET", "/api/hermes/bootstrap", AuthLevel.ROLE_OWNER_ADMIN, "_route_get_hermes_bootstrap"),
94
+ Route("GET", "/api/admin/vault/status", AuthLevel.ROLE_OWNER_ADMIN, "_route_get_admin_vault_status"),
95
+ Route("GET", "/api/doctor/agent-dm-runtimes", AuthLevel.ROLE_OWNER_ADMIN, "_route_get_doctor_agent_dm_runtimes"),
96
+ Route("GET", "/api/admin/updates", AuthLevel.ROLE_OWNER_ADMIN, "_route_get_admin_updates"),
97
+ Route("GET", "/api/admin/audit", AuthLevel.ROLE_OWNER_ADMIN, "_route_get_admin_audit"),
98
+ # --- install-window GET ---
99
+ Route("GET", "/api/install/publish-hints", AuthLevel.PUBLIC, "_route_get_install_publish_hints", install_window=True),
100
+ Route("GET", "/api/install/url/test", AuthLevel.PUBLIC, "_route_get_install_url_test", install_window=True),
101
+ Route("GET", "/api/install/stack", AuthLevel.SESSION, "_route_get_install_stack", install_window=True),
102
+ # --- internal GET ---
103
+ Route("GET", "/api/supervisor/v1/profile.status", AuthLevel.SESSION, "_route_get_supervisor_profile_status"),
104
+ Route("GET", "/api/supervisor/v1/stack.status", AuthLevel.SESSION, "_route_get_supervisor_stack_status"),
105
+ # --- public POST (auth flow) ---
106
+ Route("POST", "/api/auth/start", AuthLevel.PUBLIC, "_route_post_auth_start"),
107
+ Route("POST", "/api/auth/verify", AuthLevel.PUBLIC, "_route_post_auth_verify"),
108
+ Route("POST", "/api/auth/logout", AuthLevel.PUBLIC, "_route_post_auth_logout"),
109
+ Route("POST", "/api/auth/refresh", AuthLevel.PUBLIC, "_route_post_auth_refresh"),
110
+ Route("POST", "/api/auth/bootstrap", AuthLevel.PUBLIC, "_route_post_auth_bootstrap"),
111
+ Route("POST", "/api/auth/google/start", AuthLevel.PUBLIC, "_route_post_auth_google_start"),
112
+ Route("POST", "/api/auth/local-bootstrap", AuthLevel.PUBLIC, "_route_post_auth_local_bootstrap"),
113
+ Route("POST", "/api/setup", AuthLevel.PUBLIC, "_route_post_setup"),
114
+ # --- session POST ---
115
+ Route("POST", "/api/hermes/model", AuthLevel.SESSION, "_route_post_hermes_model"),
116
+ Route("POST", "/api/hermes/model/apply-default", AuthLevel.SESSION, "_route_post_hermes_model_apply_default"),
117
+ Route("POST", "/api/hermes/fallback-chain", AuthLevel.SESSION, "_route_post_hermes_fallback_chain"),
118
+ Route("POST", "/api/hermes/commands/exec", AuthLevel.SESSION, "_route_post_hermes_commands_exec"),
119
+ Route("POST", "/api/hermes/gateway/exec", AuthLevel.SESSION, "_route_post_hermes_gateway_exec"),
120
+ Route("POST", "/api/chat/stop", AuthLevel.SESSION, "_route_post_chat_stop"),
121
+ Route("POST", "/api/chat/steer", AuthLevel.SESSION, "_route_post_chat_steer"),
122
+ Route("POST", "/api/me/credentials", AuthLevel.SESSION, "_route_post_me_credentials"),
123
+ Route("POST", "/api/me/telegram/link", AuthLevel.SESSION, "_route_post_me_telegram_link"),
124
+ Route("POST", "/api/board", AuthLevel.SESSION, "_route_post_board"),
125
+ Route("POST", "/api/board/update", AuthLevel.SESSION, "_route_post_board_update"),
126
+ Route("POST", "/api/board/delete", AuthLevel.SESSION, "_route_post_board_delete"),
127
+ Route("POST", "/api/content/save", AuthLevel.SESSION, "_route_post_content_save"),
128
+ Route("POST", "/api/content/delete", AuthLevel.SESSION, "_route_post_content_delete"),
129
+ Route("POST", "/api/files/write", AuthLevel.SESSION, "_route_post_files_write"),
130
+ Route("POST", "/api/files/upload", AuthLevel.SESSION, "_route_post_files_upload"),
131
+ Route("POST", "/api/chat/dispatch", AuthLevel.SESSION, "_route_post_chat_dispatch"),
132
+ Route("POST", "/api/hermes/profiles/create", AuthLevel.SESSION, "_route_post_hermes_profiles_create"),
133
+ Route("POST", "/api/hermes/profiles/start", AuthLevel.SESSION, "_route_post_hermes_profiles_start"),
134
+ Route("POST", "/api/hermes/profiles/stop", AuthLevel.SESSION, "_route_post_hermes_profiles_stop"),
135
+ Route("POST", "/api/hermes/profiles/delete", AuthLevel.SESSION, "_route_post_hermes_profiles_delete"),
136
+ Route("POST", "/api/hermes/profiles/disable", AuthLevel.SESSION, "_route_post_hermes_profiles_disable"),
137
+ Route("POST", "/api/supervisor/v1/profile.start", AuthLevel.ROLE_OWNER_ADMIN, "_route_post_supervisor_profile_start"),
138
+ Route("POST", "/api/supervisor/v1/profile.stop", AuthLevel.ROLE_OWNER_ADMIN, "_route_post_supervisor_profile_stop"),
139
+ Route("POST", "/api/supervisor/v1/profile.disable", AuthLevel.ROLE_OWNER_ADMIN, "_route_post_supervisor_profile_disable"),
140
+ # --- owner/admin POST ---
141
+ Route("POST", "/api/admin/updates/apply", AuthLevel.ROLE_OWNER_ADMIN, "_route_post_admin_updates_apply"),
142
+ Route("POST", "/api/admin/stack/restart-gateway", AuthLevel.ROLE_OWNER_ADMIN, "_route_post_admin_stack_restart_gateway"),
143
+ Route("POST", "/api/admin/vault/init", AuthLevel.ROLE_OWNER_ADMIN, "_route_post_admin_vault_init"),
144
+ Route("POST", "/api/admin/vault/unlock", AuthLevel.ROLE_OWNER_ADMIN, "_route_post_admin_vault_unlock"),
145
+ Route("POST", "/api/admin/vault/seal", AuthLevel.ROLE_OWNER_ADMIN, "_route_post_admin_vault_seal"),
146
+ Route("POST", "/api/admin/vault/wipe", AuthLevel.ROLE_OWNER_ADMIN, "_route_post_admin_vault_wipe"),
147
+ Route("POST", "/api/install/stack/branding-asset", AuthLevel.ROLE_OWNER_ADMIN, "_route_post_install_stack_branding_asset"),
148
+ # --- install-window POST ---
149
+ Route("POST", "/api/install/email/test", AuthLevel.PUBLIC, "_route_post_install_email_test", install_window=True),
150
+ Route("POST", "/api/install/url/test", AuthLevel.PUBLIC, "_route_post_install_url_test", install_window=True),
151
+ Route("POST", "/api/install/setup-https", AuthLevel.PUBLIC, "_route_post_install_setup_https", install_window=True),
152
+ Route("POST", "/api/install/complete", AuthLevel.PUBLIC, "_route_post_install_complete", install_window=True),
153
+ # --- PATCH ---
154
+ Route("PATCH", "/api/me", AuthLevel.SESSION, "_route_patch_me"),
155
+ Route("PATCH", "/api/me/native-agent", AuthLevel.SESSION, "_route_patch_me_native_agent"),
156
+ Route("PATCH", "/api/doctor/repair", AuthLevel.ROLE_OWNER_ADMIN, "_route_patch_doctor_repair"),
157
+ Route("PATCH", "/api/install/stack", AuthLevel.SESSION, "_route_patch_install_stack", install_window=True),
158
+ )
159
+
160
+ _ROUTE_PATTERNS_RAW: tuple[tuple, ...] = (
161
+ # method, regex, label, auth, sessionless_ok, install_window [, handler]
162
+ ("GET", r"^/api/files/workspace/.+", "/api/files/workspace/{path}", AuthLevel.SESSION, True, False, "_route_pattern_get_files_workspace"),
163
+ ("GET", r"^/api/public/branding/(og|favicon)$", "/api/public/branding/{kind}", AuthLevel.PUBLIC, False, False, "_route_pattern_get_public_branding"),
164
+ ("GET", r"^/api/workspace/[^/]+$", "/api/workspace/{id}", AuthLevel.SESSION, False, False, "_route_pattern_get_workspace"),
165
+ ("GET", r"^/api/workspace/[^/]+/rooms$", "/api/workspace/{id}/rooms", AuthLevel.SESSION, False, False, "_route_pattern_get_workspace_rooms"),
166
+ ("GET", r"^/api/rooms/[^/]+$", "/api/rooms/{id}", AuthLevel.SESSION, False, False, "_route_pattern_get_room"),
167
+ ("GET", r"^/api/rooms/[^/]+/members$", "/api/rooms/{id}/members", AuthLevel.SESSION, False, False, "_route_pattern_get_room_members"),
168
+ ("GET", r"^/api/rooms/[^/]+/live$", "/api/rooms/{id}/live", AuthLevel.SESSION, False, False, "_route_pattern_get_room_live"),
169
+ ("GET", r"^/api/rooms/[^/]+/activity$", "/api/rooms/{id}/activity", AuthLevel.SESSION, False, False, "_route_pattern_get_room_activity"),
170
+ ("GET", r"^/api/workspace/[^/]+/activity$", "/api/workspace/{id}/activity", AuthLevel.SESSION, False, False, "_route_pattern_get_workspace_activity"),
171
+ ("GET", r"^/api/workspace/[^/]+/kanban/tasks$", "/api/workspace/{id}/kanban/tasks", AuthLevel.SESSION, False, False, "_route_pattern_get_workspace_kanban_tasks"),
172
+ ("GET", r"^/api/workspace/[^/]+/delegation-grants$", "/api/workspace/{id}/delegation-grants", AuthLevel.SESSION, False, False, "_route_pattern_get_workspace_delegation_grants"),
173
+ ("GET", r"^/api/rooms/[^/]+/sessions$", "/api/rooms/{id}/sessions", AuthLevel.SESSION, False, False, "_route_pattern_get_room_sessions"),
174
+ ("GET", r"^/api/workspace/[^/]+/invites$", "/api/workspace/{id}/invites", AuthLevel.SESSION, False, False, "_route_pattern_get_workspace_invites"),
175
+ ("GET", r"^/api/invites/[^/]+$", "/api/invites/{token}", AuthLevel.SESSION, False, False, "_route_pattern_get_invite"),
176
+ ("GET", r"^/api/workspace/[^/]+/members$", "/api/workspace/{id}/members", AuthLevel.SESSION, False, False, "_route_pattern_get_workspace_members"),
177
+ ("GET", r"^/api/workspace/[^/]+/memory$", "/api/workspace/{id}/memory", AuthLevel.SESSION, False, False, "_route_pattern_get_workspace_memory"),
178
+ ("GET", r"^/api/workspace/[^/]+/budget$", "/api/workspace/{id}/budget", AuthLevel.SESSION, False, False, "_route_pattern_get_workspace_budget"),
179
+ ("GET", r"^/api/workspace/[^/]+/grants$", "/api/workspace/{id}/grants", AuthLevel.SESSION, False, False, "_route_pattern_get_workspace_grants"),
180
+ ("GET", r"^/api/rooms/[^/]+/messages$", "/api/rooms/{id}/messages", AuthLevel.SESSION, False, False, "_route_pattern_get_room_messages"),
181
+ ("GET", r"^/api/workspace/[^/]+/events$", "/api/workspace/{id}/events", AuthLevel.SESSION, False, False, "_route_pattern_get_workspace_events"),
182
+ ("GET", r"^/api/me/oauth/[^/]+/status$", "/api/me/oauth/{provider}/status", AuthLevel.SESSION, False, False, "_route_pattern_get_me_oauth_status"),
183
+ ("GET", r"^/api/hermes/profiles/[^/]+/soul$", "/api/hermes/profiles/{slug}/soul", AuthLevel.SESSION, False, False, "_route_pattern_get_hermes_profile_soul"),
184
+ ("GET", r"^/api/hermes/profiles/[^/]+/sessions$", "/api/hermes/profiles/{slug}/sessions", AuthLevel.SESSION, False, False, "_route_pattern_get_hermes_profile_sessions"),
185
+ ("GET", r"^/api/hermes/profiles/[^/]+/bind$", "/api/hermes/profiles/{slug}/bind", AuthLevel.SESSION, False, False, "_route_pattern_get_hermes_profile_bind"),
186
+ ("GET", r"^/api/hermes/profiles/[^/]+$", "/api/hermes/profiles/{slug}", AuthLevel.SESSION, False, False, "_route_pattern_get_hermes_profile_detail"),
187
+ ("GET", r"^/api/workspace/[^/]+/credentials$", "/api/workspace/{id}/credentials", AuthLevel.SESSION, False, False, "_route_pattern_get_workspace_credentials"),
188
+ ("GET", r"^/api/agents/[^/]+/credentials$", "/api/agents/{id}/credentials", AuthLevel.SESSION, False, False, "_route_pattern_get_agent_credentials"),
189
+ ("POST", r"^/api/me/oauth/[^/]+/start$", "/api/me/oauth/{provider}/start", AuthLevel.SESSION, False, False, "_route_pattern_post_me_oauth_start"),
190
+ ("POST", r"^/api/me/providers/[^/]+/disconnect$", "/api/me/providers/{id}/disconnect", AuthLevel.SESSION, False, False, "_route_pattern_post_me_providers_disconnect"),
191
+ ("POST", r"^/api/workspace/[^/]+/credentials/store$", "/api/workspace/{id}/credentials/store", AuthLevel.SESSION, False, False, "_route_pattern_post_workspace_credentials_store"),
192
+ ("POST", r"^/api/workspace/[^/]+/credentials/[^/]+/revoke$", "/api/workspace/{id}/credentials/{cid}/revoke", AuthLevel.SESSION, False, False, "_route_pattern_post_workspace_credentials_revoke"),
193
+ ("POST", r"^/api/workspace/[^/]+/rooms$", "/api/workspace/{id}/rooms", AuthLevel.SESSION, False, False, "_route_pattern_post_workspace_rooms"),
194
+ ("POST", r"^/api/workspace/[^/]+/rooms/create$", "/api/workspace/{id}/rooms/create", AuthLevel.SESSION, False, False, "_route_pattern_post_workspace_rooms_create"),
195
+ ("POST", r"^/api/rooms/[^/]+/members$", "/api/rooms/{id}/members", AuthLevel.SESSION, False, False, "_route_pattern_post_room_members"),
196
+ ("POST", r"^/api/rooms/[^/]+/bind$", "/api/rooms/{id}/bind", AuthLevel.SESSION, False, False, "_route_pattern_post_room_bind"),
197
+ ("POST", r"^/api/rooms/[^/]+/sessions/activate$", "/api/rooms/{id}/sessions/activate", AuthLevel.SESSION, False, False, "_route_pattern_post_room_sessions_activate"),
198
+ ("POST", r"^/api/rooms/[^/]+/messages/send$", "/api/rooms/{id}/messages/send", AuthLevel.SESSION, False, False, "_route_pattern_post_room_messages_send"),
199
+ ("POST", r"^/api/workspace/[^/]+/invites$", "/api/workspace/{id}/invites", AuthLevel.SESSION, False, False, "_route_pattern_post_workspace_invites"),
200
+ ("POST", r"^/api/invites/[^/]+/accept$", "/api/invites/{token}/accept", AuthLevel.SESSION, False, False, "_route_pattern_post_invites_accept"),
201
+ ("POST", r"^/api/workspace/[^/]+/members$", "/api/workspace/{id}/members", AuthLevel.SESSION, False, False, "_route_pattern_post_workspace_members"),
202
+ ("POST", r"^/api/workspace/[^/]+/kanban/tasks$", "/api/workspace/{id}/kanban/tasks", AuthLevel.SESSION, False, False, "_route_pattern_post_workspace_kanban_tasks"),
203
+ ("POST", r"^/api/workspace/[^/]+/delegation-grants$", "/api/workspace/{id}/delegation-grants", AuthLevel.SESSION, False, False, "_route_pattern_post_workspace_delegation_grants"),
204
+ ("POST", r"^/api/workspace/[^/]+/runtime-profiles/purge$", "/api/workspace/{id}/runtime-profiles/purge", AuthLevel.SESSION, False, False, "_route_pattern_post_workspace_runtime_profiles_purge"),
205
+ ("POST", r"^/api/workspace/[^/]+/memory$", "/api/workspace/{id}/memory", AuthLevel.SESSION, False, False, "_route_pattern_post_workspace_memory"),
206
+ ("POST", r"^/api/memory/[^/]+$", "/api/memory/{id}", AuthLevel.SESSION, False, False, "_route_pattern_post_memory"),
207
+ ("POST", r"^/api/workspace/[^/]+/budget$", "/api/workspace/{id}/budget", AuthLevel.SESSION, False, False, "_route_pattern_post_workspace_budget"),
208
+ ("POST", r"^/api/workspace/[^/]+/grants$", "/api/workspace/{id}/grants", AuthLevel.SESSION, False, False, "_route_pattern_post_workspace_grants"),
209
+ ("POST", r"^/api/hermes/profiles/[^/]+/bootstrap-dm$", "/api/hermes/profiles/{slug}/bootstrap-dm", AuthLevel.SESSION, False, False, "_route_pattern_post_hermes_profile_bootstrap_dm"),
210
+ ("POST", r"^/api/hermes/profiles/[^/]+/soul$", "/api/hermes/profiles/{slug}/soul", AuthLevel.SESSION, False, False, "_route_pattern_post_hermes_profile_soul"),
211
+ ("POST", r"^/api/hermes/profiles/[^/]+/bind$", "/api/hermes/profiles/{slug}/bind", AuthLevel.SESSION, False, False, "_route_pattern_post_hermes_profile_bind"),
212
+ ("POST", r"^/api/hermes/profiles/[^/]+/sessions$", "/api/hermes/profiles/{slug}/sessions", AuthLevel.SESSION, False, False, "_route_pattern_post_hermes_profile_sessions"),
213
+ ("POST", r"^/api/hermes/profiles/[^/]+/messages$", "/api/hermes/profiles/{slug}/messages", AuthLevel.SESSION, False, False, "_route_pattern_post_hermes_profile_messages"),
214
+ ("POST", r"^/api/hermes/profiles/[^/]+/messages/stream$", "/api/hermes/profiles/{slug}/messages/stream", AuthLevel.SESSION, False, False, "_route_pattern_post_hermes_profile_messages_stream"),
215
+ ("PATCH", r"^/api/hermes/profiles/[^/]+$", "/api/hermes/profiles/{slug}", AuthLevel.SESSION, False, False, "_route_pattern_patch_hermes_profile"),
216
+ ("PATCH", r"^/api/rooms/[^/]+$", "/api/rooms/{id}", AuthLevel.SESSION, False, False, "_route_pattern_patch_room"),
217
+ ("PATCH", r"^/api/workspace/[^/]+/integrations$", "/api/workspace/{id}/integrations", AuthLevel.SESSION, False, False, "_route_pattern_patch_workspace_integrations"),
218
+ ("PATCH", r"^/api/workspace/[^/]+/members$", "/api/workspace/{id}/members", AuthLevel.SESSION, False, False, "_route_pattern_patch_workspace_members"),
219
+ ("PATCH", r"^/api/workspace/[^/]+$", "/api/workspace/{id}", AuthLevel.SESSION, False, False, "_route_pattern_patch_workspace"),
220
+ ("DELETE", r"^/api/rooms/[^/]+$", "/api/rooms/{id}", AuthLevel.SESSION, False, False, "_route_pattern_delete_room"),
221
+ ("DELETE", r"^/api/memory/[^/]+$", "/api/memory/{id}", AuthLevel.SESSION, False, False, "_route_pattern_delete_memory"),
222
+ ("DELETE", r"^/api/workspace/[^/]+/members$", "/api/workspace/{id}/members", AuthLevel.SESSION, False, False, "_route_pattern_delete_workspace_members"),
223
+ ("DELETE", r"^/api/me/credentials/[^/]+$", "/api/me/credentials/{provider}", AuthLevel.SESSION, False, False, "_route_pattern_delete_me_credentials"),
224
+ )
225
+
226
+
227
+ def _coerce_pattern_row(row: tuple) -> tuple[str, str, str, AuthLevel, bool, bool, str | None]:
228
+ if len(row) >= 7:
229
+ return row[:7] # type: ignore[return-value]
230
+ m, rx, label, auth, sl, iw = row
231
+ return (m, rx, label, auth, sl, iw, None)
232
+
233
+
234
+ ROUTE_PATTERNS: tuple[RoutePattern, ...] = tuple(
235
+ RoutePattern(
236
+ method=m,
237
+ pattern=re.compile(rx),
238
+ label=label,
239
+ auth=auth,
240
+ sessionless_ok=sl,
241
+ install_window=iw,
242
+ handler=h,
243
+ )
244
+ for m, rx, label, auth, sl, iw, h in (_coerce_pattern_row(r) for r in _ROUTE_PATTERNS_RAW)
143
245
  )
144
246
 
145
247
  _ROUTES_BY_METHOD_PATH: dict[tuple[str, str], Route] = {
146
248
  (r.method.upper(), r.path): r for r in ROUTES
147
249
  }
148
250
 
149
-
150
- INSTALL_ROUTE_METHODS: dict[str, frozenset[str]] = {
151
- "GET": frozenset({
152
- "/api/install/publish-hints",
153
- "/api/install/url/test",
154
- }),
155
- "PATCH": frozenset({"/api/install/stack"}),
156
- "POST": frozenset({
157
- "/api/install/email/test",
158
- "/api/install/complete",
159
- "/api/install/url/test",
160
- "/api/install/setup-https",
161
- }),
251
+ _DISPATCH_ROUTES: dict[tuple[str, str], Route] = {
252
+ (r.method.upper(), r.path): r for r in ROUTES if r.handler
162
253
  }
163
254
 
164
255
 
165
- def resolve_auth_level(method: str, path: str) -> AuthLevel:
166
- """Map HTTP method + path to required auth level. All /api/* paths are covered."""
256
+ @dataclass(frozen=True, slots=True)
257
+ class AuthSpec:
258
+ auth: AuthLevel
259
+ sessionless_ok: bool = False
260
+ install_window: bool = False
261
+
262
+
263
+ def lookup_auth(method: str, path: str) -> AuthSpec | None:
264
+ """Return auth spec for a registered /api/* route, or None when unregistered."""
167
265
  m = str(method or "GET").upper()
168
266
  p = str(path or "").strip()
169
- if m == "GET" and p == "/api/auth/hermes-dashboard-gate":
170
- return AuthLevel.PUBLIC
171
- if m == "GET" and p in GET_ALWAYS_PUBLIC_ROUTES:
172
- return AuthLevel.PUBLIC
267
+ if not p.startswith("/api/"):
268
+ return None
269
+ exact = _ROUTES_BY_METHOD_PATH.get((m, p))
270
+ if exact:
271
+ return AuthSpec(exact.auth, sessionless_ok=exact.sessionless_ok, install_window=exact.install_window)
272
+ for rp in ROUTE_PATTERNS:
273
+ if rp.method == m and rp.pattern.fullmatch(p):
274
+ return AuthSpec(rp.auth, sessionless_ok=rp.sessionless_ok, install_window=rp.install_window)
173
275
  if p.startswith("/api/public/"):
174
- return AuthLevel.PUBLIC
175
- if m == "GET" and p in GET_SINGLE_USER_PUBLIC_ROUTES:
176
- return AuthLevel.SINGLE_USER_GET
177
- if m == "POST" and p in AUTH_FLOW_POST_ROUTES:
178
- return AuthLevel.AUTH_FLOW
179
- if p in INSTALL_ROUTE_METHODS.get(m, frozenset()):
180
- return AuthLevel.INSTALL
181
- if m == "GET" and p == "/api/install/stack":
182
- return AuthLevel.INSTALL
183
- if p.startswith("/api/"):
184
- return AuthLevel.SESSION
185
- return AuthLevel.PUBLIC
276
+ return AuthSpec(AuthLevel.PUBLIC)
277
+ if p.startswith("/internal/"):
278
+ return AuthSpec(AuthLevel.INTERNAL)
279
+ return None
280
+
281
+
282
+ def resolve_auth_level(method: str, path: str) -> AuthLevel | None:
283
+ """Map HTTP method + path to required auth level. None = unregistered /api/* (404 at dispatch)."""
284
+ spec = lookup_auth(method, path)
285
+ return spec.auth if spec else None
186
286
 
187
287
 
188
288
  def deployment_allows_sessionless_data_get(
@@ -199,13 +299,15 @@ def sessionless_get_allowed(
199
299
  deployment_mode: str,
200
300
  dev_local_unsafe: bool = False,
201
301
  ) -> bool:
202
- level = resolve_auth_level("GET", path)
203
- if level == AuthLevel.PUBLIC:
302
+ spec = lookup_auth("GET", path)
303
+ if spec is None:
304
+ return False
305
+ if spec.auth == AuthLevel.PUBLIC:
306
+ return True
307
+ if spec.sessionless_ok and deployment_allows_sessionless_data_get(
308
+ deployment_mode, dev_local_unsafe=dev_local_unsafe,
309
+ ):
204
310
  return True
205
- if level == AuthLevel.SINGLE_USER_GET:
206
- return deployment_allows_sessionless_data_get(
207
- deployment_mode, dev_local_unsafe=dev_local_unsafe,
208
- )
209
311
  return False
210
312
 
211
313
 
@@ -219,32 +321,53 @@ def authorize_request(
219
321
  install_window_open: bool,
220
322
  validate_session,
221
323
  attach_user,
324
+ get_workspace_role=None,
222
325
  ) -> bool:
223
- """Return True when the request may proceed to the handler."""
326
+ """Return True when the request may proceed. Unregistered /api/* passes (404 later)."""
224
327
  m = str(method or "GET").upper()
328
+ p = str(path or "").strip()
329
+
225
330
  if dev_local_unsafe:
226
- if session_id:
227
- info = validate_session(session_id)
331
+ sid = str(session_id or "").strip()
332
+ if sid:
333
+ info = validate_session(sid)
228
334
  if info:
229
335
  attach_user(info["user_id"])
230
336
  return True
231
337
  if m == "OPTIONS":
232
338
  return True
339
+ if not p.startswith("/api/"):
340
+ return True
341
+
342
+ spec = lookup_auth(m, p)
343
+ if spec is None:
344
+ return True
233
345
 
234
- level = resolve_auth_level(m, path)
235
346
  sid = str(session_id or "").strip()
236
347
 
348
+ if spec.install_window and install_window_open:
349
+ if sid:
350
+ info = validate_session(sid)
351
+ if info:
352
+ attach_user(info["user_id"])
353
+ return True
354
+
355
+ level = spec.auth
356
+
237
357
  if level == AuthLevel.PUBLIC:
358
+ if spec.install_window and not install_window_open:
359
+ return False
238
360
  if sid:
239
361
  info = validate_session(sid)
240
362
  if info:
241
363
  attach_user(info["user_id"])
242
364
  return True
243
365
 
244
- if level == AuthLevel.SINGLE_USER_GET:
245
- if m != "GET":
246
- return _require_session(sid, validate_session, attach_user)
247
- if deployment_allows_sessionless_data_get(
366
+ if level == AuthLevel.INTERNAL:
367
+ return True
368
+
369
+ if level == AuthLevel.SESSION:
370
+ if m == "GET" and spec.sessionless_ok and deployment_allows_sessionless_data_get(
248
371
  deployment_mode, dev_local_unsafe=dev_local_unsafe,
249
372
  ):
250
373
  if sid:
@@ -254,32 +377,25 @@ def authorize_request(
254
377
  return True
255
378
  return _require_session(sid, validate_session, attach_user)
256
379
 
257
- if level == AuthLevel.AUTH_FLOW:
258
- return True
259
-
260
- if level == AuthLevel.INSTALL:
261
- if not install_window_open:
380
+ if level == AuthLevel.ROLE_OWNER_ADMIN:
381
+ if not _require_session(sid, validate_session, attach_user):
262
382
  return False
263
- if sid:
264
- info = validate_session(sid)
265
- if info:
266
- attach_user(info["user_id"])
267
- return True
268
-
269
- if level == AuthLevel.SESSION:
270
- return _require_session(sid, validate_session, attach_user)
383
+ role = ""
384
+ if callable(get_workspace_role):
385
+ role = str(get_workspace_role() or "").strip().lower()
386
+ return role in {"owner", "admin"}
271
387
 
272
388
  return False
273
389
 
274
390
 
275
391
  def lookup_route(method: str, path: str) -> Route | None:
276
- return _ROUTES_BY_METHOD_PATH.get((str(method or "GET").upper(), str(path or "").strip()))
392
+ return _DISPATCH_ROUTES.get((str(method or "GET").upper(), str(path or "").strip()))
277
393
 
278
394
 
279
395
  def dispatch_get(handler: Any, path: str, qs: dict[str, list[str]]) -> bool:
280
396
  """Invoke a registered GET handler on *handler*; return True when dispatched."""
281
397
  route = lookup_route("GET", path)
282
- if not route:
398
+ if not route or not route.handler:
283
399
  return False
284
400
  fn = getattr(handler, route.handler, None)
285
401
  if not callable(fn):
@@ -291,7 +407,7 @@ def dispatch_get(handler: Any, path: str, qs: dict[str, list[str]]) -> bool:
291
407
  def dispatch_post(handler: Any, path: str, body: dict) -> bool:
292
408
  """Invoke a registered POST handler on *handler*; return True when dispatched."""
293
409
  route = lookup_route("POST", path)
294
- if not route:
410
+ if not route or not route.handler:
295
411
  return False
296
412
  fn = getattr(handler, route.handler, None)
297
413
  if not callable(fn):
@@ -300,6 +416,42 @@ def dispatch_post(handler: Any, path: str, body: dict) -> bool:
300
416
  return True
301
417
 
302
418
 
419
+ def dispatch_patch(handler: Any, path: str, body: dict) -> bool:
420
+ """Invoke a registered PATCH handler on *handler*; return True when dispatched."""
421
+ route = lookup_route("PATCH", path)
422
+ if not route or not route.handler:
423
+ return False
424
+ fn = getattr(handler, route.handler, None)
425
+ if not callable(fn):
426
+ raise RuntimeError(f"missing handler {route.handler!r} for PATCH {path}")
427
+ fn(body)
428
+ return True
429
+
430
+
431
+ def dispatch_pattern(
432
+ method: str,
433
+ handler: Any,
434
+ path: str,
435
+ *,
436
+ qs: dict[str, list[str]] | None = None,
437
+ body: dict | None = None,
438
+ ) -> bool:
439
+ """Invoke a pattern-backed handler; return True when dispatched."""
440
+ m = str(method or "GET").upper()
441
+ for rp in ROUTE_PATTERNS:
442
+ if rp.method != m or not rp.handler or not rp.pattern.fullmatch(path):
443
+ continue
444
+ fn = getattr(handler, rp.handler, None)
445
+ if not callable(fn):
446
+ raise RuntimeError(f"missing handler {rp.handler!r} for {m} {path}")
447
+ if m == "GET":
448
+ fn(path, qs or {})
449
+ else:
450
+ fn(path, body or {})
451
+ return True
452
+ return False
453
+
454
+
303
455
  def _require_session(sid: str, validate_session, attach_user) -> bool:
304
456
  if not sid:
305
457
  return False
@@ -313,18 +465,6 @@ def _require_session(sid: str, validate_session, attach_user) -> bool:
313
465
  def iter_registered_api_paths() -> frozenset[tuple[str, str]]:
314
466
  """Exact (method, path) pairs declared in the registry (for audit tests)."""
315
467
  out: set[tuple[str, str]] = set()
316
- for p in GET_ALWAYS_PUBLIC_ROUTES:
317
- out.add(("GET", p))
318
- for p in GET_SINGLE_USER_PUBLIC_ROUTES:
319
- out.add(("GET", p))
320
- for p in AUTH_FLOW_POST_ROUTES:
321
- out.add(("POST", p))
322
- for m, paths in INSTALL_ROUTE_METHODS.items():
323
- for p in paths:
324
- out.add((m, p))
325
- out.add(("GET", "/api/install/stack"))
326
- out.add(("PATCH", "/api/install/stack"))
327
- out.add(("GET", "/api/auth/hermes-dashboard-gate"))
328
468
  for route in ROUTES:
329
469
  out.add((route.method.upper(), route.path))
330
470
  return frozenset(out)