synapse-orch-ai 1.3.5 → 1.4.3

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 (203) hide show
  1. package/README.md +8 -0
  2. package/backend/core/agent_logger.py +18 -9
  3. package/backend/core/api_key_middleware.py +39 -0
  4. package/backend/core/api_keys.py +143 -0
  5. package/backend/core/config.py +39 -0
  6. package/backend/core/internal_auth.py +62 -0
  7. package/backend/core/models.py +3 -0
  8. package/backend/core/orchestration/engine.py +4 -3
  9. package/backend/core/routes/api_keys.py +46 -0
  10. package/backend/core/routes/api_v1.py +539 -0
  11. package/backend/core/routes/auth.py +43 -1
  12. package/backend/core/routes/settings.py +29 -2
  13. package/backend/core/server.py +10 -2
  14. package/backend/core/usage_tracker.py +3 -2
  15. package/backend/core/user_auth.py +48 -0
  16. package/backend/tools/code_search.py +46 -24
  17. package/frontend-build/.next/BUILD_ID +1 -1
  18. package/frontend-build/.next/app-path-routes-manifest.json +4 -0
  19. package/frontend-build/.next/build-manifest.json +3 -3
  20. package/frontend-build/.next/prerender-manifest.json +27 -3
  21. package/frontend-build/.next/required-server-files.json +2 -0
  22. package/frontend-build/.next/routes-manifest.json +24 -0
  23. package/frontend-build/.next/server/app/_global-error/page.js +1 -1
  24. package/frontend-build/.next/server/app/_global-error/page.js.nft.json +1 -1
  25. package/frontend-build/.next/server/app/_global-error.html +1 -1
  26. package/frontend-build/.next/server/app/_global-error.rsc +1 -1
  27. package/frontend-build/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  28. package/frontend-build/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  29. package/frontend-build/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  30. package/frontend-build/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  31. package/frontend-build/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  32. package/frontend-build/.next/server/app/_not-found/page.js +1 -1
  33. package/frontend-build/.next/server/app/_not-found/page.js.nft.json +1 -1
  34. package/frontend-build/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  35. package/frontend-build/.next/server/app/_not-found.html +1 -1
  36. package/frontend-build/.next/server/app/_not-found.rsc +2 -2
  37. package/frontend-build/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  38. package/frontend-build/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  39. package/frontend-build/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  40. package/frontend-build/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  41. package/frontend-build/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  42. package/frontend-build/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  43. package/frontend-build/.next/server/app/api/agent-types/route.js +1 -1
  44. package/frontend-build/.next/server/app/api/agent-types/route.js.nft.json +1 -1
  45. package/frontend-build/.next/server/app/api/agents/generate-prompt/route.js +1 -1
  46. package/frontend-build/.next/server/app/api/agents/generate-prompt/route.js.nft.json +1 -1
  47. package/frontend-build/.next/server/app/api/auth/login/route/app-paths-manifest.json +3 -0
  48. package/frontend-build/.next/server/app/api/auth/login/route/build-manifest.json +9 -0
  49. package/frontend-build/.next/server/app/api/auth/login/route/server-reference-manifest.json +4 -0
  50. package/frontend-build/.next/server/app/api/auth/login/route.js +6 -0
  51. package/frontend-build/.next/server/app/api/auth/login/route.js.map +5 -0
  52. package/frontend-build/.next/server/app/api/auth/login/route.js.nft.json +1 -0
  53. package/frontend-build/.next/server/app/api/auth/login/route_client-reference-manifest.js +3 -0
  54. package/frontend-build/.next/server/app/api/auth/logout/route/app-paths-manifest.json +3 -0
  55. package/frontend-build/.next/server/app/api/auth/logout/route/build-manifest.json +9 -0
  56. package/frontend-build/.next/server/app/api/auth/logout/route/server-reference-manifest.json +4 -0
  57. package/frontend-build/.next/server/app/api/auth/logout/route.js +6 -0
  58. package/frontend-build/.next/server/app/api/auth/logout/route.js.map +5 -0
  59. package/frontend-build/.next/server/app/api/auth/logout/route.js.nft.json +1 -0
  60. package/frontend-build/.next/server/app/api/auth/logout/route_client-reference-manifest.js +3 -0
  61. package/frontend-build/.next/server/app/api/auth/status/route/app-paths-manifest.json +3 -0
  62. package/frontend-build/.next/server/app/api/auth/status/route/build-manifest.json +9 -0
  63. package/frontend-build/.next/server/app/api/auth/status/route/server-reference-manifest.json +4 -0
  64. package/frontend-build/.next/server/app/api/auth/status/route.js +6 -0
  65. package/frontend-build/.next/server/app/api/auth/status/route.js.map +5 -0
  66. package/frontend-build/.next/server/app/api/auth/status/route.js.nft.json +1 -0
  67. package/frontend-build/.next/server/app/api/auth/status/route_client-reference-manifest.js +3 -0
  68. package/frontend-build/.next/server/app/api/builder/chat/route.js +1 -1
  69. package/frontend-build/.next/server/app/api/builder/chat/route.js.nft.json +1 -1
  70. package/frontend-build/.next/server/app/api/builder/resume/route.js +1 -1
  71. package/frontend-build/.next/server/app/api/builder/resume/route.js.nft.json +1 -1
  72. package/frontend-build/.next/server/app/api/chat/route.js +1 -1
  73. package/frontend-build/.next/server/app/api/chat/route.js.nft.json +1 -1
  74. package/frontend-build/.next/server/app/api/chat/stream/route.js +1 -1
  75. package/frontend-build/.next/server/app/api/chat/stream/route.js.nft.json +1 -1
  76. package/frontend-build/.next/server/app/api/logs/[type]/[run_id]/route.js +1 -1
  77. package/frontend-build/.next/server/app/api/logs/[type]/[run_id]/route.js.nft.json +1 -1
  78. package/frontend-build/.next/server/app/api/logs/[type]/route.js +1 -1
  79. package/frontend-build/.next/server/app/api/logs/[type]/route.js.nft.json +1 -1
  80. package/frontend-build/.next/server/app/api/models/route.js +1 -1
  81. package/frontend-build/.next/server/app/api/models/route.js.nft.json +1 -1
  82. package/frontend-build/.next/server/app/api/orchestrations/[orch_id]/run/route.js +1 -1
  83. package/frontend-build/.next/server/app/api/orchestrations/[orch_id]/run/route.js.nft.json +1 -1
  84. package/frontend-build/.next/server/app/api/orchestrations/runs/[run_id]/human-input/route.js +1 -1
  85. package/frontend-build/.next/server/app/api/orchestrations/runs/[run_id]/human-input/route.js.nft.json +1 -1
  86. package/frontend-build/.next/server/app/api/orchestrations/runs/[run_id]/resume/route.js +1 -1
  87. package/frontend-build/.next/server/app/api/orchestrations/runs/[run_id]/resume/route.js.nft.json +1 -1
  88. package/frontend-build/.next/server/app/api/schedules/[schedule_id]/route.js +1 -1
  89. package/frontend-build/.next/server/app/api/schedules/[schedule_id]/route.js.nft.json +1 -1
  90. package/frontend-build/.next/server/app/api/schedules/[schedule_id]/run/route.js +1 -1
  91. package/frontend-build/.next/server/app/api/schedules/[schedule_id]/run/route.js.nft.json +1 -1
  92. package/frontend-build/.next/server/app/api/schedules/route.js +1 -1
  93. package/frontend-build/.next/server/app/api/schedules/route.js.nft.json +1 -1
  94. package/frontend-build/.next/server/app/index.html +1 -1
  95. package/frontend-build/.next/server/app/index.rsc +3 -3
  96. package/frontend-build/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  97. package/frontend-build/.next/server/app/index.segments/_full.segment.rsc +3 -3
  98. package/frontend-build/.next/server/app/index.segments/_head.segment.rsc +1 -1
  99. package/frontend-build/.next/server/app/index.segments/_index.segment.rsc +2 -2
  100. package/frontend-build/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  101. package/frontend-build/.next/server/app/login/page/app-paths-manifest.json +3 -0
  102. package/frontend-build/.next/server/app/login/page/build-manifest.json +17 -0
  103. package/frontend-build/.next/server/app/login/page/next-font-manifest.json +14 -0
  104. package/frontend-build/.next/server/app/login/page/react-loadable-manifest.json +1 -0
  105. package/frontend-build/.next/server/app/login/page/server-reference-manifest.json +4 -0
  106. package/frontend-build/.next/server/app/login/page.js +14 -0
  107. package/frontend-build/.next/server/app/login/page.js.map +5 -0
  108. package/frontend-build/.next/server/app/login/page.js.nft.json +1 -0
  109. package/frontend-build/.next/server/app/login/page_client-reference-manifest.js +3 -0
  110. package/frontend-build/.next/server/app/login.html +1 -0
  111. package/frontend-build/.next/server/app/login.meta +15 -0
  112. package/frontend-build/.next/server/app/login.rsc +27 -0
  113. package/frontend-build/.next/server/app/login.segments/_full.segment.rsc +27 -0
  114. package/frontend-build/.next/server/app/login.segments/_head.segment.rsc +6 -0
  115. package/frontend-build/.next/server/app/login.segments/_index.segment.rsc +7 -0
  116. package/frontend-build/.next/server/app/login.segments/_tree.segment.rsc +8 -0
  117. package/frontend-build/.next/server/app/login.segments/login/__PAGE__.segment.rsc +9 -0
  118. package/frontend-build/.next/server/app/login.segments/login.segment.rsc +5 -0
  119. package/frontend-build/.next/server/app/page.js +1 -1
  120. package/frontend-build/.next/server/app/page.js.nft.json +1 -1
  121. package/frontend-build/.next/server/app/page_client-reference-manifest.js +1 -1
  122. package/frontend-build/.next/server/app/settings/[tab]/page.js +1 -1
  123. package/frontend-build/.next/server/app/settings/[tab]/page.js.nft.json +1 -1
  124. package/frontend-build/.next/server/app/settings/[tab]/page_client-reference-manifest.js +1 -1
  125. package/frontend-build/.next/server/app-paths-manifest.json +4 -0
  126. package/frontend-build/.next/server/chunks/[root-of-the-server]__00ci3m0._.js +3 -0
  127. package/frontend-build/.next/server/chunks/[root-of-the-server]__01~.b7w._.js +3 -0
  128. package/frontend-build/.next/server/chunks/[root-of-the-server]__066njqe._.js +3 -0
  129. package/frontend-build/.next/server/chunks/[root-of-the-server]__08cioyi._.js +3 -0
  130. package/frontend-build/.next/server/chunks/[root-of-the-server]__0df0b6v._.js +3 -0
  131. package/frontend-build/.next/server/chunks/[root-of-the-server]__0efx8a5._.js +3 -0
  132. package/frontend-build/.next/server/chunks/[root-of-the-server]__0j8-xkl._.js +1 -1
  133. package/frontend-build/.next/server/chunks/[root-of-the-server]__0kk2o8d._.js +3 -0
  134. package/frontend-build/.next/server/chunks/[root-of-the-server]__0kyyn1g._.js +3 -0
  135. package/frontend-build/.next/server/chunks/[root-of-the-server]__0o2ckji._.js +3 -0
  136. package/frontend-build/.next/server/chunks/[root-of-the-server]__0r1_rdp._.js +3 -0
  137. package/frontend-build/.next/server/chunks/[root-of-the-server]__0s65g6m._.js +3 -0
  138. package/frontend-build/.next/server/chunks/[root-of-the-server]__0u8_aw2._.js +3 -0
  139. package/frontend-build/.next/server/chunks/[root-of-the-server]__0z5q5.1._.js +3 -0
  140. package/frontend-build/.next/server/chunks/[root-of-the-server]__0~o635_._.js +3 -0
  141. package/frontend-build/.next/server/chunks/[root-of-the-server]__10p49e~._.js +3 -0
  142. package/frontend-build/.next/server/chunks/[root-of-the-server]__115~uq6._.js +3 -0
  143. package/frontend-build/.next/server/chunks/[root-of-the-server]__12ug7cf._.js +3 -0
  144. package/frontend-build/.next/server/chunks/[root-of-the-server]__12vs3wg._.js +3 -0
  145. package/frontend-build/.next/server/chunks/_next-internal_server_app_api_auth_login_route_actions_0zukc38.js +3 -0
  146. package/frontend-build/.next/server/chunks/_next-internal_server_app_api_auth_logout_route_actions_0fm~3ij.js +3 -0
  147. package/frontend-build/.next/server/chunks/_next-internal_server_app_api_auth_status_route_actions_0rmqb6l.js +3 -0
  148. package/frontend-build/.next/server/chunks/ssr/[root-of-the-server]__08l1kmh._.js +3 -0
  149. package/frontend-build/.next/server/chunks/ssr/[root-of-the-server]__09c9368._.js +3 -0
  150. package/frontend-build/.next/server/chunks/ssr/_0b~n.nn._.js +91 -19
  151. package/frontend-build/.next/server/chunks/ssr/_0rwng5.._.js +4 -0
  152. package/frontend-build/.next/server/chunks/ssr/_next-internal_server_app_login_page_actions_02kefem.js +3 -0
  153. package/frontend-build/.next/server/chunks/ssr/node_modules_0g2b~5_._.js +6 -0
  154. package/frontend-build/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_0__0o-2._.js +3 -0
  155. package/frontend-build/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0328vov.js +4 -0
  156. package/frontend-build/.next/server/chunks/ssr/src_app_login_page_tsx_0jt.sij._.js +3 -0
  157. package/frontend-build/.next/server/edge/chunks/[root-of-the-server]__08ca2jv._.js +11 -0
  158. package/frontend-build/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0kvehva.js +3 -0
  159. package/frontend-build/.next/server/edge/chunks/turbopack-node_modules_next_dist_esm_build_templates_edge-wrapper_0lcpsxv.js +3 -0
  160. package/frontend-build/.next/server/middleware-build-manifest.js +3 -3
  161. package/frontend-build/.next/server/middleware-manifest.json +30 -2
  162. package/frontend-build/.next/server/next-font-manifest.js +1 -1
  163. package/frontend-build/.next/server/next-font-manifest.json +7 -0
  164. package/frontend-build/.next/server/pages/404.html +1 -1
  165. package/frontend-build/.next/server/pages/500.html +1 -1
  166. package/frontend-build/.next/server/server-reference-manifest.js +1 -1
  167. package/frontend-build/.next/server/server-reference-manifest.json +1 -1
  168. package/frontend-build/.next/static/N0fsCSqo5vmIgwJh-r_dk/_clientMiddlewareManifest.js +6 -0
  169. package/frontend-build/.next/static/chunks/0e0doloxq75td.js +1 -0
  170. package/frontend-build/.next/static/chunks/0k0-nxx35rq9v.js +1 -0
  171. package/frontend-build/.next/static/chunks/0w5f~4w31j03v.js +2 -0
  172. package/frontend-build/.next/static/chunks/0yq31mtv7c.bs.css +1 -0
  173. package/frontend-build/.next/static/chunks/18b0o~cnvc.gn.js +124 -0
  174. package/frontend-build/package.json +1 -0
  175. package/frontend-build/server.js +1 -1
  176. package/package.json +1 -1
  177. package/frontend-build/.next/server/chunks/[root-of-the-server]__00sljji._.js +0 -3
  178. package/frontend-build/.next/server/chunks/[root-of-the-server]__03e7r3a._.js +0 -3
  179. package/frontend-build/.next/server/chunks/[root-of-the-server]__053~3b.._.js +0 -3
  180. package/frontend-build/.next/server/chunks/[root-of-the-server]__0afb7iz._.js +0 -3
  181. package/frontend-build/.next/server/chunks/[root-of-the-server]__0bc5iuk._.js +0 -3
  182. package/frontend-build/.next/server/chunks/[root-of-the-server]__0bygctj._.js +0 -3
  183. package/frontend-build/.next/server/chunks/[root-of-the-server]__0c7o1w_._.js +0 -3
  184. package/frontend-build/.next/server/chunks/[root-of-the-server]__0cy6bl4._.js +0 -3
  185. package/frontend-build/.next/server/chunks/[root-of-the-server]__0dmb8p1._.js +0 -3
  186. package/frontend-build/.next/server/chunks/[root-of-the-server]__0gx4j6x._.js +0 -3
  187. package/frontend-build/.next/server/chunks/[root-of-the-server]__0h2-vsq._.js +0 -3
  188. package/frontend-build/.next/server/chunks/[root-of-the-server]__0kkdqe3._.js +0 -3
  189. package/frontend-build/.next/server/chunks/[root-of-the-server]__0n1p1jk._.js +0 -3
  190. package/frontend-build/.next/server/chunks/[root-of-the-server]__0xccig4._.js +0 -3
  191. package/frontend-build/.next/server/chunks/[root-of-the-server]__13dxl3m._.js +0 -3
  192. package/frontend-build/.next/server/chunks/ssr/[root-of-the-server]__0ssmzpx._.js +0 -3
  193. package/frontend-build/.next/server/chunks/ssr/_0ayz11y._.js +0 -4
  194. package/frontend-build/.next/server/chunks/ssr/node_modules_0zhhlrq._.js +0 -6
  195. package/frontend-build/.next/server/chunks/ssr/node_modules_1041ur2._.js +0 -6
  196. package/frontend-build/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_0ag5..o._.js +0 -3
  197. package/frontend-build/.next/static/chunks/0n53o._htd6sy.js +0 -2
  198. package/frontend-build/.next/static/chunks/0n_aip.6onjkv.js +0 -52
  199. package/frontend-build/.next/static/chunks/0or4d2ll~v9-c.css +0 -1
  200. package/frontend-build/.next/static/chunks/0vn.s1wic2p_8.js +0 -1
  201. package/frontend-build/.next/static/ijTAG225bIapjjOjjyPUj/_clientMiddlewareManifest.js +0 -1
  202. /package/frontend-build/.next/static/{ijTAG225bIapjjOjjyPUj → N0fsCSqo5vmIgwJh-r_dk}/_buildManifest.js +0 -0
  203. /package/frontend-build/.next/static/{ijTAG225bIapjjOjjyPUj → N0fsCSqo5vmIgwJh-r_dk}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -55,6 +55,14 @@ pip install synapse-orch-ai
55
55
  synapse
56
56
  ```
57
57
 
58
+ ### Upgrading
59
+
60
+ | Install method | Upgrade command |
61
+ |---|---|
62
+ | Bash / PowerShell installer (recommended) | `synapse upgrade` |
63
+ | pip | `pip install --upgrade synapse-orch-ai` |
64
+ | npm | `npm update -g synapse-orch-ai` |
65
+
58
66
  ---
59
67
 
60
68
  ## CLI
@@ -3,8 +3,9 @@ Plain-text debug logging for individual agent runs.
3
3
  Logs each call to an agent (from chat or orchestration) including all tools
4
4
  used and their responses, in the same terminal-style format as orchestration logs.
5
5
  """
6
- import asyncio
7
6
  import json
7
+ import queue
8
+ import threading
8
9
  import time
9
10
  from pathlib import Path
10
11
 
@@ -43,6 +44,9 @@ class AgentLogger:
43
44
  self.run_id = f"agentrun_{short_id}_{int(time.time() * 1000)}"
44
45
  self.path = LOGS_DIR / f"{self.run_id}.log"
45
46
  self._start_time = time.time()
47
+ self._q: queue.SimpleQueue = queue.SimpleQueue()
48
+ self._thread = threading.Thread(target=self._drain, daemon=True, name=f"agent-log-{self.run_id}")
49
+ self._thread.start()
46
50
 
47
51
  self._write(f"""
48
52
  {'='*80}
@@ -61,18 +65,23 @@ class AgentLogger:
61
65
  # ── Core write ─────────────────────────────────────────────────
62
66
 
63
67
  def _write(self, text: str):
64
- """Sync write — only call from a thread (via _write_async) or startup."""
65
68
  with open(self.path, "a", encoding="utf-8") as f:
66
69
  f.write(text)
67
70
 
68
71
  def _write_bg(self, text: str):
69
- """Fire-and-forget write that offloads to a thread so the event loop isn't blocked."""
70
- try:
71
- loop = asyncio.get_running_loop()
72
- loop.run_in_executor(None, self._write, text)
73
- except RuntimeError:
74
- # No running loop (e.g. called from sync context at startup)
75
- self._write(text)
72
+ """Fire-and-forget: enqueue for the background writer thread."""
73
+ self._q.put(text)
74
+
75
+ def _drain(self):
76
+ """Background thread: drains the write queue in order."""
77
+ while True:
78
+ text = self._q.get()
79
+ if text is None:
80
+ break
81
+ try:
82
+ self._write(text)
83
+ except Exception:
84
+ pass
76
85
 
77
86
  # ── Run lifecycle ──────────────────────────────────────────────
78
87
 
@@ -0,0 +1,39 @@
1
+ """
2
+ API Key Authentication Dependency
3
+ ----------------------------------
4
+ FastAPI dependency that validates Bearer tokens on /api/v1/* routes.
5
+
6
+ Usage in route handlers:
7
+ from core.api_key_middleware import require_api_key
8
+
9
+ @router.post("/chat")
10
+ async def chat(key_record: dict = Depends(require_api_key)):
11
+ # key_record contains id, name, key_prefix, etc.
12
+ ...
13
+ """
14
+ from fastapi import HTTPException, Security
15
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
16
+
17
+ from core.api_keys import validate_api_key
18
+
19
+ _security = HTTPBearer(
20
+ scheme_name="API Key",
21
+ description="API key in Bearer format: `Authorization: Bearer sk-syn-...`",
22
+ )
23
+
24
+
25
+ async def require_api_key(
26
+ credentials: HTTPAuthorizationCredentials = Security(_security),
27
+ ) -> dict:
28
+ """FastAPI dependency: validates Bearer token and returns the key record.
29
+
30
+ Raises 401 if the key is missing, invalid, or revoked.
31
+ """
32
+ record = validate_api_key(credentials.credentials)
33
+ if not record:
34
+ raise HTTPException(
35
+ status_code=401,
36
+ detail="Invalid or revoked API key",
37
+ headers={"WWW-Authenticate": "Bearer"},
38
+ )
39
+ return record
@@ -0,0 +1,143 @@
1
+ """
2
+ API Key Management
3
+ ------------------
4
+ Generate, validate, list, revoke, and delete API keys.
5
+
6
+ Keys use the format: sk-syn-<32 hex chars>
7
+ Only the SHA-256 hash is persisted — the raw key is returned exactly once
8
+ at generation time and never stored.
9
+
10
+ Storage: DATA_DIR/api_keys.json
11
+ """
12
+ import hashlib
13
+ import json
14
+ import os
15
+ import secrets
16
+ import threading
17
+ import uuid
18
+ from datetime import datetime, timezone
19
+ from typing import Optional
20
+
21
+ from core.config import DATA_DIR
22
+
23
+ API_KEYS_FILE = os.path.join(DATA_DIR, "api_keys.json")
24
+ _lock = threading.Lock()
25
+
26
+ # Key prefix format
27
+ _KEY_PREFIX = "sk-syn-"
28
+ _KEY_HEX_LENGTH = 32 # 32 hex chars = 128 bits of entropy
29
+
30
+
31
+ def _load_keys() -> list[dict]:
32
+ """Load all key records from disk."""
33
+ if not os.path.exists(API_KEYS_FILE):
34
+ return []
35
+ try:
36
+ with open(API_KEYS_FILE, "r", encoding="utf-8") as f:
37
+ return json.load(f)
38
+ except Exception:
39
+ return []
40
+
41
+
42
+ def _save_keys(keys: list[dict]):
43
+ """Persist key records to disk."""
44
+ with open(API_KEYS_FILE, "w", encoding="utf-8") as f:
45
+ json.dump(keys, f, indent=2, ensure_ascii=False)
46
+
47
+
48
+ def _hash_key(raw_key: str) -> str:
49
+ """SHA-256 hash of the raw API key."""
50
+ return hashlib.sha256(raw_key.encode("utf-8")).hexdigest()
51
+
52
+
53
+ def generate_api_key(name: str) -> tuple[str, dict]:
54
+ """Generate a new API key.
55
+
56
+ Returns:
57
+ (plaintext_key, key_record) — the plaintext key is shown ONCE.
58
+ """
59
+ hex_part = secrets.token_hex(_KEY_HEX_LENGTH)
60
+ raw_key = f"{_KEY_PREFIX}{hex_part}"
61
+
62
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
63
+ record = {
64
+ "id": str(uuid.uuid4()),
65
+ "name": name or "Unnamed Key",
66
+ "key_hash": _hash_key(raw_key),
67
+ "key_prefix": raw_key[:12], # "sk-syn-XXXX" for display
68
+ "created_at": now,
69
+ "last_used_at": None,
70
+ "is_active": True,
71
+ }
72
+
73
+ with _lock:
74
+ keys = _load_keys()
75
+ keys.append(record)
76
+ _save_keys(keys)
77
+
78
+ return raw_key, record
79
+
80
+
81
+ def validate_api_key(raw_key: str) -> Optional[dict]:
82
+ """Validate a raw API key.
83
+
84
+ Returns the key record if valid and active, None otherwise.
85
+ Also updates last_used_at on success.
86
+ """
87
+ if not raw_key or not raw_key.startswith(_KEY_PREFIX):
88
+ return None
89
+
90
+ key_hash = _hash_key(raw_key)
91
+
92
+ with _lock:
93
+ keys = _load_keys()
94
+ for key in keys:
95
+ if key["key_hash"] == key_hash and key.get("is_active", True):
96
+ key["last_used_at"] = datetime.now(timezone.utc).strftime(
97
+ "%Y-%m-%dT%H:%M:%SZ"
98
+ )
99
+ _save_keys(keys)
100
+ return key
101
+ return None
102
+
103
+
104
+ def list_api_keys() -> list[dict]:
105
+ """Return all key records with metadata (no hashes)."""
106
+ with _lock:
107
+ keys = _load_keys()
108
+ # Strip sensitive fields
109
+ return [
110
+ {
111
+ "id": k["id"],
112
+ "name": k["name"],
113
+ "key_prefix": k["key_prefix"],
114
+ "created_at": k["created_at"],
115
+ "last_used_at": k.get("last_used_at"),
116
+ "is_active": k.get("is_active", True),
117
+ }
118
+ for k in keys
119
+ ]
120
+
121
+
122
+ def revoke_api_key(key_id: str) -> bool:
123
+ """Soft-revoke a key (set is_active=False). Returns True if found."""
124
+ with _lock:
125
+ keys = _load_keys()
126
+ for key in keys:
127
+ if key["id"] == key_id:
128
+ key["is_active"] = False
129
+ _save_keys(keys)
130
+ return True
131
+ return False
132
+
133
+
134
+ def delete_api_key(key_id: str) -> bool:
135
+ """Hard-delete a key. Returns True if found and deleted."""
136
+ with _lock:
137
+ keys = _load_keys()
138
+ original_count = len(keys)
139
+ keys = [k for k in keys if k["id"] != key_id]
140
+ if len(keys) < original_count:
141
+ _save_keys(keys)
142
+ return True
143
+ return False
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import json
3
+ import secrets as _secrets
3
4
  from pathlib import Path
4
5
  from urllib.parse import urlparse, urlunparse
5
6
 
@@ -57,6 +58,9 @@ def load_settings():
57
58
  "messaging_enabled": True,
58
59
  "embed_code": False,
59
60
  "bash_allowed_dirs": [],
61
+ "login_enabled": False,
62
+ "login_username": "",
63
+ "login_password_hash": "",
60
64
  }
61
65
 
62
66
  if not os.path.exists(SETTINGS_FILE):
@@ -72,6 +76,41 @@ def load_settings():
72
76
  return default_settings
73
77
 
74
78
 
79
+ def get_or_create_jwt_secret() -> str:
80
+ """Return SYNAPSE_JWT_SECRET from the environment or .env file.
81
+
82
+ Persistence is handled by the CLI (synapse/cli.py) before the server starts.
83
+ If the secret is missing here (e.g. server run directly without the CLI),
84
+ an ephemeral in-memory value is used for this session only.
85
+ """
86
+ env_file = _PROJECT_ROOT / ".env"
87
+ var = "SYNAPSE_JWT_SECRET"
88
+
89
+ existing = os.environ.get(var, "")
90
+ if existing:
91
+ return existing
92
+
93
+ if env_file.exists():
94
+ try:
95
+ for line in env_file.read_text().splitlines():
96
+ line = line.strip()
97
+ if line.startswith(f"{var}=") and len(line) > len(f"{var}="):
98
+ val = line.split("=", 1)[1].strip()
99
+ if val:
100
+ os.environ[var] = val
101
+ return val
102
+ except Exception:
103
+ pass
104
+
105
+ secret = _secrets.token_hex(32)
106
+ os.environ[var] = secret
107
+ print(
108
+ f"Warning: {var} was not found; generated an ephemeral in-memory secret. "
109
+ f"Set {var} in the environment (or run 'synapse start') to persist across restarts."
110
+ )
111
+ return secret
112
+
113
+
75
114
  def sanitize_db_url(raw: str) -> str:
76
115
  """Normalize a PostgreSQL URL for use with psycopg (not SQLAlchemy).
77
116
 
@@ -0,0 +1,62 @@
1
+ """
2
+ Internal Token Middleware
3
+ -------------------------
4
+ Protects all /api/* routes from direct external access.
5
+
6
+ Only the Next.js frontend knows the SYNAPSE_INTERNAL_TOKEN and injects it
7
+ as an X-Synapse-Internal header on every proxied request. External callers
8
+ that try to hit /api/settings, /api/agents, etc. directly will get 403.
9
+
10
+ Rules:
11
+ - /api/v1/* → SKIP (uses API key auth instead)
12
+ - /docs, /openapi.json, /redoc → SKIP (FastAPI docs)
13
+ - /chat*, /auth/* → SKIP (direct backend routes, not under /api/)
14
+ - /api/* → REQUIRE X-Synapse-Internal header
15
+ - If SYNAPSE_INTERNAL_TOKEN is not set → permissive (backward compatible)
16
+ """
17
+ import os
18
+
19
+ from fastapi import Request
20
+ from fastapi.responses import JSONResponse
21
+ from starlette.middleware.base import BaseHTTPMiddleware
22
+
23
+
24
+ class InternalTokenMiddleware(BaseHTTPMiddleware):
25
+ """Block direct access to internal /api/* routes without the internal token."""
26
+
27
+ def __init__(self, app):
28
+ super().__init__(app)
29
+ self.token = os.getenv("SYNAPSE_INTERNAL_TOKEN", "")
30
+
31
+ async def dispatch(self, request: Request, call_next):
32
+ path = request.url.path
33
+
34
+ # If no token configured, be permissive (local dev / backward compat)
35
+ if not self.token:
36
+ return await call_next(request)
37
+
38
+ # Skip: V1 API routes (they use API key auth)
39
+ if path.startswith("/api/v1/") or path == "/api/v1":
40
+ return await call_next(request)
41
+
42
+ # Skip: MCP OAuth callback — called by external OAuth providers, not frontend
43
+ if path == "/api/mcp/oauth/callback":
44
+ return await call_next(request)
45
+
46
+ # Skip: FastAPI docs
47
+ if path in ("/docs", "/openapi.json", "/redoc"):
48
+ return await call_next(request)
49
+
50
+ # Skip: non-API routes (chat, auth, health, websocket, etc.)
51
+ if not path.startswith("/api/"):
52
+ return await call_next(request)
53
+
54
+ # This is an /api/* route — require internal token
55
+ provided = request.headers.get("X-Synapse-Internal", "")
56
+ if provided != self.token:
57
+ return JSONResponse(
58
+ status_code=403,
59
+ content={"detail": "Forbidden"},
60
+ )
61
+
62
+ return await call_next(request)
@@ -112,6 +112,9 @@ class Settings(BaseModel):
112
112
  report_agent_enabled: bool = True
113
113
  coding_agent_enabled: bool = True
114
114
  messaging_enabled: bool = True
115
+ login_enabled: bool = False
116
+ login_username: str = ""
117
+ login_password_hash: str = ""
115
118
 
116
119
 
117
120
  class PersonalAddress(BaseModel):
@@ -239,17 +239,18 @@ class OrchestrationEngine:
239
239
 
240
240
  except Exception as e:
241
241
  import traceback; print(f"DEBUG ENGINE: ❌ EXCEPTION in step '{step.id}': {e}\n{traceback.format_exc()}", flush=True)
242
+ safe_error = "An internal error occurred while executing this step."
242
243
  run.step_history.append({
243
244
  "step_id": step.id,
244
245
  "step_name": step.name,
245
246
  "step_type": step.type.value,
246
247
  "status": "failed",
247
- "error": str(e),
248
+ "error": safe_error,
248
249
  })
249
250
  run.status = "failed"
250
251
  if logger:
251
- logger.step_end(step.id, "failed", str(e))
252
- yield {"type": "step_error", "orch_step_id": step.id, "error": str(e)}
252
+ logger.step_end(step.id, "failed", safe_error)
253
+ yield {"type": "step_error", "orch_step_id": step.id, "error": safe_error}
253
254
  break
254
255
 
255
256
  # Finalize
@@ -0,0 +1,46 @@
1
+ """
2
+ API Key Management Endpoints
3
+ -----------------------------
4
+ CRUD endpoints for managing API keys from the frontend Settings page
5
+ and the CLI.
6
+ """
7
+ from fastapi import APIRouter, HTTPException
8
+ from pydantic import BaseModel
9
+ from core.api_keys import generate_api_key, list_api_keys, delete_api_key
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ class CreateKeyRequest(BaseModel):
15
+ name: str = "Unnamed Key"
16
+
17
+
18
+ @router.get("/api/settings/api-keys")
19
+ async def list_keys():
20
+ """List all API keys (metadata only — no raw keys or hashes)."""
21
+ return list_api_keys()
22
+
23
+
24
+ @router.post("/api/settings/api-keys")
25
+ async def create_key(body: CreateKeyRequest):
26
+ """Generate a new API key.
27
+
28
+ The raw key is returned in this response ONLY — it is never stored
29
+ and cannot be retrieved again.
30
+ """
31
+ raw_key, record = generate_api_key(body.name)
32
+ return {
33
+ "key": raw_key, # shown once
34
+ "id": record["id"],
35
+ "name": record["name"],
36
+ "key_prefix": record["key_prefix"],
37
+ "created_at": record["created_at"],
38
+ }
39
+
40
+
41
+ @router.delete("/api/settings/api-keys/{key_id}")
42
+ async def remove_key(key_id: str):
43
+ """Delete an API key permanently."""
44
+ if delete_api_key(key_id):
45
+ return {"status": "deleted", "id": key_id}
46
+ raise HTTPException(status_code=404, detail="API key not found")