synapse-orch-ai 1.3.4 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/requirements-coding.txt +1 -1
- 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/Yru6SbqHLpZ5TlStu7jpG/_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/B6NiRZeoBRJSm9jT1wyru/_clientMiddlewareManifest.js +0 -1
- 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/{B6NiRZeoBRJSm9jT1wyru → Yru6SbqHLpZ5TlStu7jpG}/_buildManifest.js +0 -0
- /package/frontend-build/.next/static/{B6NiRZeoBRJSm9jT1wyru → Yru6SbqHLpZ5TlStu7jpG}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
"""
|
|
2
|
+
V1 External API Endpoints
|
|
3
|
+
--------------------------
|
|
4
|
+
Programmatic API for external apps to interact with Synapse agents
|
|
5
|
+
and orchestrations. All routes are protected by API key auth (Bearer token).
|
|
6
|
+
|
|
7
|
+
Endpoints:
|
|
8
|
+
POST /chat — Sync chat (returns only final response)
|
|
9
|
+
POST /chat/stream — SSE chat (all events)
|
|
10
|
+
POST /orchestrations/{orch_id}/run — Start orchestration (sync)
|
|
11
|
+
POST /orchestrations/{orch_id}/run/stream — Start orchestration (SSE)
|
|
12
|
+
POST /orchestrations/runs/{run_id}/resume — Resume after human input (sync)
|
|
13
|
+
POST /orchestrations/runs/{run_id}/resume/stream — Resume after human input (SSE)
|
|
14
|
+
"""
|
|
15
|
+
import asyncio
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import time
|
|
19
|
+
import uuid
|
|
20
|
+
|
|
21
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
22
|
+
from fastapi.responses import StreamingResponse
|
|
23
|
+
from pydantic import BaseModel
|
|
24
|
+
|
|
25
|
+
from core.api_key_middleware import require_api_key
|
|
26
|
+
|
|
27
|
+
router = APIRouter()
|
|
28
|
+
log = logging.getLogger("api_v1")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ── Request / Response Models ────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
class V1ChatRequest(BaseModel):
|
|
34
|
+
message: str
|
|
35
|
+
agent: str | None = None # agent name or ID (optional — first agent if omitted)
|
|
36
|
+
session_id: str | None = None # for conversation continuity (auto-generated if omitted)
|
|
37
|
+
images: list[str] = [] # optional base64 images
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class V1OrchestrationRunRequest(BaseModel):
|
|
41
|
+
message: str = ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class V1ResumeRequest(BaseModel):
|
|
45
|
+
response: dict | str = {} # human input fields
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── Helpers ──────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
def _resolve_agent_for_api(agent_identifier: str | None) -> dict | None:
|
|
51
|
+
"""Find agent by name (case-insensitive) or ID. Falls back to first agent."""
|
|
52
|
+
from core.routes.agents import load_user_agents
|
|
53
|
+
agents = load_user_agents()
|
|
54
|
+
if not agents:
|
|
55
|
+
return None
|
|
56
|
+
if not agent_identifier:
|
|
57
|
+
return agents[0]
|
|
58
|
+
# Exact ID match
|
|
59
|
+
by_id = next((a for a in agents if a["id"] == agent_identifier), None)
|
|
60
|
+
if by_id:
|
|
61
|
+
return by_id
|
|
62
|
+
# Case-insensitive name match
|
|
63
|
+
lower = agent_identifier.lower()
|
|
64
|
+
by_name = next((a for a in agents if a["name"].lower() == lower), None)
|
|
65
|
+
if by_name:
|
|
66
|
+
return by_name
|
|
67
|
+
# Partial name match
|
|
68
|
+
partial = next((a for a in agents if lower in a["name"].lower()), None)
|
|
69
|
+
if partial:
|
|
70
|
+
return partial
|
|
71
|
+
# Fallback to first agent
|
|
72
|
+
return agents[0]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _build_chat_request(body: V1ChatRequest, agent: dict) -> "ChatRequest":
|
|
76
|
+
"""Convert V1ChatRequest into the internal ChatRequest model."""
|
|
77
|
+
from core.models import ChatRequest
|
|
78
|
+
session_id = body.session_id or f"api_{uuid.uuid4().hex[:12]}"
|
|
79
|
+
return ChatRequest(
|
|
80
|
+
message=body.message,
|
|
81
|
+
session_id=session_id,
|
|
82
|
+
agent_id=agent["id"],
|
|
83
|
+
images=body.images,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _format_sse_event(event: dict) -> str:
|
|
88
|
+
"""Format an event dict as an SSE data line."""
|
|
89
|
+
return f"data: {json.dumps(event, default=str)}\n\n"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ── Chat Endpoints ──────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
@router.post("/chat")
|
|
95
|
+
async def v1_chat(body: V1ChatRequest, key_record: dict = Depends(require_api_key)):
|
|
96
|
+
"""Synchronous chat — returns only the final response."""
|
|
97
|
+
agent = _resolve_agent_for_api(body.agent)
|
|
98
|
+
if not agent:
|
|
99
|
+
raise HTTPException(status_code=400, detail="No agents configured")
|
|
100
|
+
|
|
101
|
+
import core.server as _server
|
|
102
|
+
if not _server.agent_sessions:
|
|
103
|
+
raise HTTPException(status_code=503, detail="No agent sessions available. Server may still be starting.")
|
|
104
|
+
|
|
105
|
+
chat_request = _build_chat_request(body, agent)
|
|
106
|
+
from core.react_engine import run_react_loop
|
|
107
|
+
|
|
108
|
+
final_event = None
|
|
109
|
+
error_msg = None
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
async for event in run_react_loop(chat_request, _server):
|
|
113
|
+
etype = event.get("type", "")
|
|
114
|
+
if etype == "final":
|
|
115
|
+
final_event = event
|
|
116
|
+
elif etype == "error":
|
|
117
|
+
error_msg = event.get("message", "Unknown error")
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
log.exception("[v1/chat] Unhandled error for session=%s", chat_request.session_id)
|
|
120
|
+
raise HTTPException(status_code=500, detail="An internal error occurred. Check server logs for details.")
|
|
121
|
+
|
|
122
|
+
if error_msg:
|
|
123
|
+
log.error("[v1/chat] Agent error for session=%s: %s", chat_request.session_id, error_msg)
|
|
124
|
+
raise HTTPException(status_code=500, detail="The agent encountered an error processing your request.")
|
|
125
|
+
|
|
126
|
+
response_text = "I completed the requested actions."
|
|
127
|
+
if final_event:
|
|
128
|
+
response_text = final_event.get("response", response_text)
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
"response": response_text,
|
|
132
|
+
"agent_id": agent["id"],
|
|
133
|
+
"agent_name": agent["name"],
|
|
134
|
+
"session_id": chat_request.session_id,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@router.post("/chat/stream")
|
|
139
|
+
async def v1_chat_stream(body: V1ChatRequest, key_record: dict = Depends(require_api_key)):
|
|
140
|
+
"""SSE streaming chat — returns all events."""
|
|
141
|
+
agent = _resolve_agent_for_api(body.agent)
|
|
142
|
+
if not agent:
|
|
143
|
+
raise HTTPException(status_code=400, detail="No agents configured")
|
|
144
|
+
|
|
145
|
+
import core.server as _server
|
|
146
|
+
if not _server.agent_sessions:
|
|
147
|
+
raise HTTPException(status_code=503, detail="No agent sessions available. Server may still be starting.")
|
|
148
|
+
|
|
149
|
+
chat_request = _build_chat_request(body, agent)
|
|
150
|
+
|
|
151
|
+
async def event_generator():
|
|
152
|
+
from core.react_engine import run_react_loop
|
|
153
|
+
try:
|
|
154
|
+
# Emit session info first so the consumer can capture the session_id
|
|
155
|
+
yield _format_sse_event({
|
|
156
|
+
"type": "session",
|
|
157
|
+
"session_id": chat_request.session_id,
|
|
158
|
+
"agent_id": agent["id"],
|
|
159
|
+
"agent_name": agent["name"],
|
|
160
|
+
})
|
|
161
|
+
async for event in run_react_loop(chat_request, _server):
|
|
162
|
+
etype = event.get("type", "")
|
|
163
|
+
|
|
164
|
+
if etype == "status":
|
|
165
|
+
yield _format_sse_event({"type": "status", "message": event["message"]})
|
|
166
|
+
|
|
167
|
+
elif etype == "thinking":
|
|
168
|
+
yield _format_sse_event({
|
|
169
|
+
"type": "thinking",
|
|
170
|
+
"message": event.get("message", ""),
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
elif etype == "tool_execution":
|
|
174
|
+
yield _format_sse_event({
|
|
175
|
+
"type": "tool_execution",
|
|
176
|
+
"tool_name": event["tool_name"],
|
|
177
|
+
"args": event["args"],
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
elif etype == "tool_result":
|
|
181
|
+
yield _format_sse_event({
|
|
182
|
+
"type": "tool_result",
|
|
183
|
+
"tool_name": event["tool_name"],
|
|
184
|
+
"preview": event["preview"],
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
elif etype == "llm_thought":
|
|
188
|
+
yield _format_sse_event({
|
|
189
|
+
"type": "llm_thought",
|
|
190
|
+
"thought": event["thought"],
|
|
191
|
+
"turn": event.get("turn", 1),
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
elif etype == "final":
|
|
195
|
+
yield _format_sse_event({
|
|
196
|
+
"type": "response",
|
|
197
|
+
"content": event.get("response", ""),
|
|
198
|
+
"intent": event.get("intent", "chat"),
|
|
199
|
+
"session_id": chat_request.session_id,
|
|
200
|
+
})
|
|
201
|
+
yield _format_sse_event({"type": "done"})
|
|
202
|
+
|
|
203
|
+
elif etype == "error":
|
|
204
|
+
log.error("[v1/chat/stream] Agent error session=%s: %s", chat_request.session_id, event.get("message"))
|
|
205
|
+
yield _format_sse_event({"type": "error", "message": "The agent encountered an error processing your request."})
|
|
206
|
+
|
|
207
|
+
await asyncio.sleep(0)
|
|
208
|
+
|
|
209
|
+
except Exception as e:
|
|
210
|
+
log.exception("[v1/chat/stream] Unhandled error session=%s", chat_request.session_id)
|
|
211
|
+
yield _format_sse_event({"type": "error", "message": "An internal error occurred. Check server logs for details."})
|
|
212
|
+
|
|
213
|
+
return StreamingResponse(
|
|
214
|
+
event_generator(),
|
|
215
|
+
media_type="text/event-stream",
|
|
216
|
+
headers={
|
|
217
|
+
"Cache-Control": "no-cache",
|
|
218
|
+
"Connection": "keep-alive",
|
|
219
|
+
"X-Accel-Buffering": "no",
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ── Orchestration Endpoints ─────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
@router.post("/orchestrations/{orch_id}/run")
|
|
227
|
+
async def v1_orchestration_run(
|
|
228
|
+
orch_id: str,
|
|
229
|
+
body: V1OrchestrationRunRequest,
|
|
230
|
+
key_record: dict = Depends(require_api_key),
|
|
231
|
+
):
|
|
232
|
+
"""Start an orchestration (sync). Returns final result or human_input_required."""
|
|
233
|
+
import core.server as _server
|
|
234
|
+
from core.routes.orchestrations import load_orchestrations
|
|
235
|
+
from core.models_orchestration import Orchestration
|
|
236
|
+
from core.orchestration.engine import OrchestrationEngine
|
|
237
|
+
|
|
238
|
+
orchs = load_orchestrations()
|
|
239
|
+
orch_data = next((o for o in orchs if o["id"] == orch_id), None)
|
|
240
|
+
if not orch_data:
|
|
241
|
+
raise HTTPException(status_code=404, detail=f"Orchestration '{orch_id}' not found")
|
|
242
|
+
|
|
243
|
+
run_id = f"run_{orch_id}_{int(time.time() * 1000)}"
|
|
244
|
+
orch = Orchestration.model_validate(orch_data)
|
|
245
|
+
engine = OrchestrationEngine(orch, _server)
|
|
246
|
+
|
|
247
|
+
final_response = None
|
|
248
|
+
human_input_event = None
|
|
249
|
+
step_history = []
|
|
250
|
+
shared_state = {}
|
|
251
|
+
status = "running"
|
|
252
|
+
|
|
253
|
+
async for event in engine.run(body.message, run_id):
|
|
254
|
+
etype = event.get("type", "")
|
|
255
|
+
|
|
256
|
+
if etype == "human_input_required":
|
|
257
|
+
human_input_event = event
|
|
258
|
+
status = "paused"
|
|
259
|
+
break
|
|
260
|
+
|
|
261
|
+
if etype == "orchestration_complete":
|
|
262
|
+
status = event.get("status", "completed")
|
|
263
|
+
shared_state = event.get("final_state", {})
|
|
264
|
+
|
|
265
|
+
if etype == "final":
|
|
266
|
+
final_response = event.get("response", "")
|
|
267
|
+
step_history = event.get("data", {}).get("step_history", []) if event.get("data") else []
|
|
268
|
+
|
|
269
|
+
if human_input_event:
|
|
270
|
+
return {
|
|
271
|
+
"status": "paused",
|
|
272
|
+
"run_id": run_id,
|
|
273
|
+
"human_input_required": {
|
|
274
|
+
"step_id": human_input_event.get("orch_step_id"),
|
|
275
|
+
"prompt": human_input_event.get("prompt"),
|
|
276
|
+
"fields": human_input_event.get("fields", []),
|
|
277
|
+
"agent_context": human_input_event.get("agent_context"),
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
"status": status,
|
|
283
|
+
"run_id": run_id,
|
|
284
|
+
"response": final_response or f"Orchestration {status}.",
|
|
285
|
+
"shared_state": shared_state,
|
|
286
|
+
"step_history": step_history,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@router.post("/orchestrations/{orch_id}/run/stream")
|
|
291
|
+
async def v1_orchestration_run_stream(
|
|
292
|
+
orch_id: str,
|
|
293
|
+
body: V1OrchestrationRunRequest,
|
|
294
|
+
key_record: dict = Depends(require_api_key),
|
|
295
|
+
):
|
|
296
|
+
"""Start an orchestration (SSE stream)."""
|
|
297
|
+
import core.server as _server
|
|
298
|
+
from core.routes.orchestrations import load_orchestrations
|
|
299
|
+
from core.models_orchestration import Orchestration
|
|
300
|
+
from core.orchestration.engine import OrchestrationEngine
|
|
301
|
+
|
|
302
|
+
orchs = load_orchestrations()
|
|
303
|
+
orch_data = next((o for o in orchs if o["id"] == orch_id), None)
|
|
304
|
+
if not orch_data:
|
|
305
|
+
raise HTTPException(status_code=404, detail=f"Orchestration '{orch_id}' not found")
|
|
306
|
+
|
|
307
|
+
run_id = f"run_{orch_id}_{int(time.time() * 1000)}"
|
|
308
|
+
orch = Orchestration.model_validate(orch_data)
|
|
309
|
+
engine = OrchestrationEngine(orch, _server)
|
|
310
|
+
|
|
311
|
+
async def event_stream():
|
|
312
|
+
try:
|
|
313
|
+
async for event in engine.run(body.message, run_id):
|
|
314
|
+
etype = event.get("type", "")
|
|
315
|
+
|
|
316
|
+
# Skip internal log events
|
|
317
|
+
if etype.startswith("_log_"):
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
yield _format_sse_event(event)
|
|
321
|
+
|
|
322
|
+
if etype == "human_input_required":
|
|
323
|
+
yield _format_sse_event({"type": "done"})
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
await asyncio.sleep(0)
|
|
327
|
+
except Exception as e:
|
|
328
|
+
log.exception("[v1/orch/stream] Unhandled error run_id=%s", run_id)
|
|
329
|
+
yield _format_sse_event({"type": "orchestration_error", "error": "An internal error occurred. Check server logs for details."})
|
|
330
|
+
|
|
331
|
+
yield _format_sse_event({"type": "done"})
|
|
332
|
+
|
|
333
|
+
return StreamingResponse(
|
|
334
|
+
event_stream(),
|
|
335
|
+
media_type="text/event-stream",
|
|
336
|
+
headers={
|
|
337
|
+
"Cache-Control": "no-cache",
|
|
338
|
+
"Connection": "keep-alive",
|
|
339
|
+
"X-Accel-Buffering": "no",
|
|
340
|
+
},
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ── Orchestration Resume Endpoints ──────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
@router.post("/orchestrations/runs/{run_id}/resume")
|
|
347
|
+
async def v1_orchestration_resume(
|
|
348
|
+
run_id: str,
|
|
349
|
+
body: V1ResumeRequest,
|
|
350
|
+
key_record: dict = Depends(require_api_key),
|
|
351
|
+
):
|
|
352
|
+
"""Submit human input and resume orchestration (sync)."""
|
|
353
|
+
import core.server as _server
|
|
354
|
+
from core.orchestration.engine import OrchestrationEngine
|
|
355
|
+
|
|
356
|
+
# Normalize response to dict
|
|
357
|
+
human_response = body.response
|
|
358
|
+
if isinstance(human_response, str):
|
|
359
|
+
human_response = {"response": human_response}
|
|
360
|
+
|
|
361
|
+
final_response = None
|
|
362
|
+
human_input_event = None
|
|
363
|
+
step_history = []
|
|
364
|
+
shared_state = {}
|
|
365
|
+
status = "running"
|
|
366
|
+
_allowed_statuses = {"running", "paused", "completed", "failed"}
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
async for event in OrchestrationEngine.resume(run_id, human_response, _server):
|
|
370
|
+
etype = event.get("type", "")
|
|
371
|
+
|
|
372
|
+
if etype == "human_input_required":
|
|
373
|
+
human_input_event = event
|
|
374
|
+
status = "paused"
|
|
375
|
+
break
|
|
376
|
+
|
|
377
|
+
if etype == "orchestration_complete":
|
|
378
|
+
_raw_status = event.get("status", "completed")
|
|
379
|
+
status = _raw_status if _raw_status in _allowed_statuses else "completed"
|
|
380
|
+
shared_state = event.get("final_state", {})
|
|
381
|
+
|
|
382
|
+
if etype == "final":
|
|
383
|
+
final_response = event.get("response", "")
|
|
384
|
+
step_history = event.get("data", {}).get("step_history", []) if event.get("data") else []
|
|
385
|
+
|
|
386
|
+
except FileNotFoundError:
|
|
387
|
+
raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
|
|
388
|
+
except Exception as exc:
|
|
389
|
+
log.exception("[v1/resume] Unhandled error run_id=%s", run_id)
|
|
390
|
+
raise HTTPException(status_code=500, detail="An internal error occurred. Check server logs for details.")
|
|
391
|
+
|
|
392
|
+
if human_input_event:
|
|
393
|
+
return {
|
|
394
|
+
"status": "paused",
|
|
395
|
+
"run_id": run_id,
|
|
396
|
+
"human_input_required": {
|
|
397
|
+
"step_id": human_input_event.get("orch_step_id"),
|
|
398
|
+
"prompt": human_input_event.get("prompt"),
|
|
399
|
+
"fields": human_input_event.get("fields", []),
|
|
400
|
+
"agent_context": human_input_event.get("agent_context"),
|
|
401
|
+
},
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
"status": status,
|
|
406
|
+
"run_id": run_id,
|
|
407
|
+
"response": final_response or f"Orchestration {status}.",
|
|
408
|
+
"shared_state": shared_state,
|
|
409
|
+
"step_history": step_history,
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@router.post("/orchestrations/runs/{run_id}/resume/stream")
|
|
414
|
+
async def v1_orchestration_resume_stream(
|
|
415
|
+
run_id: str,
|
|
416
|
+
body: V1ResumeRequest,
|
|
417
|
+
key_record: dict = Depends(require_api_key),
|
|
418
|
+
):
|
|
419
|
+
"""Submit human input and resume orchestration (SSE stream)."""
|
|
420
|
+
import core.server as _server
|
|
421
|
+
from core.orchestration.engine import OrchestrationEngine
|
|
422
|
+
|
|
423
|
+
# Normalize response to dict
|
|
424
|
+
human_response = body.response
|
|
425
|
+
if isinstance(human_response, str):
|
|
426
|
+
human_response = {"response": human_response}
|
|
427
|
+
|
|
428
|
+
async def event_stream():
|
|
429
|
+
try:
|
|
430
|
+
async for event in OrchestrationEngine.resume(run_id, human_response, _server):
|
|
431
|
+
etype = event.get("type", "")
|
|
432
|
+
|
|
433
|
+
if etype.startswith("_log_"):
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
yield _format_sse_event(event)
|
|
437
|
+
|
|
438
|
+
if etype == "human_input_required":
|
|
439
|
+
yield _format_sse_event({"type": "done"})
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
await asyncio.sleep(0)
|
|
443
|
+
except FileNotFoundError:
|
|
444
|
+
yield _format_sse_event({"type": "orchestration_error", "error": f"Run '{run_id}' not found"})
|
|
445
|
+
except Exception as e:
|
|
446
|
+
log.exception("[v1/resume/stream] Unhandled error run_id=%s", run_id)
|
|
447
|
+
yield _format_sse_event({"type": "orchestration_error", "error": "An internal error occurred. Check server logs for details."})
|
|
448
|
+
|
|
449
|
+
yield _format_sse_event({"type": "done"})
|
|
450
|
+
|
|
451
|
+
return StreamingResponse(
|
|
452
|
+
event_stream(),
|
|
453
|
+
media_type="text/event-stream",
|
|
454
|
+
headers={
|
|
455
|
+
"Cache-Control": "no-cache",
|
|
456
|
+
"Connection": "keep-alive",
|
|
457
|
+
"X-Accel-Buffering": "no",
|
|
458
|
+
},
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
# ── Read-Only Discovery Endpoints ──────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
@router.get("/agents")
|
|
465
|
+
async def v1_list_agents(key_record: dict = Depends(require_api_key)):
|
|
466
|
+
"""List all configured agents (id, name, type, capabilities)."""
|
|
467
|
+
from core.routes.agents import load_user_agents
|
|
468
|
+
agents = load_user_agents()
|
|
469
|
+
return [
|
|
470
|
+
{
|
|
471
|
+
"id": a["id"],
|
|
472
|
+
"name": a["name"],
|
|
473
|
+
"type": a.get("type", "conversational"),
|
|
474
|
+
"model": a.get("model", ""),
|
|
475
|
+
"capabilities": a.get("capabilities", []),
|
|
476
|
+
}
|
|
477
|
+
for a in agents
|
|
478
|
+
]
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@router.get("/agents/{agent_id}")
|
|
482
|
+
async def v1_get_agent(agent_id: str, key_record: dict = Depends(require_api_key)):
|
|
483
|
+
"""Get details for a specific agent."""
|
|
484
|
+
from core.routes.agents import load_user_agents
|
|
485
|
+
agents = load_user_agents()
|
|
486
|
+
agent = next((a for a in agents if a["id"] == agent_id), None)
|
|
487
|
+
if not agent:
|
|
488
|
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
|
|
489
|
+
return {
|
|
490
|
+
"id": agent["id"],
|
|
491
|
+
"name": agent["name"],
|
|
492
|
+
"type": agent.get("type", "conversational"),
|
|
493
|
+
"model": agent.get("model", ""),
|
|
494
|
+
"capabilities": agent.get("capabilities", []),
|
|
495
|
+
"description": agent.get("description", ""),
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
@router.get("/orchestrations")
|
|
500
|
+
async def v1_list_orchestrations(key_record: dict = Depends(require_api_key)):
|
|
501
|
+
"""List all configured orchestrations (id, name, steps summary)."""
|
|
502
|
+
from core.routes.orchestrations import load_orchestrations
|
|
503
|
+
orchs = load_orchestrations()
|
|
504
|
+
return [
|
|
505
|
+
{
|
|
506
|
+
"id": o["id"],
|
|
507
|
+
"name": o.get("name", ""),
|
|
508
|
+
"description": o.get("description", ""),
|
|
509
|
+
"steps": len(o.get("steps", [])),
|
|
510
|
+
}
|
|
511
|
+
for o in orchs
|
|
512
|
+
]
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
@router.get("/orchestrations/{orch_id}")
|
|
516
|
+
async def v1_get_orchestration(orch_id: str, key_record: dict = Depends(require_api_key)):
|
|
517
|
+
"""Get details for a specific orchestration including step definitions."""
|
|
518
|
+
from core.routes.orchestrations import load_orchestrations
|
|
519
|
+
orchs = load_orchestrations()
|
|
520
|
+
orch = next((o for o in orchs if o["id"] == orch_id), None)
|
|
521
|
+
if not orch:
|
|
522
|
+
raise HTTPException(status_code=404, detail=f"Orchestration '{orch_id}' not found")
|
|
523
|
+
return {
|
|
524
|
+
"id": orch["id"],
|
|
525
|
+
"name": orch.get("name", ""),
|
|
526
|
+
"description": orch.get("description", ""),
|
|
527
|
+
"steps": [
|
|
528
|
+
{
|
|
529
|
+
"id": s.get("id", ""),
|
|
530
|
+
"label": s.get("label", ""),
|
|
531
|
+
"type": s.get("type", ""),
|
|
532
|
+
"agent_id": s.get("agent_id", ""),
|
|
533
|
+
"depends_on": s.get("depends_on", []),
|
|
534
|
+
}
|
|
535
|
+
for s in orch.get("steps", [])
|
|
536
|
+
],
|
|
537
|
+
"edges": orch.get("edges", []),
|
|
538
|
+
}
|
|
539
|
+
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Authentication routes
|
|
2
|
+
Authentication routes: Google OAuth + Synapse login gate.
|
|
3
3
|
"""
|
|
4
4
|
import os
|
|
5
5
|
from fastapi import APIRouter, HTTPException
|
|
6
6
|
from fastapi.responses import RedirectResponse, JSONResponse
|
|
7
|
+
from pydantic import BaseModel
|
|
7
8
|
from services.google import get_auth_url, finish_auth
|
|
9
|
+
from core.config import load_settings
|
|
10
|
+
from core.user_auth import verify_password, create_session_token
|
|
8
11
|
|
|
9
12
|
router = APIRouter()
|
|
10
13
|
|
|
@@ -28,6 +31,45 @@ async def login():
|
|
|
28
31
|
raise HTTPException(status_code=500, detail=str(e))
|
|
29
32
|
|
|
30
33
|
|
|
34
|
+
class LoginRequest(BaseModel):
|
|
35
|
+
username: str
|
|
36
|
+
password: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@router.get("/api/auth/status")
|
|
40
|
+
async def auth_status():
|
|
41
|
+
"""Returns whether login is enabled and fully configured."""
|
|
42
|
+
s = load_settings()
|
|
43
|
+
login_enabled = s.get("login_enabled", False)
|
|
44
|
+
login_configured = bool(
|
|
45
|
+
login_enabled
|
|
46
|
+
and s.get("login_username")
|
|
47
|
+
and s.get("login_password_hash")
|
|
48
|
+
)
|
|
49
|
+
return {"login_enabled": login_enabled, "login_configured": login_configured}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@router.post("/api/auth/login")
|
|
53
|
+
async def user_login(body: LoginRequest):
|
|
54
|
+
"""Validate username/password and return a signed JWT on success."""
|
|
55
|
+
s = load_settings()
|
|
56
|
+
if not s.get("login_enabled"):
|
|
57
|
+
return {"success": True, "token": None}
|
|
58
|
+
stored_username = s.get("login_username", "")
|
|
59
|
+
stored_hash = s.get("login_password_hash", "")
|
|
60
|
+
if not (stored_username and stored_hash):
|
|
61
|
+
raise HTTPException(status_code=500, detail="Login is enabled but credentials are not configured")
|
|
62
|
+
if body.username != stored_username or not verify_password(body.password, stored_hash):
|
|
63
|
+
raise HTTPException(status_code=401, detail="Invalid username or password")
|
|
64
|
+
return {"success": True, "token": create_session_token(body.username)}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@router.post("/api/auth/logout")
|
|
68
|
+
async def user_logout():
|
|
69
|
+
"""Stateless logout — cookie cleared by the Next.js route handler."""
|
|
70
|
+
return {"success": True}
|
|
71
|
+
|
|
72
|
+
|
|
31
73
|
@router.get("/auth/callback")
|
|
32
74
|
async def callback(code: str, state: str = None):
|
|
33
75
|
try:
|
|
@@ -28,6 +28,12 @@ class EmbedSetupRequest(BaseModel):
|
|
|
28
28
|
password: str = ""
|
|
29
29
|
db_name: str = "synapse"
|
|
30
30
|
|
|
31
|
+
|
|
32
|
+
class LoginConfigRequest(BaseModel):
|
|
33
|
+
login_enabled: bool
|
|
34
|
+
login_username: str = ""
|
|
35
|
+
login_password: str = ""
|
|
36
|
+
|
|
31
37
|
# Path to the examples directory (sibling of this file's package root)
|
|
32
38
|
_EXAMPLES_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "examples")
|
|
33
39
|
|
|
@@ -87,19 +93,40 @@ async def get_status():
|
|
|
87
93
|
@router.get("/api/settings")
|
|
88
94
|
async def get_settings():
|
|
89
95
|
settings = load_settings()
|
|
96
|
+
settings.pop("login_password_hash", None)
|
|
90
97
|
return settings
|
|
91
98
|
|
|
92
99
|
|
|
100
|
+
@router.post("/api/settings/login")
|
|
101
|
+
async def update_login_settings(body: LoginConfigRequest):
|
|
102
|
+
from core.user_auth import hash_password
|
|
103
|
+
existing = load_settings()
|
|
104
|
+
existing["login_enabled"] = body.login_enabled
|
|
105
|
+
if body.login_enabled:
|
|
106
|
+
if not body.login_username.strip():
|
|
107
|
+
raise HTTPException(status_code=400, detail="Username is required when login is enabled")
|
|
108
|
+
existing["login_username"] = body.login_username.strip()
|
|
109
|
+
if body.login_password:
|
|
110
|
+
existing["login_password_hash"] = hash_password(body.login_password)
|
|
111
|
+
elif not existing.get("login_password_hash"):
|
|
112
|
+
raise HTTPException(status_code=400, detail="Password is required for first-time login setup")
|
|
113
|
+
save_settings(existing)
|
|
114
|
+
return {"status": "ok", "login_enabled": body.login_enabled}
|
|
115
|
+
|
|
116
|
+
|
|
93
117
|
@router.post("/api/settings")
|
|
94
118
|
async def update_settings(settings: Settings):
|
|
95
|
-
|
|
119
|
+
_safe = {k: (v[:4] + '…' + v[-4:] if isinstance(v, str) and len(v) > 12 and any(kw in k for kw in ('key', 'token', 'secret', 'password')) else v) for k, v in settings.dict(exclude_unset=True).items()}
|
|
120
|
+
print(f"DEBUG: update_settings called with keys: {list(_safe.keys())}")
|
|
96
121
|
# Get the latest payload and strip unset values to avoid overwriting existing properties with defaults
|
|
97
122
|
try:
|
|
98
123
|
data = settings.dict(exclude_unset=True)
|
|
99
124
|
except Exception:
|
|
100
125
|
data = settings.dict()
|
|
101
|
-
|
|
126
|
+
|
|
102
127
|
existing = load_settings()
|
|
128
|
+
# Never overwrite the bcrypt hash via the general settings endpoint
|
|
129
|
+
data.pop("login_password_hash", None)
|
|
103
130
|
existing.update(data)
|
|
104
131
|
data = existing
|
|
105
132
|
|