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.
- package/README.md +8 -0
- package/backend/core/agent_logger.py +18 -9
- package/backend/core/api_key_middleware.py +39 -0
- package/backend/core/api_keys.py +143 -0
- package/backend/core/config.py +39 -0
- package/backend/core/internal_auth.py +62 -0
- package/backend/core/models.py +3 -0
- package/backend/core/orchestration/engine.py +4 -3
- package/backend/core/routes/api_keys.py +46 -0
- package/backend/core/routes/api_v1.py +539 -0
- package/backend/core/routes/auth.py +43 -1
- package/backend/core/routes/settings.py +29 -2
- package/backend/core/server.py +10 -2
- package/backend/core/usage_tracker.py +3 -2
- package/backend/core/user_auth.py +48 -0
- package/backend/tools/code_search.py +46 -24
- package/frontend-build/.next/BUILD_ID +1 -1
- package/frontend-build/.next/app-path-routes-manifest.json +4 -0
- package/frontend-build/.next/build-manifest.json +3 -3
- package/frontend-build/.next/prerender-manifest.json +27 -3
- package/frontend-build/.next/required-server-files.json +2 -0
- package/frontend-build/.next/routes-manifest.json +24 -0
- package/frontend-build/.next/server/app/_global-error/page.js +1 -1
- package/frontend-build/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/frontend-build/.next/server/app/_global-error.html +1 -1
- package/frontend-build/.next/server/app/_global-error.rsc +1 -1
- package/frontend-build/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_not-found/page.js +1 -1
- package/frontend-build/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/frontend-build/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/frontend-build/.next/server/app/_not-found.html +1 -1
- package/frontend-build/.next/server/app/_not-found.rsc +2 -2
- package/frontend-build/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/frontend-build/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/frontend-build/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/frontend-build/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/frontend-build/.next/server/app/api/agent-types/route.js +1 -1
- package/frontend-build/.next/server/app/api/agent-types/route.js.nft.json +1 -1
- package/frontend-build/.next/server/app/api/agents/generate-prompt/route.js +1 -1
- package/frontend-build/.next/server/app/api/agents/generate-prompt/route.js.nft.json +1 -1
- package/frontend-build/.next/server/app/api/auth/login/route/app-paths-manifest.json +3 -0
- package/frontend-build/.next/server/app/api/auth/login/route/build-manifest.json +9 -0
- package/frontend-build/.next/server/app/api/auth/login/route/server-reference-manifest.json +4 -0
- package/frontend-build/.next/server/app/api/auth/login/route.js +6 -0
- package/frontend-build/.next/server/app/api/auth/login/route.js.map +5 -0
- package/frontend-build/.next/server/app/api/auth/login/route.js.nft.json +1 -0
- package/frontend-build/.next/server/app/api/auth/login/route_client-reference-manifest.js +3 -0
- package/frontend-build/.next/server/app/api/auth/logout/route/app-paths-manifest.json +3 -0
- package/frontend-build/.next/server/app/api/auth/logout/route/build-manifest.json +9 -0
- package/frontend-build/.next/server/app/api/auth/logout/route/server-reference-manifest.json +4 -0
- package/frontend-build/.next/server/app/api/auth/logout/route.js +6 -0
- package/frontend-build/.next/server/app/api/auth/logout/route.js.map +5 -0
- package/frontend-build/.next/server/app/api/auth/logout/route.js.nft.json +1 -0
- package/frontend-build/.next/server/app/api/auth/logout/route_client-reference-manifest.js +3 -0
- package/frontend-build/.next/server/app/api/auth/status/route/app-paths-manifest.json +3 -0
- package/frontend-build/.next/server/app/api/auth/status/route/build-manifest.json +9 -0
- package/frontend-build/.next/server/app/api/auth/status/route/server-reference-manifest.json +4 -0
- package/frontend-build/.next/server/app/api/auth/status/route.js +6 -0
- package/frontend-build/.next/server/app/api/auth/status/route.js.map +5 -0
- package/frontend-build/.next/server/app/api/auth/status/route.js.nft.json +1 -0
- package/frontend-build/.next/server/app/api/auth/status/route_client-reference-manifest.js +3 -0
- package/frontend-build/.next/server/app/api/builder/chat/route.js +1 -1
- package/frontend-build/.next/server/app/api/builder/chat/route.js.nft.json +1 -1
- package/frontend-build/.next/server/app/api/builder/resume/route.js +1 -1
- package/frontend-build/.next/server/app/api/builder/resume/route.js.nft.json +1 -1
- package/frontend-build/.next/server/app/api/chat/route.js +1 -1
- package/frontend-build/.next/server/app/api/chat/route.js.nft.json +1 -1
- package/frontend-build/.next/server/app/api/chat/stream/route.js +1 -1
- package/frontend-build/.next/server/app/api/chat/stream/route.js.nft.json +1 -1
- package/frontend-build/.next/server/app/api/logs/[type]/[run_id]/route.js +1 -1
- package/frontend-build/.next/server/app/api/logs/[type]/[run_id]/route.js.nft.json +1 -1
- package/frontend-build/.next/server/app/api/logs/[type]/route.js +1 -1
- package/frontend-build/.next/server/app/api/logs/[type]/route.js.nft.json +1 -1
- package/frontend-build/.next/server/app/api/models/route.js +1 -1
- package/frontend-build/.next/server/app/api/models/route.js.nft.json +1 -1
- package/frontend-build/.next/server/app/api/orchestrations/[orch_id]/run/route.js +1 -1
- package/frontend-build/.next/server/app/api/orchestrations/[orch_id]/run/route.js.nft.json +1 -1
- package/frontend-build/.next/server/app/api/orchestrations/runs/[run_id]/human-input/route.js +1 -1
- package/frontend-build/.next/server/app/api/orchestrations/runs/[run_id]/human-input/route.js.nft.json +1 -1
- package/frontend-build/.next/server/app/api/orchestrations/runs/[run_id]/resume/route.js +1 -1
- package/frontend-build/.next/server/app/api/orchestrations/runs/[run_id]/resume/route.js.nft.json +1 -1
- package/frontend-build/.next/server/app/api/schedules/[schedule_id]/route.js +1 -1
- package/frontend-build/.next/server/app/api/schedules/[schedule_id]/route.js.nft.json +1 -1
- package/frontend-build/.next/server/app/api/schedules/[schedule_id]/run/route.js +1 -1
- package/frontend-build/.next/server/app/api/schedules/[schedule_id]/run/route.js.nft.json +1 -1
- package/frontend-build/.next/server/app/api/schedules/route.js +1 -1
- package/frontend-build/.next/server/app/api/schedules/route.js.nft.json +1 -1
- package/frontend-build/.next/server/app/index.html +1 -1
- package/frontend-build/.next/server/app/index.rsc +3 -3
- package/frontend-build/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/frontend-build/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/frontend-build/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/frontend-build/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/frontend-build/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/frontend-build/.next/server/app/login/page/app-paths-manifest.json +3 -0
- package/frontend-build/.next/server/app/login/page/build-manifest.json +17 -0
- package/frontend-build/.next/server/app/login/page/next-font-manifest.json +14 -0
- package/frontend-build/.next/server/app/login/page/react-loadable-manifest.json +1 -0
- package/frontend-build/.next/server/app/login/page/server-reference-manifest.json +4 -0
- package/frontend-build/.next/server/app/login/page.js +14 -0
- package/frontend-build/.next/server/app/login/page.js.map +5 -0
- package/frontend-build/.next/server/app/login/page.js.nft.json +1 -0
- package/frontend-build/.next/server/app/login/page_client-reference-manifest.js +3 -0
- package/frontend-build/.next/server/app/login.html +1 -0
- package/frontend-build/.next/server/app/login.meta +15 -0
- package/frontend-build/.next/server/app/login.rsc +27 -0
- package/frontend-build/.next/server/app/login.segments/_full.segment.rsc +27 -0
- package/frontend-build/.next/server/app/login.segments/_head.segment.rsc +6 -0
- package/frontend-build/.next/server/app/login.segments/_index.segment.rsc +7 -0
- package/frontend-build/.next/server/app/login.segments/_tree.segment.rsc +8 -0
- package/frontend-build/.next/server/app/login.segments/login/__PAGE__.segment.rsc +9 -0
- package/frontend-build/.next/server/app/login.segments/login.segment.rsc +5 -0
- package/frontend-build/.next/server/app/page.js +1 -1
- package/frontend-build/.next/server/app/page.js.nft.json +1 -1
- package/frontend-build/.next/server/app/page_client-reference-manifest.js +1 -1
- package/frontend-build/.next/server/app/settings/[tab]/page.js +1 -1
- package/frontend-build/.next/server/app/settings/[tab]/page.js.nft.json +1 -1
- package/frontend-build/.next/server/app/settings/[tab]/page_client-reference-manifest.js +1 -1
- package/frontend-build/.next/server/app-paths-manifest.json +4 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__00ci3m0._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__01~.b7w._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__066njqe._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__08cioyi._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0df0b6v._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0efx8a5._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0j8-xkl._.js +1 -1
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0kk2o8d._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0kyyn1g._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0o2ckji._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0r1_rdp._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0s65g6m._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0u8_aw2._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0z5q5.1._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0~o635_._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__10p49e~._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__115~uq6._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__12ug7cf._.js +3 -0
- package/frontend-build/.next/server/chunks/[root-of-the-server]__12vs3wg._.js +3 -0
- package/frontend-build/.next/server/chunks/_next-internal_server_app_api_auth_login_route_actions_0zukc38.js +3 -0
- package/frontend-build/.next/server/chunks/_next-internal_server_app_api_auth_logout_route_actions_0fm~3ij.js +3 -0
- package/frontend-build/.next/server/chunks/_next-internal_server_app_api_auth_status_route_actions_0rmqb6l.js +3 -0
- package/frontend-build/.next/server/chunks/ssr/[root-of-the-server]__08l1kmh._.js +3 -0
- package/frontend-build/.next/server/chunks/ssr/[root-of-the-server]__09c9368._.js +3 -0
- package/frontend-build/.next/server/chunks/ssr/_0b~n.nn._.js +91 -19
- package/frontend-build/.next/server/chunks/ssr/_0rwng5.._.js +4 -0
- package/frontend-build/.next/server/chunks/ssr/_next-internal_server_app_login_page_actions_02kefem.js +3 -0
- package/frontend-build/.next/server/chunks/ssr/node_modules_0g2b~5_._.js +6 -0
- package/frontend-build/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_0__0o-2._.js +3 -0
- package/frontend-build/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0328vov.js +4 -0
- package/frontend-build/.next/server/chunks/ssr/src_app_login_page_tsx_0jt.sij._.js +3 -0
- package/frontend-build/.next/server/edge/chunks/[root-of-the-server]__08ca2jv._.js +11 -0
- package/frontend-build/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_0kvehva.js +3 -0
- package/frontend-build/.next/server/edge/chunks/turbopack-node_modules_next_dist_esm_build_templates_edge-wrapper_0lcpsxv.js +3 -0
- package/frontend-build/.next/server/middleware-build-manifest.js +3 -3
- package/frontend-build/.next/server/middleware-manifest.json +30 -2
- package/frontend-build/.next/server/next-font-manifest.js +1 -1
- package/frontend-build/.next/server/next-font-manifest.json +7 -0
- package/frontend-build/.next/server/pages/404.html +1 -1
- package/frontend-build/.next/server/pages/500.html +1 -1
- package/frontend-build/.next/server/server-reference-manifest.js +1 -1
- package/frontend-build/.next/server/server-reference-manifest.json +1 -1
- package/frontend-build/.next/static/N0fsCSqo5vmIgwJh-r_dk/_clientMiddlewareManifest.js +6 -0
- package/frontend-build/.next/static/chunks/0e0doloxq75td.js +1 -0
- package/frontend-build/.next/static/chunks/0k0-nxx35rq9v.js +1 -0
- package/frontend-build/.next/static/chunks/0w5f~4w31j03v.js +2 -0
- package/frontend-build/.next/static/chunks/0yq31mtv7c.bs.css +1 -0
- package/frontend-build/.next/static/chunks/18b0o~cnvc.gn.js +124 -0
- package/frontend-build/package.json +1 -0
- package/frontend-build/server.js +1 -1
- package/package.json +1 -1
- package/frontend-build/.next/server/chunks/[root-of-the-server]__00sljji._.js +0 -3
- package/frontend-build/.next/server/chunks/[root-of-the-server]__03e7r3a._.js +0 -3
- package/frontend-build/.next/server/chunks/[root-of-the-server]__053~3b.._.js +0 -3
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0afb7iz._.js +0 -3
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0bc5iuk._.js +0 -3
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0bygctj._.js +0 -3
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0c7o1w_._.js +0 -3
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0cy6bl4._.js +0 -3
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0dmb8p1._.js +0 -3
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0gx4j6x._.js +0 -3
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0h2-vsq._.js +0 -3
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0kkdqe3._.js +0 -3
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0n1p1jk._.js +0 -3
- package/frontend-build/.next/server/chunks/[root-of-the-server]__0xccig4._.js +0 -3
- package/frontend-build/.next/server/chunks/[root-of-the-server]__13dxl3m._.js +0 -3
- package/frontend-build/.next/server/chunks/ssr/[root-of-the-server]__0ssmzpx._.js +0 -3
- package/frontend-build/.next/server/chunks/ssr/_0ayz11y._.js +0 -4
- package/frontend-build/.next/server/chunks/ssr/node_modules_0zhhlrq._.js +0 -6
- package/frontend-build/.next/server/chunks/ssr/node_modules_1041ur2._.js +0 -6
- package/frontend-build/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_0ag5..o._.js +0 -3
- package/frontend-build/.next/static/chunks/0n53o._htd6sy.js +0 -2
- package/frontend-build/.next/static/chunks/0n_aip.6onjkv.js +0 -52
- package/frontend-build/.next/static/chunks/0or4d2ll~v9-c.css +0 -1
- package/frontend-build/.next/static/chunks/0vn.s1wic2p_8.js +0 -1
- package/frontend-build/.next/static/ijTAG225bIapjjOjjyPUj/_clientMiddlewareManifest.js +0 -1
- /package/frontend-build/.next/static/{ijTAG225bIapjjOjjyPUj → N0fsCSqo5vmIgwJh-r_dk}/_buildManifest.js +0 -0
- /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
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
self.
|
|
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
|
package/backend/core/config.py
CHANGED
|
@@ -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)
|
package/backend/core/models.py
CHANGED
|
@@ -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":
|
|
248
|
+
"error": safe_error,
|
|
248
249
|
})
|
|
249
250
|
run.status = "failed"
|
|
250
251
|
if logger:
|
|
251
|
-
logger.step_end(step.id, "failed",
|
|
252
|
-
yield {"type": "step_error", "orch_step_id": step.id, "error":
|
|
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")
|