[Haik]: ckpt, fixed nine router inter dependency issues with local and prod sharing ports. Also centralized all port handling to a ports.config.json in the root dir

This commit is contained in:
haikdc
2026-03-30 07:56:15 -07:00
parent 95555df667
commit eb792617a5
22 changed files with 139 additions and 59 deletions
+34 -21
View File
@@ -55,31 +55,44 @@ function CallbackContent() {
}
// Method 4: Direct exchange via OpenSwarm backend (works even when postMessage fails)
// Fetch pending OAuth data from OpenSwarm, then call 9Router's exchange endpoint
// The backend port is dynamic (8325 dev, 8324+ prod), so try known ports and
// use whichever has a matching pending state entry.
if (code && state) {
(async () => {
try {
const pendingRes = await fetch(`http://localhost:8324/api/subscriptions/pending/${encodeURIComponent(state)}`);
if (pendingRes.ok) {
const pending = await pendingRes.json();
if (pending.provider && pending.code_verifier) {
const exchangeRes = await fetch(`/api/oauth/${pending.provider}/exchange`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
code,
redirectUri: pending.redirect_uri,
codeVerifier: pending.code_verifier,
state,
}),
});
if (exchangeRes.ok) {
console.log("Direct exchange successful");
}
const portsToTry = [8325, 8324];
let pending = null;
for (const port of portsToTry) {
try {
const res = await fetch(
`http://localhost:${port}/api/subscriptions/pending/${encodeURIComponent(state)}`,
{ signal: AbortSignal.timeout(1000) },
);
if (res.ok) {
pending = await res.json();
break;
}
} catch {
/* port not reachable, try next */
}
}
if (pending?.provider && pending?.code_verifier) {
try {
const exchangeRes = await fetch(`/api/oauth/${pending.provider}/exchange`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
code,
redirectUri: pending.redirect_uri,
codeVerifier: pending.code_verifier,
state,
}),
});
if (exchangeRes.ok) {
console.log("Direct exchange successful");
}
} catch (e) {
console.log("Direct exchange failed:", e);
}
} catch (e) {
console.log("Direct exchange fallback failed:", e);
}
})();
}
+9 -9
View File
@@ -52,7 +52,7 @@ For other integrations, edit `backend/.env`:
| Variable | Purpose |
|----------|---------|
| `BACKEND_PORT` | Backend server port (default: `8324`) |
| `BACKEND_PORT` | Backend server port (default: `8325` for dev; prod uses `8324`) |
| `GOOGLE_OAUTH_CLIENT_ID` | Google Workspace integration (Gmail, Calendar, Drive) |
| `GOOGLE_OAUTH_CLIENT_SECRET` | Google Workspace integration |
| `APPLE_ID` | macOS code signing & notarization (release builds only) |
@@ -70,14 +70,14 @@ For other integrations, edit `backend/.env`:
bash run/local.sh
```
This starts the backend (port 8324), frontend (port 3000), and Electron shell together. The script handles virtual environments and dependency installation automatically.
This starts the backend (port 8325), frontend (port 3000), and Electron shell together. The script handles virtual environments and dependency installation automatically.
### Option B: Run services individually
**Backend** (in one terminal):
```bash
bash backend/run.sh # API at http://localhost:8324 — docs at /docs
bash backend/run.sh # API at http://localhost:8325 — docs at /docs
```
**Frontend** (in another terminal):
@@ -94,7 +94,7 @@ bash frontend/run.sh # App at http://localhost:3000
cd backend
source .venv/bin/activate
cd ..
python -m uvicorn backend.main:app --host 0.0.0.0 --port 8324 --reload --reload-dir backend
python -m uvicorn backend.main:app --host 0.0.0.0 --port 8325 --reload --reload-dir backend
```
**Terminal 2 — Frontend dev server:**
@@ -113,8 +113,8 @@ Once everything is running:
| Service | URL |
|---------|-----|
| **Frontend (UI)** | [http://localhost:3000](http://localhost:3000) |
| **Backend API** | [http://localhost:8324](http://localhost:8324) |
| **API Docs (Swagger)** | [http://localhost:8324/docs](http://localhost:8324/docs) |
| **Backend API** | [http://localhost:8325](http://localhost:8325) |
| **API Docs (Swagger)** | [http://localhost:8325/docs](http://localhost:8325/docs) |
---
@@ -144,7 +144,7 @@ To use Google Calendar, Gmail, Drive, and other Google tools from your agents, y
- Add your Google account as a test user (required while the app is in "Testing" status)
4. Back on the credentials page, create an **OAuth client ID**:
- Application type: **Web application**
- Authorized redirect URIs: `http://localhost:8324/api/tools/oauth/callback`
- Authorized redirect URIs: `http://localhost:8325/api/tools/oauth/callback`
5. Copy the **Client ID** and **Client Secret**
### c. Add credentials to your `.env`
@@ -239,11 +239,11 @@ Please open an issue first for larger changes so we can discuss the approach.
Make sure you're running from the **project root** (not from `backend/`):
```bash
cd openswarm
python -m uvicorn backend.main:app --host 0.0.0.0 --port 8324 --reload
python -m uvicorn backend.main:app --host 0.0.0.0 --port 8325 --reload
```
### Frontend proxy errors / API calls failing
The frontend dev server proxies `/api` requests to `http://localhost:8324`. Make sure the backend is running first.
The frontend dev server proxies `/api` requests to `http://localhost:8325`. Make sure the backend is running first.
### Mock mode vs real mode
If you see mock responses, either:
+2 -2
View File
@@ -89,7 +89,7 @@ cd openswarm
bash run/local.sh
```
This starts the backend (port 8324), frontend (port 3000), and Electron shell together. Once running, set your Anthropic API key in the in-app Settings page.
This starts the backend (port 8325), frontend (port 3000), and Electron shell together. Once running, set your Anthropic API key in the in-app Settings page.
See the **[Contributing Guide](CONTRIBUTING.md)** for detailed setup options, environment configuration, Google Workspace integration, and troubleshooting.
@@ -101,7 +101,7 @@ See the **[Contributing Guide](CONTRIBUTING.md)** for detailed setup options, en
Electron Shell (desktop wrapper, auto-updater)
├─────────────────────────────────────────────────────────────────────┐
│ │
│ Frontend (React/TypeScript :3000) Backend (FastAPI :8324) │
│ Frontend (React/TypeScript :3000) Backend (FastAPI :8325) │
│ ┌───────────────────────────────┐ ┌───────────────────────┐ │
│ │ Spatial Dashboard Canvas │◄────►│ REST API (/api/*) │ │
│ │ Agent Chat (streaming) │ │ WebSocket (/ws/*) │ │
+3 -1
View File
@@ -1,7 +1,9 @@
# =============================================================================
# Backend Server
# =============================================================================
BACKEND_PORT=8324
# Dev default is 8325 (avoids collision with prod's 8324).
# Override via OPENSWARM_PORT env var or ports.config.json.
BACKEND_PORT=8325
GOOGLE_OAUTH_CLIENT_ID=your-google-oauth-client-id.apps.googleusercontent.com
GOOGLE_OAUTH_CLIENT_SECRET=your-google-oauth-client-secret
+4 -3
View File
@@ -26,6 +26,7 @@ from backend.apps.tools_lib.tools_lib import (
load_builtin_permissions,
)
from backend.apps.common.mcp_utils import sanitize_server_name as _sanitize_server_name
from backend.ports import BACKEND_DEV_PORT, NINE_ROUTER_PORT
logger = logging.getLogger(__name__)
@@ -71,7 +72,7 @@ async def build_agent_options(
if not _browser_all_denied:
browser_agent_server_path = os.path.join(os.path.dirname(__file__), "browser_agent_mcp_server.py")
backend_port = os.environ.get("OPENSWARM_PORT", "8324")
backend_port = os.environ.get("OPENSWARM_PORT", str(BACKEND_DEV_PORT))
pre_selected_bids = get_pre_selected_browser_ids(session.dashboard_id)
mcp_servers["openswarm-browser-agent"] = {
"command": sys.executable,
@@ -91,7 +92,7 @@ async def build_agent_options(
if not _invoke_all_denied:
invoke_agent_server_path = os.path.join(os.path.dirname(__file__), "invoke_agent_mcp_server.py")
backend_port = os.environ.get("OPENSWARM_PORT", "8324")
backend_port = os.environ.get("OPENSWARM_PORT", str(BACKEND_DEV_PORT))
mcp_servers["openswarm-invoke-agent"] = {
"command": sys.executable,
"args": [invoke_agent_server_path],
@@ -128,7 +129,7 @@ async def build_agent_options(
elif _9r_running():
options_kwargs["env"] = {
"ANTHROPIC_API_KEY": "9router",
"ANTHROPIC_BASE_URL": "http://localhost:20128",
"ANTHROPIC_BASE_URL": f"http://localhost:{NINE_ROUTER_PORT}",
}
options_kwargs["extra_args"] = {"bare": None}
logger.info("[MCP-DEBUG] Using 9Router (bare mode)")
@@ -22,7 +22,7 @@ except ImportError:
from browser_agent_mcp_schemas import TOOLS # noqa: E402 (sibling script import)
BACKEND_PORT = os.environ.get("OPENSWARM_PORT", "8324")
BACKEND_PORT = os.environ.get("OPENSWARM_PORT", "8325")
BACKEND_URL = f"http://127.0.0.1:{BACKEND_PORT}/api/agents/browser-agent/run"
MODEL = os.environ.get("OPENSWARM_AGENT_MODEL", "sonnet")
DASHBOARD_ID = os.environ.get("OPENSWARM_DASHBOARD_ID", "")
+1 -1
View File
@@ -23,7 +23,7 @@ except ImportError:
from browser_mcp_schemas import TOOLS # noqa: E402 (sibling script import)
BACKEND_PORT = os.environ.get("OPENSWARM_PORT", "8324")
BACKEND_PORT = os.environ.get("OPENSWARM_PORT", "8325")
BACKEND_URL = f"http://127.0.0.1:{BACKEND_PORT}/api/agents/browser/command"
@@ -13,7 +13,7 @@ import os
import urllib.request
import urllib.error
BACKEND_PORT = os.environ.get("OPENSWARM_PORT", "8324")
BACKEND_PORT = os.environ.get("OPENSWARM_PORT", "8325")
BACKEND_URL = f"http://127.0.0.1:{BACKEND_PORT}/api/agents/invoke-agent/run"
PARENT_SESSION_ID = os.environ.get("OPENSWARM_PARENT_SESSION_ID", "")
DASHBOARD_ID = os.environ.get("OPENSWARM_DASHBOARD_ID", "")
+1 -1
View File
@@ -17,7 +17,7 @@ import httpx
logger = logging.getLogger(__name__)
NINE_ROUTER_PORT = 20128
from backend.ports import NINE_ROUTER_PORT
NINE_ROUTER_URL = f"http://localhost:{NINE_ROUTER_PORT}"
NINE_ROUTER_API = f"{NINE_ROUTER_URL}/api"
NINE_ROUTER_V1 = f"{NINE_ROUTER_URL}/v1"
+4 -2
View File
@@ -19,7 +19,8 @@ def _check_9router() -> bool:
"""Check if 9Router is running locally."""
try:
import httpx
r = httpx.get("http://localhost:20128/v1/models", timeout=2.0)
from backend.ports import NINE_ROUTER_PORT
r = httpx.get(f"http://localhost:{NINE_ROUTER_PORT}/v1/models", timeout=2.0)
return r.status_code == 200
except Exception:
return False
@@ -146,9 +147,10 @@ def get_anthropic_client(settings: AppSettings) -> anthropic.AsyncAnthropic:
# Fall back to 9Router subscription (free for users with Claude/ChatGPT/Gemini subscriptions)
if _check_9router():
from backend.ports import NINE_ROUTER_PORT
return anthropic.AsyncAnthropic(
api_key="9router",
base_url="http://localhost:20128",
base_url=f"http://localhost:{NINE_ROUTER_PORT}",
)
raise ValueError("No AI provider configured. Set an API key or connect a subscription.")
+4 -2
View File
@@ -13,6 +13,8 @@ from typing import Any, Optional
from urllib.parse import urlencode
import httpx
from backend.ports import BACKEND_DEV_PORT
from fastapi import HTTPException, Query
from fastapi.responses import HTMLResponse
@@ -43,7 +45,7 @@ async def oauth_callback(code: str = Query(...), state: str = Query("")):
client_id = os.environ.get(provider.client_id_env, "")
client_secret = os.environ.get(provider.client_secret_env, "")
_port = os.environ.get("OPENSWARM_PORT", "8324")
_port = os.environ.get("OPENSWARM_PORT", str(BACKEND_DEV_PORT))
redirect_uri = f"http://localhost:{_port}/api/tools/oauth/callback"
token_data: dict[str, str] = {
@@ -166,7 +168,7 @@ async def oauth_start(tool_id: str):
if not client_id:
raise HTTPException(status_code=400, detail=f"{provider.client_id_env} not set in backend .env")
_port = os.environ.get("OPENSWARM_PORT", "8324")
_port = os.environ.get("OPENSWARM_PORT", str(BACKEND_DEV_PORT))
redirect_uri = f"http://localhost:{_port}/api/tools/oauth/callback"
provider_key = tool.oauth_provider or "google"
state = f"{provider_key}:{tool_id}"
+2 -1
View File
@@ -1,5 +1,6 @@
import os
from backend.ports import BACKEND_DEV_PORT
from fastapi import FastAPI, APIRouter
# import debug
from uuid import uuid4
@@ -32,7 +33,7 @@ class MainApp:
for sub_app in sub_apps:
# debug(sub_app.name)
await stack.enter_async_context(sub_app.lifespan())
_port = os.environ.get("OPENSWARM_PORT", "8324")
_port = os.environ.get("OPENSWARM_PORT", str(BACKEND_DEV_PORT))
print(f"\nCheck out the API docs at: http://127.0.0.1:{_port}/docs\n")
yield
+3 -1
View File
@@ -1,6 +1,8 @@
import logging
import os
from backend.ports import get_backend_port
logger = logging.getLogger(__name__)
from backend.config.Apps import MainApp
@@ -68,7 +70,7 @@ if __name__ == "__main__":
import uvicorn
parser = argparse.ArgumentParser(description="OpenSwarm backend server")
parser.add_argument("--port", type=int, default=int(os.environ.get("OPENSWARM_PORT", "8324")))
parser.add_argument("--port", type=int, default=get_backend_port())
parser.add_argument("--host", default=os.environ.get("OPENSWARM_HOST", "127.0.0.1"))
parser.add_argument("--reload", action="store_true", default=False)
args = parser.parse_args()
+23
View File
@@ -0,0 +1,23 @@
"""Centralized port configuration.
Reads from ports.config.json at the project root so every part of the
Python backend uses the same port numbers without hardcoding them.
"""
import json
import os
_config_path = os.path.join(os.path.dirname(__file__), "..", "ports.config.json")
with open(_config_path) as _f:
_cfg = json.load(_f)
BACKEND_DEV_PORT: int = _cfg["backend"]["dev"]
BACKEND_PROD_PORT_START: int = _cfg["backend"]["prod"]["start"]
BACKEND_PROD_PORT_END: int = _cfg["backend"]["prod"]["end"]
FRONTEND_DEV_PORT: int = _cfg["frontend"]["dev"]
NINE_ROUTER_PORT: int = _cfg["nineRouter"]
def get_backend_port() -> int:
"""Return the active backend port (env override or dev default)."""
return int(os.environ.get("OPENSWARM_PORT", str(BACKEND_DEV_PORT)))
+5 -2
View File
@@ -68,9 +68,12 @@ if [[ $? -ne 0 ]]; then
exit 1
fi
# --- Read dev port from ports.config.json ---
BACKEND_PORT=$(python3 -c "import json; print(json.load(open('$PROJECT_ROOT_ABSPATH/ports.config.json'))['backend']['dev'])")
# --- Start the backend server ---
echo "Starting backend server on http://0.0.0.0:8324 ..."
echo "Starting backend server on http://0.0.0.0:${BACKEND_PORT} ..."
cd "$PROJECT_ROOT_ABSPATH"
python3 -m uvicorn backend.main:app --host 0.0.0.0 --port 8324 --reload \
python3 -m uvicorn backend.main:app --host 0.0.0.0 --port "$BACKEND_PORT" --reload \
--reload-dir "$BACKEND_DIR_ABSPATH" \
--reload-exclude '*.pyc'
+11 -2
View File
@@ -8,6 +8,15 @@ const fs = require('fs');
const getPort = require('get-port');
const http = require('http');
const portsConfig = JSON.parse(
fs.readFileSync(
app.isPackaged
? path.join(process.resourcesPath, 'ports.config.json')
: path.join(__dirname, '..', 'ports.config.json'),
'utf8',
)
);
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling');
app.commandLine.appendSwitch('ignore-gpu-blocklist');
app.commandLine.appendSwitch('enable-gpu-rasterization');
@@ -139,7 +148,7 @@ function waitForBackend(port, timeoutMs = 60000) {
}
async function startBackend() {
backendPort = await getPort({ port: getPort.makeRange(8324, 8424) });
backendPort = await getPort({ port: getPort.makeRange(portsConfig.backend.prod.start, portsConfig.backend.prod.end) });
const pythonPath = getPythonPath();
const backendDir = getResourcePath('backend');
@@ -368,7 +377,7 @@ app.whenReady().then(async () => {
try {
if (isDev) {
backendPort = parseInt(process.env.OPENSWARM_PORT || '8324', 10);
backendPort = parseInt(process.env.OPENSWARM_PORT || String(portsConfig.backend.dev), 10);
console.log(`Dev mode: using existing backend on port ${backendPort}`);
} else {
await startBackend();
+4
View File
@@ -59,6 +59,10 @@
]
},
"extraResources": [
{
"from": "build-staging/ports.config.json",
"to": "ports.config.json"
},
{
"from": "build-staging/frontend",
"to": "frontend",
+1 -1
View File
@@ -1,4 +1,4 @@
const port = (window as any).__OPENSWARM_PORT__ || 8324;
const port = (window as any).__OPENSWARM_PORT__ || 8325;
const host = window.location.hostname || 'localhost';
export const API_BASE = `http://${host}:${port}/api`;
+4 -2
View File
@@ -2,6 +2,8 @@ const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const portsConfig = require('../ports.config.json');
module.exports = (env, argv) => {
const isDevelopment = argv.mode === 'development';
@@ -79,13 +81,13 @@ module.exports = (env, argv) => {
devServer: {
static: { directory: path.join(__dirname, 'public') },
compress: true,
port: 3000,
port: portsConfig.frontend.dev,
hot: true,
open: false,
historyApiFallback: true,
proxy: {
'/api': {
target: 'http://localhost:8324',
target: `http://localhost:${portsConfig.backend.dev}`,
changeOrigin: true,
},
},
+10
View File
@@ -0,0 +1,10 @@
{
"backend": {
"dev": 8325,
"prod": { "start": 8326, "end": 8424 }
},
"frontend": {
"dev": 3000
},
"nineRouter": 20128
}
+10 -6
View File
@@ -85,6 +85,10 @@ if [ ! -f "$UV_BIN_DIR/uvx" ]; then
rm -rf /tmp/uv-*-apple-darwin
fi
# --- Read ports from config ---
BACKEND_PORT=$(python3 -c "import json; print(json.load(open('$PROJECT_ROOT/ports.config.json'))['backend']['dev'])")
FRONTEND_PORT=$(python3 -c "import json; print(json.load(open('$PROJECT_ROOT/ports.config.json'))['frontend']['dev'])")
# --- Start backend ---
echo -e "${BLUE}${BOLD}[backend]${RESET} Starting backend server..."
bash "$PROJECT_ROOT/backend/run.sh" > >(
@@ -95,11 +99,11 @@ bash "$PROJECT_ROOT/backend/run.sh" > >(
BACKEND_PID=$!
# --- Wait for backend to become healthy ---
echo -e "${YELLOW}${BOLD}Waiting for backend (http://localhost:8324) to be ready...${RESET}"
echo -e "${YELLOW}${BOLD}Waiting for backend (http://localhost:${BACKEND_PORT}) to be ready...${RESET}"
MAX_WAIT=120
elapsed=0
while (( elapsed < MAX_WAIT )); do
if curl -s -o /dev/null --connect-timeout 1 http://localhost:8324/ 2>/dev/null; then
if curl -s -o /dev/null --connect-timeout 1 "http://localhost:${BACKEND_PORT}/" 2>/dev/null; then
echo -e "${GREEN}${BOLD}Backend is ready!${RESET}"
break
fi
@@ -126,11 +130,11 @@ bash "$PROJECT_ROOT/frontend/run.sh" > >(
FRONTEND_PID=$!
# --- Wait for frontend dev server to become available ---
echo -e "${YELLOW}${BOLD}Waiting for frontend (http://localhost:3000) to be ready...${RESET}"
echo -e "${YELLOW}${BOLD}Waiting for frontend (http://localhost:${FRONTEND_PORT}) to be ready...${RESET}"
FRONTEND_MAX_WAIT=60
frontend_elapsed=0
while (( frontend_elapsed < FRONTEND_MAX_WAIT )); do
if curl -s -o /dev/null --connect-timeout 1 http://localhost:3000/ 2>/dev/null; then
if curl -s -o /dev/null --connect-timeout 1 "http://localhost:${FRONTEND_PORT}/" 2>/dev/null; then
echo -e "${GREEN}${BOLD}Frontend is ready!${RESET}"
break
fi
@@ -175,8 +179,8 @@ ELECTRON_PID=$!
echo ""
echo -e "${BOLD}All services are running. Press Ctrl+C to stop.${RESET}"
echo -e " Backend: ${BLUE}http://localhost:8324${RESET}"
echo -e " Frontend: ${GREEN}http://localhost:3000${RESET}"
echo -e " Backend: ${BLUE}http://localhost:${BACKEND_PORT}${RESET}"
echo -e " Frontend: ${GREEN}http://localhost:${FRONTEND_PORT}${RESET}"
echo -e " Electron: ${MAGENTA}dev shell (pid $ELECTRON_PID)${RESET}"
echo ""
+2
View File
@@ -118,6 +118,8 @@ STAGING_DIR="$PROJECT_ROOT/electron/build-staging"
rm -rf "$STAGING_DIR"
mkdir -p "$STAGING_DIR"
cp "$PROJECT_ROOT/ports.config.json" "$STAGING_DIR/ports.config.json"
rsync -a \
--exclude='__pycache__' --exclude='**/__pycache__' \
--exclude='*.pyc' --exclude='.venv' \