Skip to content
Open
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
8 changes: 4 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,16 @@ services:
environment:
# Database connection (uses Docker service name 'postgres')
# Use postgresql+psycopg:// (supports both sync migrations and async app code)
DATABASE_URL: postgresql+psycopg://agent_control:agent_control@postgres:5432/agent_control
AGENT_CONTROL_DB_URL: postgresql+psycopg://agent_control:agent_control@postgres:5432/agent_control
# Server configuration
HOST: 0.0.0.0
PORT: 8000
AGENT_CONTROL_HOST: 0.0.0.0
AGENT_CONTROL_PORT: 8000
# API authentication (override via host env or server/.env; see server/.env.example)
AGENT_CONTROL_API_KEY_ENABLED: ${AGENT_CONTROL_API_KEY_ENABLED:-false}
AGENT_CONTROL_API_KEYS: ${AGENT_CONTROL_API_KEYS:-}
AGENT_CONTROL_ADMIN_API_KEYS: ${AGENT_CONTROL_ADMIN_API_KEYS:-}
AGENT_CONTROL_SESSION_SECRET: ${AGENT_CONTROL_SESSION_SECRET:-}
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:4000}
AGENT_CONTROL_CORS_ORIGINS: ${AGENT_CONTROL_CORS_ORIGINS:-http://localhost:4000}
depends_on:
postgres:
condition: service_healthy
Expand Down
9 changes: 5 additions & 4 deletions sdks/python/Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.PHONY: help test lint lint-fix typecheck build publish

TEST_DB ?= agent_control_test
TEST_DB_ENV := env -u AGENT_CONTROL_DB_URL -u DATABASE_URL -u DB_URL AGENT_CONTROL_DB_HOST=localhost AGENT_CONTROL_DB_PORT=5432 AGENT_CONTROL_DB_USER=agent_control AGENT_CONTROL_DB_PASSWORD=agent_control AGENT_CONTROL_DB_DATABASE=$(TEST_DB) AGENT_CONTROL_DB_DRIVER=psycopg

help:
@echo "Agent Control SDK - Makefile commands"
Expand All @@ -19,15 +20,15 @@ help:

test:
@echo "Starting server for tests..."
DB_DATABASE=$(TEST_DB) uv run --package agent-control-server python scripts/reset_test_db.py
DB_DATABASE=$(TEST_DB) $(MAKE) -C ../../ server-alembic-upgrade
$(TEST_DB_ENV) uv run --package agent-control-server python scripts/reset_test_db.py
$(TEST_DB_ENV) $(MAKE) -C ../../ server-alembic-upgrade
@# Start server in background and save PID
@DB_DATABASE=$(TEST_DB) uv run --package agent-control-server uvicorn agent_control_server.main:app --port 8000 --host 0.0.0.0 > server.log 2>&1 & echo $$! > server.pid
@$(TEST_DB_ENV) uv run --package agent-control-server uvicorn agent_control_server.main:app --port 8000 --host 0.0.0.0 > server.log 2>&1 & echo $$! > server.pid
@echo "Waiting for server..."
@bash -c 'for i in {1..30}; do if curl -s http://localhost:8000/health >/dev/null; then echo "Server up!"; exit 0; fi; sleep 1; done; echo "Server failed"; cat server.log; exit 1'
@# Run tests, capture exit code, and ensure cleanup
@set -e; \
DB_DATABASE=$(TEST_DB) uv run pytest --cov=src --cov-report=xml:../../coverage-sdk.xml -q; \
$(TEST_DB_ENV) uv run pytest --cov=src --cov-report=xml:../../coverage-sdk.xml -q; \
TEST_EXIT_CODE=$$?; \
echo "Stopping server..."; \
if [ -f server.pid ]; then kill `cat server.pid` && rm server.pid; fi; \
Expand Down
4 changes: 2 additions & 2 deletions sdks/python/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ For SQLite (local testing):

```bash
cd server
echo "DB_URL=sqlite+aiosqlite:///./test_agent_control.db" > .env
echo "AGENT_CONTROL_DB_URL=sqlite+aiosqlite:///./test_agent_control.db" > .env
uv run alembic upgrade head
```

Expand Down Expand Up @@ -231,7 +231,7 @@ jobs:
uv run uvicorn agent_control_server.main:app &
sleep 5
env:
DB_URL: postgresql+psycopg://postgres:postgres@localhost/agent_control_test
AGENT_CONTROL_DB_URL: postgresql+psycopg://postgres:postgres@localhost/agent_control_test
AGENT_CONTROL_API_KEYS: test-api-key-ci
AGENT_CONTROL_ADMIN_API_KEYS: test-api-key-ci

Expand Down
28 changes: 14 additions & 14 deletions server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
###############################

# Server host/port
HOST=0.0.0.0
PORT=8000
AGENT_CONTROL_HOST=0.0.0.0
AGENT_CONTROL_PORT=8000

################################
# API key authentication (auth) #
Expand Down Expand Up @@ -35,15 +35,15 @@ AGENT_CONTROL_SESSION_SECRET="change-me-to-a-long-random-string"
# set this to the exact UI origin so cookie-based auth works cross-origin.
# In production (static UI served by FastAPI on the same origin), this can
# usually be left as "*".
CORS_ORIGINS="http://localhost:4000"

#################
# Database (DB_) #
#################

# Example PostgreSQL settings (used when DATABASE_URL is not set)
DB_HOST=localhost
DB_PORT=5432
DB_USER=agent_control
DB_PASSWORD=agent_control
DB_DATABASE=agent_control
AGENT_CONTROL_CORS_ORIGINS="http://localhost:4000"

#########################################
# Database (preferred AGENT_CONTROL_DB_) #
#########################################

# Example PostgreSQL settings (legacy DB_* and DATABASE_URL are still accepted)
AGENT_CONTROL_DB_HOST=localhost
AGENT_CONTROL_DB_PORT=5432
AGENT_CONTROL_DB_USER=agent_control
AGENT_CONTROL_DB_PASSWORD=agent_control
AGENT_CONTROL_DB_DATABASE=agent_control
4 changes: 2 additions & 2 deletions server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ RUN uv sync \
ENV PATH="/app/.venv/bin:$PATH"

# Set default environment variables
ENV HOST=0.0.0.0
ENV PORT=8000
ENV AGENT_CONTROL_HOST=0.0.0.0
ENV AGENT_CONTROL_PORT=8000

# Expose the port
EXPOSE 8000
Expand Down
5 changes: 3 additions & 2 deletions server/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ SHOW ?= head
STAMP ?= head

TEST_DB ?= agent_control_test
TEST_DB_ENV := env -u AGENT_CONTROL_DB_URL -u DATABASE_URL -u DB_URL AGENT_CONTROL_DB_HOST=localhost AGENT_CONTROL_DB_PORT=5432 AGENT_CONTROL_DB_USER=agent_control AGENT_CONTROL_DB_PASSWORD=agent_control AGENT_CONTROL_DB_DATABASE=$(TEST_DB) AGENT_CONTROL_DB_DRIVER=psycopg
.PHONY: help run start-dependencies test migrate alembic-migrate alembic-revision alembic-upgrade alembic-downgrade alembic-current alembic-history alembic-heads alembic-show alembic-stamp

help:
@echo "Available targets:"
@echo " run - start FastAPI server (reload)"
@echo " start-dependencies - docker compose up -d (start local dependencies)"
@echo " test - run server tests (uses DB_DATABASE=$(TEST_DB))"
@echo " test - run server tests (uses $(TEST_DB_ENV))"
@echo " migrate - run database migrations (alembic upgrade head)"
@echo " alembic-migrate MSG='message' - autogenerate alembic revision"
@echo " alembic-upgrade UP=head - upgrade to revision"
Expand Down Expand Up @@ -63,7 +64,7 @@ start-dependencies:
@echo "PostgreSQL is ready!"

test:
DB_DATABASE=$(TEST_DB) uv run --package agent-control-server pytest --cov=src --cov-report=xml:../coverage-server.xml -q
$(TEST_DB_ENV) uv run --package agent-control-server pytest --cov=src --cov-report=xml:../coverage-server.xml -q

run: start-dependencies migrate
uv run --package agent-control-server uvicorn agent_control_server.main:app --reload
153 changes: 94 additions & 59 deletions server/src/agent_control_server/config.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
"""Server configuration settings."""

import logging
import os
import secrets
from functools import cached_property
from typing import Any

from pydantic import AliasChoices, Field
from pydantic_settings import BaseSettings, SettingsConfigDict

_config_logger = logging.getLogger(__name__)

_COMMON_SETTINGS_CONFIG = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
env_ignore_empty=True,
extra="ignore",
populate_by_name=True,
)


def _env_alias_field(default: Any, *env_names: str) -> Any:
"""Create a field that accepts multiple environment variable names."""
return Field(default=default, validation_alias=AliasChoices(*env_names))


class AuthSettings(BaseSettings):
"""Authentication configuration for API key validation."""

model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
env_prefix="AGENT_CONTROL_",
)
model_config = SettingsConfigDict(**_COMMON_SETTINGS_CONFIG, env_prefix="AGENT_CONTROL_")

# Master toggle for authentication (disabled by default for local development)
# Enable in production: AGENT_CONTROL_API_KEY_ENABLED=true
Expand Down Expand Up @@ -91,32 +101,33 @@ def get_session_secret(self) -> str:


class AgentControlServerDatabaseConfig(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
env_prefix="DB_",
extra="ignore", # Ignore extra fields in .env
)
"""Database configuration for the server."""

model_config = SettingsConfigDict(**_COMMON_SETTINGS_CONFIG, env_prefix="AGENT_CONTROL_DB_")

# Allow direct URL override for SQLite in local dev
url: str | None = None
url: str | None = _env_alias_field(None, "AGENT_CONTROL_DB_URL", "DATABASE_URL", "DB_URL")

# PostgreSQL settings (only used if url is not set)
host: str = "localhost"
port: int = 5432
user: str = "agent_control"
password: str = "agent_control"
database: str = "agent_control"
driver: str = "psycopg"
host: str = _env_alias_field("localhost", "AGENT_CONTROL_DB_HOST", "DB_HOST")
port: int = _env_alias_field(5432, "AGENT_CONTROL_DB_PORT", "DB_PORT")
user: str = _env_alias_field("agent_control", "AGENT_CONTROL_DB_USER", "DB_USER")
password: str = _env_alias_field(
"agent_control",
"AGENT_CONTROL_DB_PASSWORD",
"DB_PASSWORD",
)
database: str = _env_alias_field(
"agent_control",
"AGENT_CONTROL_DB_DATABASE",
"DB_DATABASE",
)
driver: str = _env_alias_field("psycopg", "AGENT_CONTROL_DB_DRIVER", "DB_DRIVER")

def get_url(self) -> str:
"""Get database URL, preferring explicit url if set."""

# Check for DATABASE_URL first (Docker standard), then DB_URL
database_url = os.getenv('DATABASE_URL') or self.url
if database_url:
return database_url
"""Get database URL, preferring an explicit URL if configured."""
if self.url:
return self.url
return (
f"postgresql+{self.driver}://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}"
)
Expand All @@ -125,48 +136,69 @@ def get_url(self) -> str:
class Settings(BaseSettings):
"""Server configuration settings."""

model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore", # Ignore extra fields in .env (like DB_* fields)
)
model_config = SettingsConfigDict(**_COMMON_SETTINGS_CONFIG, env_prefix="AGENT_CONTROL_")

# Server settings
host: str = "0.0.0.0"
port: int = 8000
debug: bool = False
host: str = _env_alias_field("0.0.0.0", "AGENT_CONTROL_HOST", "HOST")
port: int = _env_alias_field(8000, "AGENT_CONTROL_PORT", "PORT")
debug: bool = _env_alias_field(False, "AGENT_CONTROL_DEBUG", "DEBUG")

# API settings
api_version: str = "v1"
api_prefix: str = "/api"
api_version: str = _env_alias_field("v1", "AGENT_CONTROL_API_VERSION", "API_VERSION")
api_prefix: str = _env_alias_field("/api", "AGENT_CONTROL_API_PREFIX", "API_PREFIX")

# Prometheus metrics settings
prometheus_metrics_prefix: str = "agent_control_server"
prometheus_metrics_prefix: str = _env_alias_field(
"agent_control_server",
"AGENT_CONTROL_PROMETHEUS_METRICS_PREFIX",
"PROMETHEUS_METRICS_PREFIX",
)

# CORS settings
cors_origins: list[str] | str = "*"
allow_methods: list[str] = ["*"]
allow_headers: list[str] = ["*"]
cors_origins: list[str] | str = _env_alias_field(
"*",
"AGENT_CONTROL_CORS_ORIGINS",
"CORS_ORIGINS",
)
allow_methods: list[str] | str = _env_alias_field(
["*"],
"AGENT_CONTROL_ALLOW_METHODS",
"ALLOW_METHODS",
)
allow_headers: list[str] | str = _env_alias_field(
["*"],
"AGENT_CONTROL_ALLOW_HEADERS",
"ALLOW_HEADERS",
)

def get_cors_origins(self) -> list[str]:
"""Parse CORS origins from string or list."""
if isinstance(self.cors_origins, str):
if self.cors_origins == "*":
return self._parse_list_setting(self.cors_origins)

def get_allow_methods(self) -> list[str]:
"""Parse allow_methods from string or list."""
return self._parse_list_setting(self.allow_methods)

def get_allow_headers(self) -> list[str]:
"""Parse allow_headers from string or list."""
return self._parse_list_setting(self.allow_headers)

@staticmethod
def _parse_list_setting(value: list[str] | str) -> list[str]:
"""Parse wildcard/comma-separated settings from string or list."""
if isinstance(value, str):
if value == "*":
return ["*"]
return [origin.strip() for origin in self.cors_origins.split(",")]
return self.cors_origins
return [item.strip() for item in value.split(",") if item.strip()]
return value


class ObservabilitySettings(BaseSettings):
"""Observability configuration settings."""

model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
env_prefix="OBSERVABILITY_",
extra="ignore",
**_COMMON_SETTINGS_CONFIG,
env_prefix="AGENT_CONTROL_OBSERVABILITY_",
)

# Enable/disable observability features
Expand All @@ -176,16 +208,19 @@ class ObservabilitySettings(BaseSettings):
stdout: bool = False


class LoggingSettings(BaseSettings):
"""Server logging configuration settings."""

model_config = SettingsConfigDict(**_COMMON_SETTINGS_CONFIG, env_prefix="AGENT_CONTROL_LOG_")

level: str | None = None
json_logs: bool = _env_alias_field(False, "AGENT_CONTROL_LOG_JSON")


class UISettings(BaseSettings):
"""Static UI hosting configuration settings."""

model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
env_prefix="AGENT_CONTROL_UI_",
extra="ignore",
)
model_config = SettingsConfigDict(**_COMMON_SETTINGS_CONFIG, env_prefix="AGENT_CONTROL_UI_")

dist_dir: str | None = None

Expand Down
Loading
Loading