Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,6 @@ data/

# Node
node_modules/

# Repo Reference for Onboarding
REPO_REFERENCE.md
188 changes: 188 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ servers:
main:
host: localhost
port: 8000
protocol: auto # optional: auto | http | https
tls_verify: true # optional: true | false (HTTPS certificate verification)
ca_bundle_path: null # optional: custom CA bundle path for private/self-signed certs
client_cert_path: null # optional: client cert path for mTLS
client_key_path: null # optional: client key path for mTLS
username: admin
password: admin
default_server: main
Expand All @@ -176,6 +181,187 @@ chat_defaults: {}
audit_log: []
```

## HTTPS / SSL Server Configuration

Condor supports both HTTP and HTTPS when connecting to Hummingbot API servers.

### Protocol resolution

For each server in `config.yml`:
- If `host` includes a scheme (`http://` or `https://`), that scheme is used.
- Otherwise, `protocol` is used when provided (`http` or `https`).
- If `protocol` is omitted or set to `auto`:
- port `443` => HTTPS
- any other port => HTTP

### Recommended patterns

**Local development (HTTP):**
```yaml
servers:
local:
host: localhost
port: 8000
protocol: auto
username: admin
password: admin
```

**Production (HTTPS on 443):**
```yaml
servers:
production:
host: api.example.com
port: 443
protocol: auto
tls_verify: true
username: admin
password: strong_password
```

**Explicit HTTPS custom port:**
```yaml
servers:
production_custom:
host: api.example.com
port: 8443
protocol: https
tls_verify: true
username: admin
password: strong_password
```

**Host with explicit scheme:**
```yaml
servers:
explicit:
host: https://api.example.com
port: 443
tls_verify: true
username: admin
password: strong_password
```

**HTTPS with private/internal CA:**
```yaml
servers:
private_ca:
host: api.internal.example.com
port: 443
protocol: auto
tls_verify: true
ca_bundle_path: /etc/ssl/certs/internal-ca.pem
username: admin
password: strong_password
```

**mTLS (client certificate authentication):**
```yaml
servers:
mtls:
host: api.example.com
port: 443
protocol: auto
tls_verify: true
ca_bundle_path: /etc/ssl/certs/ca.pem
client_cert_path: /etc/ssl/certs/condor-client.pem
client_key_path: /etc/ssl/private/condor-client.key
username: admin
password: strong_password
```

**Temporary insecure mode (not recommended):**
```yaml
servers:
lab:
host: lab.example.local
port: 8443
protocol: https
tls_verify: false
username: admin
password: admin
```

### Validation behavior

Condor validates server URL settings and rejects conflicting configurations, for example:
- `host: "https://api.example.com:9000"` with `port: 8000`
- `host: "https://api.example.com"` with `protocol: http`

When `tls_verify` is `true` and `ca_bundle_path` is provided, Condor uses that CA bundle for certificate validation.
When `tls_verify` is `false`, Condor disables certificate verification for that server.
If `client_cert_path` or `client_key_path` is provided, both must be provided (mTLS pair).

## End-to-End Certificate Setup (Hummingbot API + Condor)

The easiest secure local setup is to generate a local CA, issue a server cert for Hummingbot API, then configure Condor to trust that CA.

### 1) Generate a local CA and server cert (OpenSSL example)

```bash
# Create a local CA
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
-out ca.pem -subj "/CN=Condor Local CA"

# Create server key + CSR
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj "/CN=localhost"

# Sign server cert with local CA
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
-out server.pem -days 825 -sha256
```

### Alternative: automated generation from `hummingbot-api`

If you run the sibling `hummingbot-api` repo setup:

```bash
make generate-certs
```

It will generate certs in `hummingbot-api/certs` and print exact paths you can copy into Condor (`ca_bundle_path`, and optional mTLS client cert/key paths).

### 2) Run Hummingbot API with HTTPS

```bash
uvicorn main:app --host 0.0.0.0 --port 8443 \
--ssl-certfile /path/to/server.pem \
--ssl-keyfile /path/to/server.key
```

### 3) Configure Condor to trust that CA

```yaml
servers:
local_https:
host: localhost
port: 8443
protocol: auto
tls_verify: true
ca_bundle_path: /path/to/ca.pem
username: admin
password: admin
```

### 4) Optional mTLS (if server requires client certs)

Generate client cert/key signed by the same CA and add:

```yaml
client_cert_path: /path/to/client.pem
client_key_path: /path/to/client.key
```

### Backward compatibility and migration

Existing configurations continue to work without changes:
- `host: localhost`, `port: 8000` still resolves to `http://localhost:8000`
- configurations already using `http://...` or `https://...` in `host` remain supported

For migration to HTTPS, update one server at a time and verify status in `/servers` before switching `default_server`.

## Troubleshooting

| Issue | Solution |
Expand All @@ -185,6 +371,8 @@ audit_log: []
| Commands failing | Verify Hummingbot API is running |
| Connection refused | Check server host:port in `/config` |
| Auth error | Verify server credentials |
| TLS handshake/certificate errors | Verify API certificate validity and hostname; if using custom CA, ensure Condor environment trusts it |
| Invalid server URL config | Check for conflicts between scheme/host/port/protocol (for example host URL already includes a different port) |
| DEX features unavailable | Ensure Gateway is configured and running |

## Docker Deployment
Expand Down
2 changes: 1 addition & 1 deletion condor/trading_agent/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def build_tick_prompt(
"Make your best move now — there will be no follow-up ticks."
)

# Server credentials are injected via env vars into the MCP process,
# Server credentials are injected via CLI args into the MCP process,
# so no need to include them in the prompt or call configure_server.

# Strategy instructions
Expand Down
63 changes: 51 additions & 12 deletions config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

import yaml
from aiohttp import ClientTimeout
from utils.hummingbot_client_factory import create_initialized_client
from utils.url_builder import build_server_url_from_config

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -94,6 +96,13 @@ def _load_config(self):

# Ensure all sections exist
self._data.setdefault("servers", {})
for server in self._data["servers"].values():
server.setdefault("protocol", "auto")
server.setdefault("tls_verify", True)
server.setdefault("ca_bundle_path", None)
server.setdefault("client_cert_path", None)
server.setdefault("client_key_path", None)

self._data.setdefault("default_server", None)
self._data.setdefault("users", {})
self._data.setdefault("server_access", {})
Expand Down Expand Up @@ -233,6 +242,11 @@ def add_server(
username: str,
password: str,
owner_id: int = None,
protocol: str = "auto",
tls_verify: bool = True,
ca_bundle_path: str | None = None,
client_cert_path: str | None = None,
client_key_path: str | None = None,
) -> bool:
"""Add a new server."""
servers = self._data["servers"]
Expand All @@ -245,6 +259,11 @@ def add_server(
"port": port,
"username": username,
"password": password,
"protocol": protocol,
"tls_verify": tls_verify,
"ca_bundle_path": ca_bundle_path,
"client_cert_path": client_cert_path,
"client_key_path": client_key_path,
}

# Register ownership
Expand All @@ -262,6 +281,11 @@ def modify_server(
port: int = None,
username: str = None,
password: str = None,
protocol: str = None,
tls_verify: bool = None,
ca_bundle_path: str = None,
client_cert_path: str = None,
client_key_path: str = None,
) -> bool:
"""Modify an existing server."""
servers = self._data["servers"]
Expand All @@ -281,6 +305,16 @@ def modify_server(
servers[name]["username"] = username
if password is not None:
servers[name]["password"] = password
if protocol is not None:
servers[name]["protocol"] = protocol
if tls_verify is not None:
servers[name]["tls_verify"] = tls_verify
if ca_bundle_path is not None:
servers[name]["ca_bundle_path"] = ca_bundle_path
if client_cert_path is not None:
servers[name]["client_cert_path"] = client_cert_path
if client_key_path is not None:
servers[name]["client_key_path"] = client_key_path

self._save_config()
logger.info(f"Modified server '{name}'")
Expand Down Expand Up @@ -324,8 +358,6 @@ def set_default_server(self, name: str) -> bool:

async def get_client(self, name: str = None):
"""Get or create API client for a server."""
from hummingbot_api_client import HummingbotAPIClient

if name is None:
name = self.get_default_server()
if not name:
Expand All @@ -349,9 +381,9 @@ async def get_client(self, name: str = None):
self._client_locks[name] = asyncio.Lock()

async with self._client_locks[name]:
return await self._get_or_create_client(name, HummingbotAPIClient)
return await self._get_or_create_client(name)

async def _get_or_create_client(self, name: str, HummingbotAPIClient):
async def _get_or_create_client(self, name: str):
"""Inner client acquisition — must be called under _client_locks[name]."""
# Re-check under lock (another coroutine may have just created it)
if name in self._clients:
Expand Down Expand Up @@ -385,16 +417,19 @@ async def _get_or_create_client(self, name: str, HummingbotAPIClient):

# Create new client
server = self._data["servers"][name]
base_url = f"http://{server['host']}:{server['port']}"
client = HummingbotAPIClient(
base_url = build_server_url_from_config(server)
client = await create_initialized_client(
base_url=base_url,
username=server["username"],
password=server["password"],
timeout=ClientTimeout(total=60, connect=10),
tls_verify=server.get("tls_verify", True),
ca_bundle_path=server.get("ca_bundle_path"),
client_cert_path=server.get("client_cert_path"),
client_key_path=server.get("client_key_path"),
)

try:
await client.init()
await client.accounts.list_accounts()
self._clients[name] = (client, time.time())
logger.info(f"Connected to server '{name}' at {base_url}")
Expand Down Expand Up @@ -442,23 +477,27 @@ async def get_client_for_chat(

async def check_server_status(self, name: str) -> dict:
"""Check if a server is online."""
from hummingbot_api_client import HummingbotAPIClient

if name not in self._data["servers"]:
return {"status": "error", "message": "Server not found"}

server = self._data["servers"][name]
base_url = f"http://{server['host']}:{server['port']}"
try:
base_url = build_server_url_from_config(server)
except ValueError as e:
return {"status": "error", "message": f"Invalid server URL config: {e}"}

client = HummingbotAPIClient(
client = await create_initialized_client(
base_url=base_url,
username=server["username"],
password=server["password"],
timeout=ClientTimeout(total=3, connect=2),
tls_verify=server.get("tls_verify", True),
ca_bundle_path=server.get("ca_bundle_path"),
client_cert_path=server.get("client_cert_path"),
client_key_path=server.get("client_key_path"),
)

try:
await client.init()
await client.accounts.list_accounts()
return {"status": "online", "message": "Connected and authenticated"}
except Exception as e:
Expand Down
Loading
Loading