diff --git a/apps/agents/tasks.py b/apps/agents/tasks.py index 4b2384b..0685655 100644 --- a/apps/agents/tasks.py +++ b/apps/agents/tasks.py @@ -10,10 +10,10 @@ import asyncio @shared_task def start_agent_task_mcp(execution_id): - print(f"[start_agent_task_mcp] invoked with execution_id={execution_id}") + print(f"invoked with execution_id={execution_id}") try: execution = AgentExecution.objects.get(uuid=execution_id) - print(f"[start_agent_task_mcp] execution record loaded: agent={execution.agent.uuid}") + print(f"execution record loaded: agent={execution.agent.uuid}") execution.status = 'running' execution.started_at = timezone.now() execution.save() @@ -30,7 +30,7 @@ def start_agent_task_mcp(execution_id): "content": { "execution_id": str(execution.uuid), "agent_id": str(execution.agent.uuid), - "message": "Agent execution started (via MCP)" + "message": "Agent execution started" }, "timestamp": timezone.now().isoformat() } @@ -57,7 +57,7 @@ def start_agent_task_mcp(execution_id): ) result = asyncio.run(execute_remote()) - print(f"[start_agent_task_mcp] MCP result: {result.get('status')}") + print(f"MCP result: {result.get('status')}") if result.get('events'): for event in result['events']: @@ -110,7 +110,7 @@ def start_agent_task_mcp(execution_id): except AgentExecution.DoesNotExist: print(f"Execution {execution_id} not found") except Exception as e: - print(f"[start_agent_task_mcp] exception: {e}") + print(f"exception: {e}") import traceback traceback.print_exc() try: diff --git a/compose/dev/docker-compose.yml b/compose/dev/docker-compose.yml index c700d13..600b1c7 100644 --- a/compose/dev/docker-compose.yml +++ b/compose/dev/docker-compose.yml @@ -1,6 +1,21 @@ services: + fyp-postgres: + image: postgres:15-alpine + container_name: ${POSTGRES_CONTAINER_NAME:-fyp-postgres} + env_file: + - ../../.env + environment: + POSTGRES_HOST_AUTH_METHOD: trust + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-fyp}"] + interval: 5s + timeout: 3s + retries: 5 + fyp-redis: image: redis:7-alpine container_name: ${REDIS_CONTAINER_NAME:-fyp-redis} @@ -41,7 +56,6 @@ services: - "0.0.0.0:8000:8000" volumes: - ../../:/app - - venv:/venv environment: DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-dev-secret-key-change-in-production} DJANGO_DEBUG: "true" @@ -49,10 +63,13 @@ services: DJANGO_CELERY_BROKER_URL: redis://${REDIS_CONTAINER_NAME:-fyp-redis}:6379/0 DJANGO_CORS_ALLOWED_ORIGINS: http://localhost:5173,http://127.0.0.1:5173 DJANGO_SETTINGS_MODULE: config.settings - MCP_AGENT_URL: http://mcp-agent-server:8001 + env_file: + - ../../.env depends_on: fyp-redis: condition: service_healthy + fyp-postgres: + condition: service_healthy web: condition: service_started mcp-agent-server: @@ -65,16 +82,18 @@ services: container_name: dynavera-celery volumes: - ../../:/app - - venv:/venv - ${USERPROFILE}/.cache/gpt4all:/root/.cache/gpt4all:rw environment: DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-dev-secret-key-change-in-production} DJANGO_CELERY_BROKER_URL: redis://${REDIS_CONTAINER_NAME:-fyp-redis}:6379/0 DJANGO_SETTINGS_MODULE: config.settings - MCP_AGENT_URL: http://mcp-agent-server:8001 + env_file: + - ../../.env depends_on: fyp-redis: condition: service_healthy + fyp-postgres: + condition: service_healthy mcp-agent-server: condition: service_started @@ -94,10 +113,15 @@ services: DJANGO_SETTINGS_MODULE: config.settings PYTHONUNBUFFERED: "1" HOME: /root + env_file: + - ../../.env depends_on: fyp-redis: condition: service_healthy + fyp-postgres: + condition: service_healthy volumes: redis_data: venv: + postgres_data: diff --git a/compose/dev/python/Dockerfile b/compose/dev/python/Dockerfile index e41afb0..a018e04 100644 --- a/compose/dev/python/Dockerfile +++ b/compose/dev/python/Dockerfile @@ -3,6 +3,7 @@ FROM python:3.12-bookworm RUN apt-get update && apt-get install --no-install-recommends -y \ build-essential \ libpq-dev \ + wait-for-it \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* @@ -16,4 +17,7 @@ WORKDIR /app COPY requirements/base.txt . RUN pip install --no-cache-dir --requirement base.txt -CMD ["daphne", "-b", "0.0.0.0", "-p", "8000", "config.asgi:application"] \ No newline at end of file +COPY ./compose/prod/start /start +RUN sed -i 's/\r$//g' /start && chmod +x /start + +CMD ["/start"] \ No newline at end of file diff --git a/compose/prod/docker-compose.local.yml b/compose/prod/docker-compose.local.yml new file mode 100644 index 0000000..ea21ba6 --- /dev/null +++ b/compose/prod/docker-compose.local.yml @@ -0,0 +1,51 @@ +services: + fyp-traefik: + image: traefik:v2.10 + restart: unless-stopped + command: + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.mcp.address=:${MCP_PORT:-58001}" + ports: + - "${MCP_PORT:-58001}:${MCP_PORT:-58001}" + - "8080:8080" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" + networks: + - mcp-internal + + fyp-mcp: + build: + context: ../.. + dockerfile: compose/dev/mcp/Dockerfile + container_name: dynavera-mcp-server + restart: unless-stopped + deploy: + mode: replicated + replicas: 1 + env_file: + - ../../.env + environment: + - MCP_HTTP_HOST=0.0.0.0 + - MCP_HTTP_PORT=8001 + command: python -m mcp_agent.mcp_server + volumes: + - ../../:/app + - ${USERPROFILE}/.cache/gpt4all:/root/.cache/gpt4all:rw + - ../../build/rag_db:/app/build/rag_db:ro + labels: + - "traefik.enable=true" + - "traefik.http.routers.fyp-mcp.rule=Host(`${MCP_DOMAIN}`)" + - "traefik.http.routers.fyp-mcp.entrypoints=mcp" + - "traefik.http.services.fyp-mcp.loadbalancer.server.port=8001" + - "com.centurylinklabs.watchtower.enable=true" + - "com.centurylinklabs.watchtower.scope=fyp" + networks: + - mcp-internal + +networks: + mcp-internal: + driver: bridge + + diff --git a/compose/prod/docker-compose.yml b/compose/prod/docker-compose.yml index 6930458..ff093a4 100644 --- a/compose/prod/docker-compose.yml +++ b/compose/prod/docker-compose.yml @@ -1,6 +1,19 @@ services: + + fyp-postgres: + image: postgres:15-alpine + restart: unless-stopped + env_file: + - ../../.env + environment: + POSTGRES_HOST_AUTH_METHOD: trust + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - proxy + fyp-web: image: ${IMAGE} restart: unless-stopped @@ -65,4 +78,6 @@ volumes: gitlab-runner-config: name: gitlab-runner-config gitlab-machine-config: - name: gitlab-machine-config \ No newline at end of file + name: gitlab-machine-config + postgres_data: + name: fyp_postgres_data \ No newline at end of file diff --git a/compose/prod/start b/compose/prod/start index 6f2437d..1093810 100644 --- a/compose/prod/start +++ b/compose/prod/start @@ -4,8 +4,24 @@ set -o errexit set -o pipefail set -o nounset +DB_HOST="${POSTGRES_HOST:-localhost}" +DB_PORT="${POSTGRES_PORT:-5432}" + +echo "Waiting for database at ${DB_HOST}:${DB_PORT}..." +wait-for-it ${DB_HOST}:${DB_PORT} --timeout=30 --strict || { + echo "Timed out waiting for database" >&2 + exit 1 +} + +echo "Database is available, continuing startup..." + python manage.py makemigrations -python manage.py migrate -python manage.py loaddata data/site/users.json +python manage.py migrate --noinput + +for fixture in /app/data/site/*.json; do + echo "Loading fixture: $fixture" + python manage.py loaddata "$fixture" +done + python manage.py collectstatic --noinput -exec /usr/local/bin/daphne -b 0.0.0.0 -p 8000 config.asgi:application +exec daphne -b 0.0.0.0 -p 8000 config.asgi:application diff --git a/config/__pycache__/settings.cpython-313.pyc b/config/__pycache__/settings.cpython-313.pyc index b215aa6..5ca8620 100644 Binary files a/config/__pycache__/settings.cpython-313.pyc and b/config/__pycache__/settings.cpython-313.pyc differ diff --git a/config/settings.py b/config/settings.py index 1e0a0a8..c2aa90e 100644 --- a/config/settings.py +++ b/config/settings.py @@ -10,6 +10,7 @@ BUILD_DIR = os.getenv('DJANGO_BUILD_DIR', BASE_DIR / 'build') SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') DEBUG = str(os.getenv('DJANGO_DEBUG')).lower() in ('1', 'true', 'yes', 'on') +DOMAIN_NAME = os.getenv('DOMAIN_NAME', 'localhost') ALLOWED_HOSTS = [stripped_host for host in os.getenv('DJANGO_ALLOWED_HOSTS', 'localhost').split(',') if (stripped_host:=host.strip())] @@ -76,6 +77,8 @@ CHANNEL_LAYERS = { }, } +SESSION_ENGINE = 'django.contrib.sessions.backends.db' + TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -93,11 +96,11 @@ TEMPLATES = [ DB_ENGINE = os.getenv('DJANGO_DB_ENGINE', 'django.db.backends.sqlite3') -DB_NAME = os.getenv('DJANGO_DB_NAME', BASE_DIR / 'db.sqlite3') -DB_USER = os.getenv('DJANGO_DB_USER') -DB_PASSWORD = os.getenv('DJANGO_DB_PASSWORD') -DB_HOST = os.getenv('DJANGO_DB_HOST') -DB_PORT = os.getenv('DJANGO_DB_PORT', 5432) +DB_NAME = os.getenv('POSTGRES_DB', BASE_DIR / 'db.sqlite3') +DB_USER = os.getenv('POSTGRES_USER') +DB_PASSWORD = os.getenv('POSTGRES_PASSWORD') +DB_HOST = os.getenv('POSTGRES_HOST') +DB_PORT = os.getenv('POSTGRES_PORT', 5432) DATABASES = { 'default': { @@ -164,14 +167,32 @@ CELERY_TIMEZONE = 'UTC' CELERY_TASK_TRACK_STARTED = True CELERY_TASK_TIME_LIMIT = 30 * 60 -MCP_AGENT_URL = os.getenv('MCP_AGENT_URL', 'http://localhost:8001') +MCP_AGENT_URL = os.getenv('MCP_AGENT_URL') -if origins:=os.getenv('DJANGO_CORS_ALLOWED_ORIGINS'): - CORS_ALLOWED_ORIGINS = origins.split(',') +X_FRAME_OPTIONS = 'SAMEORIGIN' CORS_ALLOW_CREDENTIALS = True +CORS_ALLOWED_ORIGINS = [ + f'http://{DOMAIN_NAME}', + f'https://{DOMAIN_NAME}', +] +CSRF_TRUSTED_ORIGINS = [ + f'http://{DOMAIN_NAME}', + f'https://{DOMAIN_NAME}', +] -if trusted_origins:=os.getenv('DJANGO_CSRF_TRUSTED_ORIGINS'): - CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in trusted_origins.split(',')] + +CSRF_COOKIE_HTTPONLY = False +CSRF_COOKIE_SECURE = not DEBUG +CSRF_COOKIE_SAMESITE = 'Lax' + +SESSION_COOKIE_SAMESITE = 'Lax' +SESSION_COOKIE_HTTPONLY = True +SESSION_COOKIE_SECURE = not DEBUG +SESSION_COOKIE_AGE = 1209600 +SESSION_SAVE_EVERY_REQUEST = True if DEBUG: - SECURE_CROSS_ORIGIN_OPENER_POLICY = None + CORS_ALLOWED_ORIGINS.append(f'http://{DOMAIN_NAME}:5173') + CORS_ALLOWED_ORIGINS.append(f'http://{DOMAIN_NAME}:8000') + CSRF_TRUSTED_ORIGINS.append(f'http://{DOMAIN_NAME}:5173') + CSRF_TRUSTED_ORIGINS.append(f'http://{DOMAIN_NAME}:8000') diff --git a/data/site/agents.json b/data/site/agents.json new file mode 100644 index 0000000..3aef5fc --- /dev/null +++ b/data/site/agents.json @@ -0,0 +1,18 @@ +[ +{ + "model": "agents.agent", + "pk": 1, + "fields": { + "uuid": "bc5ec7f0-a894-420f-b85b-bc799334eca7", + "user": 1, + "name": "Test Agent", + "description": "General Purpose Agent", + "status": "idle", + "task_id": null, + "created_at": "2025-12-20T20:29:57.607Z", + "updated_at": "2025-12-20T20:29:57.607Z", + "started_at": null, + "completed_at": null + } +} +] diff --git a/data/site/users.json b/data/site/users.json index 854def4..9389469 100644 --- a/data/site/users.json +++ b/data/site/users.json @@ -18,7 +18,7 @@ "avatar_url": "", "is_active": true, "is_staff": true, - "role": "employee", + "role": "manager", "groups": [], "user_permissions": [] } diff --git a/index.html b/index.html index 9ea63cc..eb8ae15 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,6 @@ - Dynavera diff --git a/mcp_agent/mcp_client.py b/mcp_agent/mcp_client.py index 1a29b35..6976242 100644 --- a/mcp_agent/mcp_client.py +++ b/mcp_agent/mcp_client.py @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) class MCPAgentClient: def __init__(self, server_url: Optional[str] = None): - self.server_url = server_url or getattr(settings, 'MCP_AGENT_URL', 'http://localhost:8001') + self.server_url = server_url or getattr(settings, 'MCP_AGENT_URL') self.http_client = httpx.AsyncClient( timeout=httpx.Timeout(300.0), follow_redirects=True @@ -114,7 +114,7 @@ async def get_mcp_client() -> MCPAgentClient: async with _client_lock: if _mcp_client_instance is None: - server_url = getattr(settings, 'MCP_AGENT_URL', 'http://localhost:8001') + server_url = getattr(settings, 'MCP_AGENT_URL') _mcp_client_instance = MCPAgentClient(server_url=server_url) return _mcp_client_instance diff --git a/requirements/base.txt b/requirements/base.txt index 05353b1..3c47bba 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -10,6 +10,7 @@ django-celery-results==2.5.1 django-celery-beat==2.8.1 gunicorn==23.0.0 jinja2==3.1.6 +psycopg2-binary==2.9.10 python-dotenv==1.2.1 requests==2.32.5 sqlparse==0.5.3 diff --git a/src/lib/api.ts b/src/lib/api.ts index 6004d7d..7c15ba7 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -8,8 +8,18 @@ class ApiClient { } private getCsrfToken(): string { - const match = document.cookie.match(/(?:^|; )csrftoken=([^;]+)/); - return match ? decodeURIComponent(match[1]) : ''; + let cookieValue = ''; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + if (cookie.substring(0, 10) === 'csrftoken=') { + cookieValue = decodeURIComponent(cookie.substring(10)); + break; + } + } + } + return cookieValue; } private withCsrf(config?: AxiosRequestConfig): AxiosRequestConfig {