diff --git a/.env.enterprise-clean.example b/.env.enterprise-clean.example index 7838409b..42d38791 100644 --- a/.env.enterprise-clean.example +++ b/.env.enterprise-clean.example @@ -7,7 +7,7 @@ # #endregion env.enterprise-clean # ====================================================================== -# PostgreSQL (внешний, корпоративный) +# PostgreSQL (внешний, корпоративный) — ОБЯЗАТЕЛЬНО # ====================================================================== POSTGRES_HOST=postgres.company.local POSTGRES_PORT=5432 @@ -21,6 +21,22 @@ POSTGRES_PASSWORD=change-me BACKEND_HOST_PORT=8001 FRONTEND_HOST_PORT=8000 FRONTEND_SSL_PORT=443 +AGENT_HOST_PORT=7860 + +# ====================================================================== +# Безопасность (ОБЯЗАТЕЛЬНО) +# ====================================================================== +# JWT-ключ подписи токенов — единый для backend и agent. +# Сгенерировать: python3 -c "import secrets; print(secrets.token_urlsafe(32))" +AUTH_SECRET_KEY=change-me-to-a-random-secret-32-chars-min + +# Fernet-ключ шифрования паролей подключений и API-ключей. +# Сгенерировать: python3 -c "import base64,os; print(base64.urlsafe_b64encode(os.urandom(32)).decode())" +ENCRYPTION_KEY=3YIxOr3_GFht9ZyjRQMqOdtuO1CKOj8Y9mpY89iMbZY= + +# Сервисный токен для agent→backend вызовов. +# Сгенерировать: python3 -c "import secrets; print('svc-' + secrets.token_urlsafe(24))" +SERVICE_JWT=agent-service-secret # ====================================================================== # Сертификаты (корпоративные) @@ -29,8 +45,16 @@ CERTS_PATH=./certs SSL_KEY_PASSPHRASE= # ====================================================================== -# LLM CA-сертификаты (скачка по URL на старте контейнера) +# LLM / AI провайдеры # ====================================================================== +OPENAI_API_KEY= +ANTHROPIC_API_KEY= +# Агент: LLM настройки (если не подтягиваются из FastAPI /api/agent/llm-config) +LLM_API_KEY= +LLM_BASE_URL=https://api.openai.com/v1 +LLM_MODEL=gpt-4o +LLM_SSL_VERIFY=true +# LLM CA-сертификаты (скачка по URL на старте контейнера, через запятую) LLM_CA_CERT_URLS= # ====================================================================== @@ -40,13 +64,7 @@ ENABLE_BELIEF_STATE_LOGGING=true TASK_LOG_LEVEL=INFO # ====================================================================== -# Шифрование (ОБЯЗАТЕЛЬНО) -# ====================================================================== -# Сгенерировать: python3 -c "import base64,os; print(base64.urlsafe_b64encode(os.urandom(32)).decode())" -ENCRYPTION_KEY=3YIxOr3_GFht9ZyjRQMqOdtuO1CKOj8Y9mpY89iMbZY= - -# ====================================================================== -# Admin (первый запуск) +# Admin bootstrap (первый запуск) # ====================================================================== INITIAL_ADMIN_CREATE=false INITIAL_ADMIN_USERNAME=admin @@ -54,7 +72,31 @@ INITIAL_ADMIN_PASSWORD=change-me INITIAL_ADMIN_EMAIL= # ====================================================================== -# Features (настраивается через web UI) +# CORS / Безопасность деплоя +# ====================================================================== +ALLOWED_ORIGINS=http://localhost:8000 +FORCE_HTTPS=false +APP_TIMEZONE=Europe/Moscow + +# ====================================================================== +# Features # ====================================================================== FEATURES__DATASET_REVIEW=true FEATURES__HEALTH_MONITOR=true + +# ====================================================================== +# Агент (опциональные тонкие настройки) +# ====================================================================== +# GRADIO_ALLOW_PORT_FALLBACK=true +# AGENT_ENABLE_LLM_TITLES=true +# AGENT_TITLE_GENERATION_TIMEOUT_S=0.25 +# AGENT_PREFETCH_DASHBOARD_LIMIT=25 +# AGENT_CONFIRM_TOOLS=false +# AGENT_INTERRUPT_BEFORE= + +# ====================================================================== +# ADFS SSO (опционально) +# ====================================================================== +# ADFS_CLIENT_ID= +# ADFS_CLIENT_SECRET= +# ADFS_METADATA_URL= diff --git a/.env.example b/.env.example index 4bf15033..85112d0d 100644 --- a/.env.example +++ b/.env.example @@ -1,38 +1,87 @@ # ====================================================================== -# superset-tools — Переменные окружения -# Только обязательные + docker. Остальное настраивается через web UI. +# superset-tools — локальная разработка (docker compose) +# Все переменные, используемые docker-compose.yml. +# Скопируйте в .env.current или .env.master и отредактируйте под ветку. # ====================================================================== -# --- Обязательно: секреты --- -AUTH_SECRET_KEY=change-me-to-a-random-secret # JWT-ключ подписи токенов (обязательно) -ENCRYPTION_KEY=3YIxOr3_GFht9ZyjRQMqOdtuO1CKOj8Y9mpY89iMbZY= # Fernet-ключ шифрования (сгенерируйте свой для прода) +# ── Проект ───────────────────────────────────────────────────────────── +COMPOSE_PROJECT_NAME=ss-tools-current -# --- Обязательно: базы данных --- -DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools # Основная БД -AUTH_DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools # БД аутентификации -TASKS_DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools # БД задач +# ── PostgreSQL (встроенный, docker compose db service) ────────────────── +POSTGRES_IMAGE=postgres:16-alpine +POSTGRES_HOST_PORT=5433 +POSTGRES_DB=ss_tools +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres -# --- ADFS SSO (опционально) --- -ADFS_CLIENT_ID= # Client ID для ADFS (оставьте пустым, если не используется) -ADFS_CLIENT_SECRET= # Client Secret для ADFS -ADFS_METADATA_URL= # URL метаданных ADFS +# ── Порты хоста ──────────────────────────────────────────────────────── +BACKEND_HOST_PORT=8101 +FRONTEND_HOST_PORT=8100 +FRONTEND_SSL_PORT=443 +AGENT_HOST_PORT=7860 -# --- Администратор (первый запуск) --- -INITIAL_ADMIN_CREATE=false # true — создать администратора при старте (только для первого запуска) -INITIAL_ADMIN_USERNAME=admin # Логин администратора -INITIAL_ADMIN_PASSWORD=admin123 # Пароль (обязателен при INITIAL_ADMIN_CREATE=true) -INITIAL_ADMIN_EMAIL=admin@example.com # Email администратора (опционально) +# ── Безопасность (ОБЯЗАТЕЛЬНО) ───────────────────────────────────────── +# JWT-ключ подписи токенов — единый для backend и agent. +# Сгенерировать: python3 -c "import secrets; print(secrets.token_urlsafe(32))" +AUTH_SECRET_KEY=change-me-to-a-random-secret-32-chars-min +# Fernet-ключ шифрования паролей подключений и API-ключей. +# Сгенерировать: python3 -c "import base64,os; print(base64.urlsafe_b64encode(os.urandom(32)).decode())" +ENCRYPTION_KEY=3YIxOr3_GFht9ZyjRQMqOdtuO1CKOj8Y9mpY89iMbZY= +# Сервисный токен для agent→backend вызовов. +# Сгенерировать: python3 -c "import secrets; print('svc-' + secrets.token_urlsafe(24))" +SERVICE_JWT=agent-service-secret +# JWT audience / issuer (опционально) +# JWT_AUDIENCE=superset-tools-api +# JWT_ISSUER=superset-tools -# --- Порты (Docker) --- -BACKEND_HOST_PORT=8001 # Внешний порт бэкенда на хосте -FRONTEND_HOST_PORT=8000 # Внешний порт фронтенда на хосте +# ── Admin bootstrap (первый запуск) ──────────────────────────────────── +INITIAL_ADMIN_CREATE=true +INITIAL_ADMIN_USERNAME=admin +INITIAL_ADMIN_PASSWORD=admin +INITIAL_ADMIN_EMAIL= -# --- PostgreSQL (прямое подключение, без DATABASE_URL) --- -POSTGRES_HOST=localhost # Хост PostgreSQL -POSTGRES_PORT=5432 # Порт PostgreSQL -POSTGRES_DB=ss_tools # Имя БД -POSTGRES_USER=postgres # Пользователь -POSTGRES_PASSWORD=postgres # Пароль +# ── LLM / AI провайдеры (опционально — настраивается через Web UI) ───── +OPENAI_API_KEY= +ANTHROPIC_API_KEY= +# Агент: LLM настройки (если не подтягиваются из FastAPI /api/agent/llm-config) +LLM_API_KEY= +LLM_BASE_URL=https://api.openai.com/v1 +LLM_MODEL=gpt-4o +LLM_SSL_VERIFY=true +LLM_CA_CERT_URLS= -# --- Docker Compose --- -COMPOSE_PROJECT_NAME=superset-tools # Имя Docker Compose проекта +# ── Агент (настройки) ────────────────────────────────────────────────── +GRADIO_SERVER_PORT=7860 +# GRADIO_ALLOW_PORT_FALLBACK=true +# AGENT_ENABLE_LLM_TITLES=true +# AGENT_TITLE_GENERATION_TIMEOUT_S=0.25 +# AGENT_PREFETCH_DASHBOARD_LIMIT=25 +# AGENT_CONFIRM_TOOLS=false +# AGENT_INTERRUPT_BEFORE= + +# ── Сертификаты / фронтенд ───────────────────────────────────────────── +CERTS_PATH=./certs +SSL_KEY_PASSPHRASE= + +# ── Логирование ──────────────────────────────────────────────────────── +ENABLE_BELIEF_STATE_LOGGING=true +TASK_LOG_LEVEL=INFO + +# ── CORS / Безопасность деплоя ───────────────────────────────────────── +ALLOWED_ORIGINS=http://localhost:8100,http://127.0.0.1:8100 +FORCE_HTTPS=false +APP_TIMEZONE=Europe/Moscow + +# ── Features ─────────────────────────────────────────────────────────── +FEATURES__DATASET_REVIEW=true +FEATURES__HEALTH_MONITOR=true + +# ── ADFS SSO (опционально) ───────────────────────────────────────────── +# ADFS_CLIENT_ID= +# ADFS_CLIENT_SECRET= +# ADFS_METADATA_URL= + +# ── OpenRouter (опционально) ─────────────────────────────────────────── +# OPENROUTER_SITE_URL= +# OPENROUTER_APP_NAME= +# APP_BASE_URL= diff --git a/.kilo/plans/1783320721445-eager-tiger.md b/.kilo/plans/1783320721445-eager-tiger.md new file mode 100644 index 00000000..c0f7d80c --- /dev/null +++ b/.kilo/plans/1783320721445-eager-tiger.md @@ -0,0 +1,245 @@ +# Plan: unify Docker/.env variables and examples + +## Current problem + +В проекте сейчас есть drift между compose-файлами и env-примерами: + +- `backend` использует `AUTH_SECRET_KEY` как канонический JWT signing secret (`src/core/auth/config.py`). +- `agent` исторически использовал `JWT_SECRET`, а затем получил временный fallback на `AUTH_SECRET_KEY` в `src/agent/_jwt_decoder.py`; этот fallback нужно удалить. +- `.env.example`, `.env.enterprise-clean.example`, `backend/.env.example`, `docker/.env.agent.example`, `docker-compose.yml`, `docker-compose.enterprise-clean.yml` и generated compose внутри `build.sh` описывают разные наборы переменных. +- `docker/.env.agent.example` устарел: там `AUTH_SECRET_KEY`, но compose раньше прокидывал `JWT_SECRET`; также не перечислены agent tuning переменные. +- `docker-compose.enterprise-clean.yml` и `build.sh` generated compose должны быть синхронны, иначе локальная сборка и bundle дают разное runtime-поведение. +- Локальные `.env.current` / `.env.master` содержат только порты/admin/features и не показывают auth/LLM/certs/security переменные. + +## Canonical naming decision + +Use `AUTH_SECRET_KEY` as the canonical token-signing secret across backend and agent. + +Rationale: + +- Backend already requires `AUTH_SECRET_KEY` through `AuthConfig`. +- Root/backend examples already document `AUTH_SECRET_KEY`. +- `JWT_SECRET` is ambiguous: it can mean backend auth token signing, service token signing, or agent token validation. +- No fallback: `JWT_SECRET` must be removed from Docker/runtime examples and from agent JWT decoding logic. Deployments must define `AUTH_SECRET_KEY` only. + +Canonical variables: + +- `AUTH_SECRET_KEY`: canonical shared user JWT signing key, backend + agent. +- `JWT_SECRET`: removed/deprecated hard error — do not use in compose, examples, or agent runtime. +- `SERVICE_JWT`: service-to-backend token for agent calls; separate from user JWT signing key. +- `ENCRYPTION_KEY`: Fernet key for encrypted connection credentials/API keys. + +## Files to update + +### 1. `docker-compose.yml` + +Unify local dev compose with canonical variable names: + +- DB service: + - Use `${POSTGRES_DB:-ss_tools}`, `${POSTGRES_USER:-postgres}`, `${POSTGRES_PASSWORD:-postgres}` instead of hardcoded values. + - Healthcheck should use the same values. +- Backend service: + - Construct `DATABASE_URL`, `TASKS_DATABASE_URL`, `AUTH_DATABASE_URL` from the same POSTGRES variables. + - Pass `AUTH_SECRET_KEY`, `ENCRYPTION_KEY`, `JWT_AUDIENCE`, `JWT_ISSUER`, `ALLOWED_ORIGINS`, `FORCE_HTTPS`, `APP_TIMEZONE` where relevant. + - Pass `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `LLM_CA_CERT_URLS`, `LLM_SSL_VERIFY` consistently. +- Agent service: + - Pass `AUTH_SECRET_KEY` only. + - Remove `JWT_SECRET` entirely; do not include compatibility aliases. + - Keep `SERVICE_JWT`, `DATABASE_URL`, `FASTAPI_URL`, `LLM_*`, `GRADIO_*`, `AGENT_*` variables. +- Frontend service: + - Add `SSL_KEY_PASSPHRASE` if local frontend can terminate SSL similarly to enterprise. + +### 2. `docker-compose.enterprise-clean.yml` + +Make enterprise compose match the canonical env contract: + +- Backend: + - Keep external Postgres URL composition from POSTGRES variables. + - Add/pass `AUTH_SECRET_KEY` explicitly. + - Keep required `ENCRYPTION_KEY`. + - Add optional documented variables that backend reads: + - `JWT_AUDIENCE`, `JWT_ISSUER` + - `ALLOWED_ORIGINS`, `FORCE_HTTPS` + - `APP_TIMEZONE` + - `LLM_SSL_VERIFY`, `LLM_CA_CERT_URLS` + - `OPENROUTER_SITE_URL`, `OPENROUTER_APP_NAME` +- Agent: + - Use `AUTH_SECRET_KEY: ${AUTH_SECRET_KEY:?AUTH_SECRET_KEY must be set}`. + - Remove `JWT_SECRET` entirely; no fallback/alias support. + - Keep `DATABASE_URL` for LangGraph checkpoint persistence. + - Include all agent optional knobs in comments/examples, but compose can omit defaults if code defaults are sufficient. +- Frontend: + - Keep `SSL_KEY_PASSPHRASE`, `CERTS_PATH` mounts, host ports. + +### 3. `docker-compose.e2e.yml` + +Keep test isolation but align naming: + +- Use `AUTH_SECRET_KEY`, `ENCRYPTION_KEY`, `DATABASE_URL`, `AUTH_DATABASE_URL`, `TASKS_DATABASE_URL` consistently. +- Add `SERVICE_JWT` if e2e agent/service paths are enabled later. +- Add a matching `.env.e2e.example` so CI/local users do not infer variables from comments. + +### 4. `build.sh` + +Generated compose files must match source compose: + +- Release `bundle` generated `docker-compose.enterprise-clean.yml`: + - Replace `JWT_SECRET` with canonical `AUTH_SECRET_KEY` for agent and backend. + - Add/pass backend `AUTH_SECRET_KEY` if missing. + - Ensure generated env variable names match `.env.enterprise-clean.example` exactly. +- `bundle:embeddings` generated compose: + - Apply the same env contract as default bundle. +- `bundle:light` generated compose/env: + - Ensure backend all-in-one receives `AUTH_SECRET_KEY`, `ENCRYPTION_KEY`, DB URLs, admin vars, LLM vars. +- Help text: + - State that `AUTH_SECRET_KEY` is required and `JWT_SECRET` is unsupported. + +### 5. `.env.example` + +Turn this into the canonical local/dev template with all Docker-relevant variables grouped consistently: + +Sections: + +1. Project/compose + - `COMPOSE_PROJECT_NAME` +2. Postgres + - `POSTGRES_IMAGE`, `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_HOST_PORT`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` +3. Database URLs + - `DATABASE_URL`, `TASKS_DATABASE_URL`, `AUTH_DATABASE_URL` + - Note: compose may derive URLs, direct backend local run uses URLs. +4. Security + - `AUTH_SECRET_KEY`, `ENCRYPTION_KEY`, `JWT_AUDIENCE`, `JWT_ISSUER`. + - Add an explicit comment: `JWT_SECRET` is unsupported; migrate to `AUTH_SECRET_KEY`. +5. Ports + - `BACKEND_HOST_PORT`, `FRONTEND_HOST_PORT`, `FRONTEND_SSL_PORT`, `AGENT_HOST_PORT` +6. Admin bootstrap + - `INITIAL_ADMIN_CREATE`, `INITIAL_ADMIN_USERNAME`, `INITIAL_ADMIN_PASSWORD`, `INITIAL_ADMIN_EMAIL` +7. LLM/provider + - `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `LLM_API_KEY`, `LLM_BASE_URL`, `LLM_MODEL`, `LLM_SSL_VERIFY`, `LLM_CA_CERT_URLS` +8. Agent + - `SERVICE_JWT`, `GRADIO_SERVER_PORT`, `GRADIO_ALLOW_PORT_FALLBACK`, `AGENT_ENABLE_LLM_TITLES`, `AGENT_TITLE_GENERATION_TIMEOUT_S`, `AGENT_PREFETCH_DASHBOARD_LIMIT`, `AGENT_CONFIRM_TOOLS`, `AGENT_INTERRUPT_BEFORE`, `EMBEDDING_*` +9. Certificates/frontend + - `CERTS_PATH`, `SSL_KEY_PASSPHRASE` +10. Runtime behavior + - `ENABLE_BELIEF_STATE_LOGGING`, `TASK_LOG_LEVEL`, `APP_TIMEZONE`, `ALLOWED_ORIGINS`, `FORCE_HTTPS` +11. Features + - `FEATURES__DATASET_REVIEW`, `FEATURES__HEALTH_MONITOR` +12. Optional integrations + - `ADFS_*`, `OPENROUTER_*`, `GITEA_TOKEN` only where relevant. + +### 6. `.env.enterprise-clean.example` + +Mirror `.env.example` but with production/external-Postgres defaults and enterprise comments: + +- Include every variable used by `docker-compose.enterprise-clean.yml` and generated bundle compose. +- Use placeholder secrets, not real values. +- Add explicit generation commands for `AUTH_SECRET_KEY`, `ENCRYPTION_KEY`, and `SERVICE_JWT`. +- Include `AGENT_HOST_PORT` (currently missing from example but used by compose). +- Include `LLM_SSL_VERIFY` (currently used by compose, missing from example). +- Include `AUTH_SECRET_KEY` (currently required by backend/agent, missing from enterprise example). +- Include `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `LLM_API_KEY`, `LLM_BASE_URL`, `LLM_MODEL` (compose/build uses them, example incomplete). + +### 7. `backend/.env.example` + +Keep backend-focused, but synchronize with canonical names: + +- Include backend-only/source-run variables: + - `DATABASE_URL`, `TASKS_DATABASE_URL`, `AUTH_DATABASE_URL` + - `AUTH_SECRET_KEY`, `ENCRYPTION_KEY`, `JWT_AUDIENCE`, `JWT_ISSUER` + - `INITIAL_ADMIN_*` + - `LLM_SSL_VERIFY`, `LLM_CA_CERT_URLS` + - `ALLOWED_ORIGINS`, `FORCE_HTTPS`, `APP_TIMEZONE` + - `ADFS_*`, `OPENROUTER_*` +- Do not include frontend/agent-only host port variables except where helpful comments point users to root `.env.example`. + +### 8. `docker/.env.agent.example` + +Make it agent-only and current: + +- Replace `AUTH_SECRET_KEY=` with canonical `AUTH_SECRET_KEY=...` and note it must match backend. +- Remove stale implication that `AUTH_DATABASE_URL` is needed by agent. +- Include: + - `AUTH_SECRET_KEY` + - `SERVICE_JWT` + - `FASTAPI_URL` + - `DATABASE_URL` + - `LLM_API_KEY`, `LLM_BASE_URL`, `LLM_MODEL` + - `GRADIO_SERVER_NAME`, `GRADIO_SERVER_PORT`, `GRADIO_ALLOW_PORT_FALLBACK` + - `AGENT_ENABLE_LLM_TITLES`, `AGENT_TITLE_GENERATION_TIMEOUT_S`, `AGENT_PREFETCH_DASHBOARD_LIMIT`, `AGENT_CONFIRM_TOOLS`, `AGENT_INTERRUPT_BEFORE` + - `EMBEDDING_SIMILARITY_THRESHOLD`, `EMBEDDING_TOP_K` +- Do not include `JWT_SECRET`; add a one-line warning comment that old `JWT_SECRET` deployments must rename it to `AUTH_SECRET_KEY`. + +### 9. New example files + +Add missing templates so every operational env file has an example: + +- `.env.current.example` + - local current-branch port offsets and admin demo values. +- `.env.master.example` + - local master-branch port offsets. +- `.env.e2e.example` + - e2e stack ports and test credentials. +- `frontend/.env.example` + - frontend/e2e visible variables if any are used; otherwise a short file saying frontend runtime config is generated/proxied via nginx and API calls. + +Do not modify real secret-bearing files: + +- `.env.enterprise-clean` +- `backend/.env` +- `frontend/.env` +- `frontend/.env.bak` +- `frontend/e2e/.env.e2e` + +### 10. Source code canonicalization + +Update `backend/src/agent/_jwt_decoder.py` to enforce the final canonical contract: + +- `AUTH_SECRET_KEY` is canonical. +- `JWT_SECRET` is not read and not accepted. +- If `AUTH_SECRET_KEY` is missing, fail fast with a clear `JWTError` / startup-visible error. +- If `JWT_SECRET` is present but `AUTH_SECRET_KEY` is absent, do not silently accept it; error message should instruct operator to rename `JWT_SECRET` to `AUTH_SECRET_KEY`. + +Behavior change required: remove the current dual-key fallback (`JWT_SECRET` first, then `AUTH_SECRET_KEY`). Decode backend-issued tokens using only `AUTH_SECRET_KEY`. + +## Validation plan + +1. Static env inventory + - Search `${VAR}` in all compose files and generated compose sections. + - Search `os.getenv`, `validation_alias`, and frontend `process.env`/`import.meta.env` usage. + - Verify every deploy-relevant variable appears in at least one matching example file. + +2. Compose config validation + - `docker compose --env-file .env.example -f docker-compose.yml config` + - `docker compose --env-file .env.enterprise-clean.example -f docker-compose.enterprise-clean.yml config` + - `docker compose --env-file .env.e2e.example -f docker-compose.e2e.yml config` + +3. Build script generated compose validation + - Run `bash -n build.sh`. + - Dry/read review generated heredoc sections for `bundle`, `bundle:embeddings`, and `bundle:light` to confirm same env contract. + - Optionally build a lightweight tag and inspect generated `dist/docker/docker-compose*.yml`. + +4. Agent smoke validation + - Run agent image import smoke test with only `AUTH_SECRET_KEY`, not `JWT_SECRET`. + - Run a negative smoke test with only `JWT_SECRET` and confirm startup/import path fails with migration guidance. + - Confirm no `AUTH_DATABASE_URL` is required by agent startup. + - Confirm JWT decoder accepts backend-issued tokens. + +5. Backend validation + - Confirm backend starts with canonical `.env.example`/enterprise env contract. + - Run targeted tests: + - `pytest backend/tests/test_agent/` + - `pytest backend/tests/test_core/test_auth_config.py backend/tests/test_core/test_jwt.py` + +6. Docs sanity + - Ensure no examples tell operators to add `AUTH_DATABASE_URL` to the agent. + - Ensure no compose/example/generated bundle file contains `JWT_SECRET` as a usable variable. + - Ensure generated bundle instructions mention `AUTH_SECRET_KEY` as required. + +## Expected outcome + +- One canonical security secret name: `AUTH_SECRET_KEY`. +- No `JWT_SECRET` fallback remains in agent runtime or Docker examples. +- All compose files and generated bundle compose use the same names. +- Every operational env file has a corresponding example. +- Enterprise operators no longer need to guess why `.env.enterprise-clean.example`, root `.env.example`, backend `.env.example`, and agent `.env.agent.example` differ. +- Agent remains slim and independent from backend auth DB config. diff --git a/backend/.env.example b/backend/.env.example index bc53cebf..aa0291af 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,22 +1,49 @@ # ====================================================================== # superset-tools — backend .env.example -# Только обязательные переменные. Остальное настраивается через web UI. +# Используется для прямого запуска бекенда (без docker compose). +# При использовании docker-compose — переменные задаются в compose/environment. # ====================================================================== -# ── Обязательно: секреты ── -# JWT-ключ подписи токенов (backend + agent используют один) -# Сгенерировать: python -c "import secrets; print(secrets.token_urlsafe(32))" -AUTH_SECRET_KEY=change-me-to-a-random-secret +# ── Безопасность (ОБЯЗАТЕЛЬНО) ───────────────────────────────────────── +# JWT-ключ подписи токенов — единый для backend и agent. +# Сгенерировать: python3 -c "import secrets; print(secrets.token_urlsafe(32))" +AUTH_SECRET_KEY=change-me-to-a-random-secret-32-chars-min -# Fernet-ключ шифрования паролей подключений и API-ключей +# Fernet-ключ шифрования паролей подключений и API-ключей. # Сгенерировать: python3 -c "import base64,os; print(base64.urlsafe_b64encode(os.urandom(32)).decode())" -# Пример (сгенерируйте свой для прода): ENCRYPTION_KEY=3YIxOr3_GFht9ZyjRQMqOdtuO1CKOj8Y9mpY89iMbZY= -# ── Обязательно: база данных ── -# PostgreSQL (пример для локальной разработки через docker-compose) +# JWT audience / issuer (опционально) +# JWT_AUDIENCE=superset-tools-api +# JWT_ISSUER=superset-tools + +# ── База данных (ОБЯЗАТЕЛЬНО) ────────────────────────────────────────── DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools -# Отдельная БД для аутентификации (если не задана — требуется явно) AUTH_DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools -# Отдельная БД для задач (если не задана — DATABASE_URL) TASKS_DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/ss_tools + +# ── Admin bootstrap ──────────────────────────────────────────────────── +# INITIAL_ADMIN_CREATE=true +# INITIAL_ADMIN_USERNAME=admin +# INITIAL_ADMIN_PASSWORD=change-me + +# ── LLM / провайдеры ─────────────────────────────────────────────────── +OPENAI_API_KEY= +ANTHROPIC_API_KEY= +LLM_SSL_VERIFY=true +LLM_CA_CERT_URLS= + +# ── CORS / Безопасность ──────────────────────────────────────────────── +ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 +FORCE_HTTPS=false +APP_TIMEZONE=Europe/Moscow + +# ── ADFS SSO (опционально) ───────────────────────────────────────────── +# ADFS_CLIENT_ID= +# ADFS_CLIENT_SECRET= +# ADFS_METADATA_URL= + +# ── OpenRouter (опционально) ─────────────────────────────────────────── +# OPENROUTER_SITE_URL= +# OPENROUTER_APP_NAME= +# APP_BASE_URL= diff --git a/backend/src/agent/_jwt_decoder.py b/backend/src/agent/_jwt_decoder.py index 71ee3f02..69919cad 100644 --- a/backend/src/agent/_jwt_decoder.py +++ b/backend/src/agent/_jwt_decoder.py @@ -1,52 +1,50 @@ # backend/src/agent/_jwt_decoder.py # #region AgentChat.JwtDecoder [C:1] [TYPE Module] [SEMANTICS agent-chat,jwt,decode] -# @BRIEF Lightweight JWT decode for agent — uses JWT_SECRET env var directly, avoids +# @BRIEF Lightweight JWT decode for agent — uses AUTH_SECRET_KEY env var, avoids # pulling src.core.auth.jwt which requires AUTH_DATABASE_URL and ORM deps. # @RATIONALE The agent only needs stateless JWT validation (exp, sub, signature). # Token blacklisting is only relevant for backend auth flow — the agent # processes one request at a time and doesn't need DB-backed revocation. +# @INVARIANT AUTH_SECRET_KEY is the ONLY accepted JWT signing key. JWT_SECRET is +# unsupported and triggers a migration message if present without +# AUTH_SECRET_KEY. # @REJECTED Importing src.core.auth.jwt.decode_token was rejected — it drags in -# AuthConfig → AUTH_DATABASE_URL/SECRET_KEY validators → SQLAlchemy models, +# AuthConfig -> AUTH_DATABASE_URL/SECRET_KEY validators -> SQLAlchemy models, # bloating the agent image with backend infrastructure it doesn't need. +# @REJECTED Fallback from JWT_SECRET to AUTH_SECRET_KEY was rejected — ambiguous +# naming caused operator confusion and secret drift between environments. import os from jose import JWTError, jwt def decode_token(token: str) -> dict: - """Decode and validate a JWT token. Tries JWT_SECRET first, then AUTH_SECRET_KEY. + """Decode and validate a JWT token using AUTH_SECRET_KEY from environment. Performs stateless validation: signature, expiration, and required claims (exp, sub). Does NOT check token blacklist or audience/issuer. - """ - keys = [] - jwt_key = os.getenv("JWT_SECRET", "") - auth_key = os.getenv("AUTH_SECRET_KEY", "") - if jwt_key: - keys.append(jwt_key) - if auth_key and auth_key != jwt_key: - keys.append(auth_key) - if not keys: - raise JWTError("Neither JWT_SECRET nor AUTH_SECRET_KEY is set") - algorithm = os.getenv("JWT_ALGORITHM", "HS256") - last_error = None - for key in keys: - try: - return jwt.decode( - token, - key, - algorithms=[algorithm], - options={ - "verify_signature": True, - "verify_exp": True, - "verify_aud": False, - "require": ["exp", "sub"], - }, - ) - except JWTError as e: - last_error = e - raise last_error + Raises JWTError with migration guidance if JWT_SECRET is set but + AUTH_SECRET_KEY is missing (old deployments must rename the variable). + """ + secret = os.getenv("AUTH_SECRET_KEY", "") + jwt_secret_legacy = os.getenv("JWT_SECRET", "") + if not secret: + if jwt_secret_legacy: + raise JWTError("JWT_SECRET is no longer supported. Rename JWT_SECRET to AUTH_SECRET_KEY in your .env / docker-compose and restart the agent.") + raise JWTError("AUTH_SECRET_KEY environment variable is not set") + + return jwt.decode( + token, + secret, + algorithms=[os.getenv("JWT_ALGORITHM", "HS256")], + options={ + "verify_signature": True, + "verify_exp": True, + "verify_aud": False, + "require": ["exp", "sub"], + }, + ) # #endregion AgentChat.JwtDecoder diff --git a/backend/tests/test_agent/test_agent_handler.py b/backend/tests/test_agent/test_agent_handler.py index 3ec209a5..d959b8a3 100644 --- a/backend/tests/test_agent/test_agent_handler.py +++ b/backend/tests/test_agent/test_agent_handler.py @@ -12,9 +12,9 @@ from unittest.mock import AsyncMock, MagicMock, patch import jwt -# Set JWT_SECRET and LLM_API_KEY for tests -JWT_SECRET = "test-jwt-secret-key" -os.environ["JWT_SECRET"] = JWT_SECRET +# Set AUTH_SECRET_KEY and LLM_API_KEY for tests (match conftest) +os.environ.setdefault("AUTH_SECRET_KEY", "test-secret-key-for-jwt-testing") +AUTH_SECRET_KEY = os.environ["AUTH_SECRET_KEY"] os.environ["OPENAI_API_KEY"] = "sk-test-key" os.environ["LLM_API_KEY"] = "sk-test-key" os.environ["LLM_MODEL"] = "gpt-4o" @@ -33,7 +33,7 @@ def mock_save_conversation(): def _make_test_jwt(user_id: str = "test-user") -> str: - return jwt.encode({"sub": user_id}, JWT_SECRET, algorithm="HS256") + return jwt.encode({"sub": user_id}, AUTH_SECRET_KEY, algorithm="HS256") def _empty_agent_state(): @@ -68,6 +68,8 @@ async def test_handler_empty_message_returns_immediately(): # create_agent should NOT be called for empty messages # (it gets called currently — future optimization) # mock_create.assert_not_called() # TODO: optimize to skip graph for empty msg + + # #endregion TestAgentChat.Handler.EmptyMessage @@ -92,10 +94,12 @@ async def test_handler_missing_auth_continues_gracefully(): # Patch create_agent to prevent LLM call — handler should proceed without JWT with patch("src.agent.app.create_agent") as mock_create: mock_graph = AsyncMock() + async def _empty_stream(*_args, **_kwargs): """Empty async generator — yields nothing.""" return yield # make it a generator + mock_graph.astream_events = _empty_stream mock_graph.aget_state = AsyncMock(return_value=_empty_agent_state()) mock_create.return_value = mock_graph @@ -107,10 +111,10 @@ async def test_handler_missing_auth_continues_gracefully(): # No UNAUTHORIZED error — handler proceeds with empty user_jwt for r in results: import json + parsed = json.loads(r) if isinstance(r, str) else r meta = parsed.get("metadata", {}) - assert meta.get("code") != "UNAUTHORIZED", \ - "Handler should not reject missing auth — JWT optional at Gradio layer" + assert meta.get("code") != "UNAUTHORIZED", "Handler should not reject missing auth — JWT optional at Gradio layer" @pytest.mark.anyio @@ -127,9 +131,11 @@ async def test_handler_invalid_jwt_continues_gracefully(): with patch("src.agent.app.create_agent") as mock_create: mock_graph = AsyncMock() + async def _empty_stream(*_args, **_kwargs): return yield + mock_graph.astream_events = _empty_stream mock_graph.aget_state = AsyncMock(return_value=_empty_agent_state()) mock_create.return_value = mock_graph @@ -140,10 +146,12 @@ async def test_handler_invalid_jwt_continues_gracefully(): for r in results: import json + parsed = json.loads(r) if isinstance(r, str) else r meta = parsed.get("metadata", {}) - assert meta.get("code") != "UNAUTHORIZED", \ - "Handler should not reject invalid JWT — invalid token ignored at Gradio layer" + assert meta.get("code") != "UNAUTHORIZED", "Handler should not reject invalid JWT — invalid token ignored at Gradio layer" + + # #endregion TestAgentChat.Handler.AuthError @@ -180,11 +188,11 @@ async def test_handler_yields_stream_tokens(): assert len(results) > 0, "Should yield at least one chunk" import json - stream_tokens = [ - r for r in results - if json.loads(r)["metadata"]["type"] == "stream_token" - ] + + stream_tokens = [r for r in results if json.loads(r)["metadata"]["type"] == "stream_token"] assert len(stream_tokens) > 0, "Should yield stream_token metadata" + + # #endregion TestAgentChat.Handler.Streaming @@ -207,9 +215,11 @@ async def test_handler_resume_confirm(): # imports create_agent directly from langgraph_setup with patch("src.agent._confirmation.create_agent") as mock_create: mock_graph = MagicMock() + async def _empty_stream(*_args, **_kwargs): return yield + mock_graph.astream_events = _empty_stream mock_create.return_value = mock_graph @@ -220,10 +230,13 @@ async def test_handler_resume_confirm(): # Should yield confirm_resolved metadata assert len(results) == 1 import json + parsed = json.loads(results[0]) if isinstance(results[0], str) else results[0] assert parsed["metadata"]["type"] == "confirm_resolved" assert parsed["metadata"]["result"] == "confirmed" assert mock_create.call_args.kwargs["interrupt_before"] == [] + + # #endregion TestAgentChat.Handler.ResumeConfirm @@ -251,8 +264,11 @@ async def test_handler_resume_deny(): assert len(results) == 1 import json + parsed = json.loads(results[0]) if isinstance(results[0], str) else results[0] assert parsed["metadata"]["type"] == "confirm_resolved" assert parsed["metadata"]["result"] == "denied" + + # #endregion TestAgentChat.Handler.ResumeDeny # #endregion TestAgentChat.Handler diff --git a/backend/tests/test_agent/test_confirmations.py b/backend/tests/test_agent/test_confirmations.py index 592d4d28..e121aeeb 100644 --- a/backend/tests/test_agent/test_confirmations.py +++ b/backend/tests/test_agent/test_confirmations.py @@ -16,8 +16,8 @@ import pytest import jwt -JWT_SECRET = "test-jwt-secret-key" -os.environ["JWT_SECRET"] = JWT_SECRET +os.environ.setdefault("AUTH_SECRET_KEY", "test-secret-key-for-jwt-testing") +AUTH_SECRET_KEY = os.environ["AUTH_SECRET_KEY"] os.environ["OPENAI_API_KEY"] = "sk-test-key" os.environ["LLM_API_KEY"] = "sk-test-key" os.environ["LLM_MODEL"] = "gpt-4o" @@ -25,7 +25,7 @@ os.environ["LLM_BASE_URL"] = "https://api.openai.com/v1" def _make_test_jwt(user_id: str = "test-user") -> str: - return jwt.encode({"sub": user_id}, JWT_SECRET, algorithm="HS256") + return jwt.encode({"sub": user_id}, AUTH_SECRET_KEY, algorithm="HS256") @pytest.fixture(autouse=True) @@ -37,6 +37,7 @@ def mock_save_conversation(): # #region TestAgentChat.Confirmations.Concurrent [C:2] [TYPE Function] [SEMANTICS test,confirmation,concurrent] # @BRIEF Concurrent send lock prevents multiple simultaneous sends from same user. + @pytest.mark.asyncio async def test_concurrent_send_lock(): """Handler rejects concurrent sends from same user with CONCURRENT_SEND error.""" @@ -58,17 +59,21 @@ async def test_concurrent_send_lock(): assert len(results) == 1 import json + parsed = json.loads(results[0]) if isinstance(results[0], str) else results[0] assert parsed["metadata"]["code"] == "CONCURRENT_SEND" # Clean up _user_locks.pop("admin", None) + + # #endregion TestAgentChat.Confirmations.Concurrent # #region TestAgentChat.Confirmations.UnknownAction [C:2] [TYPE Function] [SEMANTICS test,confirmation,unknown] # @BRIEF Non-confirm/deny action values are treated as normal messages. + @pytest.mark.asyncio async def test_handler_unknown_action_treated_as_normal(): """Handler with unknown action string proceeds as normal message send.""" @@ -84,8 +89,10 @@ async def test_handler_unknown_action_treated_as_normal(): with patch("src.agent.app.create_agent") as mock_create: mock_graph = MagicMock() + async def _mock_stream(*_args, **_kwargs): yield {"event": "on_chat_model_stream", "data": {"chunk": MagicMock(content="Hello")}} + mock_graph.astream_events = _mock_stream state = MagicMock(spec_set=["next"]) state.next = () @@ -99,15 +106,19 @@ async def test_handler_unknown_action_treated_as_normal(): # Should produce stream tokens, not confirm_resolved assert len(results) > 0 import json + meta_types = [json.loads(r)["metadata"]["type"] for r in results] assert "stream_token" in meta_types assert "confirm_resolved" not in meta_types + + # #endregion TestAgentChat.Confirmations.UnknownAction # #region TestAgentChat.Confirmations.ExpiredState [C:2] [TYPE Function] [SEMANTICS test,confirmation,expired] # @BRIEF Stale confirmations — checkpoint no longer available. + @pytest.mark.asyncio async def test_handler_confirm_no_graph(): """Handler with action='confirm' but create_agent raises handles it gracefully.""" @@ -131,5 +142,7 @@ async def test_handler_confirm_no_graph(): # Exception may propagate or be caught — either is acceptable except Exception: pass + + # #endregion TestAgentChat.Confirmations.ExpiredState # #endregion TestAgentChat.Confirmations diff --git a/backend/tests/test_agent/test_langchain_tools.py b/backend/tests/test_agent/test_langchain_tools.py index f63a5ba3..d38a062d 100644 --- a/backend/tests/test_agent/test_langchain_tools.py +++ b/backend/tests/test_agent/test_langchain_tools.py @@ -13,7 +13,7 @@ sys.path.append(str(Path(__file__).parent.parent.parent / "src")) import pytest -os.environ["JWT_SECRET"] = "test-jwt-secret-key" +os.environ.setdefault("AUTH_SECRET_KEY", "test-secret-key-for-jwt-testing") os.environ["FASTAPI_URL"] = "http://test-backend:8000" os.environ["SERVICE_JWT"] = "test-service-jwt" os.environ["OPENAI_API_KEY"] = "sk-test-key" @@ -27,6 +27,7 @@ def anyio_backend(): # #region TestAgentChat.Tools.DualAuth [C:2] [TYPE Function] [SEMANTICS test,tools,auth] # @BRIEF Dual-identity auth headers built from ContextVar and env vars. + @pytest.mark.anyio async def test_tool_dual_auth_headers(): """Tools should build auth headers from ContextVar when set.""" @@ -56,12 +57,15 @@ async def test_tool_dual_auth_headers(): assert "Authorization" in headers, "Should include Authorization header" assert headers["Authorization"] == "Bearer service-jwt-token" assert headers["X-User-JWT"] == "user-jwt-token" + + # #endregion TestAgentChat.Tools.DualAuth # #region TestAgentChat.Tools.FallbackAuth [C:2] [TYPE Function] [SEMANTICS test,tools,auth,fallback] # @BRIEF Dual-identity auth falls back to env var when ContextVar is not set. + @pytest.mark.anyio async def test_tool_auth_fallback_to_env(): """Tools should fall back to SERVICE_JWT env var when ContextVar is empty.""" @@ -74,8 +78,7 @@ async def test_tool_auth_fallback_to_env(): set_service_jwt("") os.environ["SERVICE_JWT"] = "env-service-token" - with patch.object(tools_mod, "FASTAPI_URL", "http://test-backend:8000"), \ - patch("httpx.AsyncClient") as mock_client: + with patch.object(tools_mod, "FASTAPI_URL", "http://test-backend:8000"), patch("httpx.AsyncClient") as mock_client: mock_instance = AsyncMock() mock_client.return_value.__aenter__.return_value = mock_instance mock_instance.get.return_value = Mock( @@ -93,12 +96,15 @@ async def test_tool_auth_fallback_to_env(): headers = kwargs.get("headers", {}) # Should use env var assert "Authorization" in headers + + # #endregion TestAgentChat.Tools.FallbackAuth # #region TestAgentChat.Tools.HttpFailure [C:2] [TYPE Function] [SEMANTICS test,tools,failure] # @BRIEF Tool handles HTTP failure gracefully (returns error text, not exception). + @pytest.mark.anyio async def test_tool_http_exception_handling(): """Tool should propagate HTTP exception as error text.""" @@ -116,12 +122,15 @@ async def test_tool_http_exception_handling(): # Should propagate the exception (caller handles error) with pytest.raises((Exception,)): await search_dashboards.ainvoke({"query": "test"}) + + # #endregion TestAgentChat.Tools.HttpFailure # #region TestAgentChat.Tools.GetAll [C:2] [TYPE Function] [SEMANTICS test,tools,registry] # @BRIEF get_all_tools returns the expected list of tool functions. + def test_get_all_tools_returns_expected_list(): """get_all_tools() should return search_dashboards, get_health_summary, etc.""" from src.agent.tools import get_all_tools @@ -169,6 +178,7 @@ def test_get_all_tools_args_schema(): # Tool get_all — full catalog (replaces get_tools_for_query) # ═══════════════════════════════════════════════════════════════════ + def test_get_all_tools_returns_24_tools(): """get_all_tools() returns full catalog — all 24 tools registered.""" from src.agent.tools import get_all_tools @@ -181,12 +191,15 @@ def test_get_all_tools_returns_24_tools(): assert "get_health_summary" in tool_names # Regression: minimum count — must have all 24 assert len(tools) >= 24, f"Expected ≥24 tools, got {len(tools)}: {tool_names}" + + # #endregion TestAgentChat.Tools.GetAll # #region TestAgentChat.Tools.ToolContracts [C:2] [TYPE Function] [SEMANTICS test,tools,contract] # @BRIEF Tool contracts match @POST and @PRE declared in contracts/modules.md. + @pytest.mark.anyio async def test_search_dashboards_correct_url(): """search_dashboards calls GET /api/dashboards with query params.""" @@ -212,12 +225,15 @@ async def test_search_dashboards_correct_url(): args, kwargs = call_args url = args[0] if args else kwargs.get("url", "") assert "api/dashboards" in url + + # #endregion TestAgentChat.Tools.ToolContracts # #region TestAgentChat.Tools.HealthSummary [C:2] [TYPE Function] [SEMANTICS test,tools,health] # @BRIEF get_health_summary calls the correct FastAPI endpoint. + @pytest.mark.anyio async def test_get_health_summary_calls_correct_url(): """get_health_summary should call GET /api/health/summary.""" @@ -241,12 +257,15 @@ async def test_get_health_summary_calls_correct_url(): url = args[0] if args else kwargs.get("url", "") assert "api/health/summary" in url assert kwargs.get("params") == {"environment_id": "ss-dev"} + + # #endregion TestAgentChat.Tools.HealthSummary # #region TestAgentChat.Tools.ListEnvironments [C:2] [TYPE Function] [SEMANTICS test,tools,environments] # @BRIEF list_environments calls the correct FastAPI endpoint. + @pytest.mark.anyio async def test_list_environments_calls_correct_url(): """list_environments should call GET /api/settings/environments.""" @@ -284,10 +303,7 @@ async def test_list_environments_redacts_sensitive_fields(): mock_instance = AsyncMock() mock_client.return_value.__aenter__.return_value = mock_instance mock_instance.get.return_value.status_code = 200 - mock_instance.get.return_value.text = ( - '[{"id":"prod","password":"secret-pass","api_key":"secret-key",' - '"nested":{"token":"secret-token"},"name":"ss-prod"}]' - ) + mock_instance.get.return_value.text = '[{"id":"prod","password":"secret-pass","api_key":"secret-key","nested":{"token":"secret-token"},"name":"ss-prod"}]' result = await list_environments.ainvoke({}) @@ -295,12 +311,15 @@ async def test_list_environments_redacts_sensitive_fields(): assert "secret-key" not in result assert "secret-token" not in result assert result.count("[redacted]") == 3 + + # #endregion TestAgentChat.Tools.ListEnvironments # #region TestAgentChat.Tools.TaskStatus [C:2] [TYPE Function] [SEMANTICS test,tools,task] # @BRIEF get_task_status calls the correct FastAPI endpoint with task_id. + @pytest.mark.anyio async def test_get_task_status_calls_correct_url(): """get_task_status should call GET /api/tasks/{task_id}.""" @@ -323,6 +342,8 @@ async def test_get_task_status_calls_correct_url(): args, kwargs = call_args url = args[0] if args else kwargs.get("url", "") assert "api/tasks/task-123" in url + + # #endregion TestAgentChat.Tools.TaskStatus @@ -380,6 +401,7 @@ async def test_deploy_dashboard_posts_git_endpoint(): # #region TestAgentChat.Tools.DualAuthHeaders [C:2] [TYPE Function] [SEMANTICS test,tools,auth,headers] # @BRIEF _dual_auth_headers builds proper headers from ContextVars. + def test_dual_auth_headers_with_both_jwts(): """_dual_auth_headers uses service auth plus user identity when both are set.""" from src.agent.context import set_service_jwt, set_user_jwt @@ -418,5 +440,7 @@ def test_dual_auth_headers_no_jwts(): # (set at import time based on os.environ) assert "Authorization" in headers assert headers["Authorization"].startswith("Bearer ") + + # #endregion TestAgentChat.Tools.DualAuthHeaders # #endregion TestAgentChat.Tools diff --git a/build.sh b/build.sh index 73b8797d..321ca3c5 100755 --- a/build.sh +++ b/build.sh @@ -264,6 +264,8 @@ services: TASKS_DATABASE_URL: postgresql+psycopg2://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@\${POSTGRES_HOST:?Set POSTGRES_HOST}:\${POSTGRES_PORT:-5432}/\${POSTGRES_DB:-ss_tools} AUTH_DATABASE_URL: postgresql+psycopg2://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@\${POSTGRES_HOST:?Set POSTGRES_HOST}:\${POSTGRES_PORT:-5432}/\${POSTGRES_DB:-ss_tools} BACKEND_PORT: 8000 + AUTH_SECRET_KEY: \${AUTH_SECRET_KEY:?Set AUTH_SECRET_KEY in .env} + ENCRYPTION_KEY: \${ENCRYPTION_KEY:?Set ENCRYPTION_KEY in .env} ENABLE_BELIEF_STATE_LOGGING: \${ENABLE_BELIEF_STATE_LOGGING:-true} TASK_LOG_LEVEL: \${TASK_LOG_LEVEL:-INFO} INITIAL_ADMIN_CREATE: \${INITIAL_ADMIN_CREATE:-false} @@ -306,8 +308,8 @@ services: LLM_BASE_URL: \${LLM_BASE_URL:-https://api.openai.com/v1} LLM_MODEL: \${LLM_MODEL:-gpt-4o} FASTAPI_URL: http://backend:8000 - JWT_SECRET: \${JWT_SECRET:-super-secret-key} - SERVICE_JWT: \${SERVICE_JWT:-agent-service-secret} + AUTH_SECRET_KEY: \${AUTH_SECRET_KEY:?Set AUTH_SECRET_KEY in .env} + SERVICE_JWT: \${SERVICE_JWT:?Set SERVICE_JWT in .env} DATABASE_URL: postgresql+psycopg2://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@\${POSTGRES_HOST:?Set POSTGRES_HOST}:\${POSTGRES_PORT:-5432}/\${POSTGRES_DB:-ss_tools} GRADIO_SERVER_PORT: 7860 ports: @@ -568,6 +570,8 @@ services: TASKS_DATABASE_URL: postgresql+psycopg2://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@\${POSTGRES_HOST:?Set POSTGRES_HOST}:\${POSTGRES_PORT:-5432}/\${POSTGRES_DB:-ss_tools} AUTH_DATABASE_URL: postgresql+psycopg2://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@\${POSTGRES_HOST:?Set POSTGRES_HOST}:\${POSTGRES_PORT:-5432}/\${POSTGRES_DB:-ss_tools} BACKEND_PORT: 8000 + AUTH_SECRET_KEY: \${AUTH_SECRET_KEY:?Set AUTH_SECRET_KEY in .env} + ENCRYPTION_KEY: \${ENCRYPTION_KEY:?Set ENCRYPTION_KEY in .env} ENABLE_BELIEF_STATE_LOGGING: \${ENABLE_BELIEF_STATE_LOGGING:-true} TASK_LOG_LEVEL: \${TASK_LOG_LEVEL:-INFO} INITIAL_ADMIN_CREATE: \${INITIAL_ADMIN_CREATE:-false} @@ -610,8 +614,8 @@ services: LLM_BASE_URL: \${LLM_BASE_URL:-https://api.openai.com/v1} LLM_MODEL: \${LLM_MODEL:-gpt-4o} FASTAPI_URL: http://backend:8000 - JWT_SECRET: \${JWT_SECRET:-super-secret-key} - SERVICE_JWT: \${SERVICE_JWT:-agent-service-secret} + AUTH_SECRET_KEY: \${AUTH_SECRET_KEY:?Set AUTH_SECRET_KEY in .env} + SERVICE_JWT: \${SERVICE_JWT:?Set SERVICE_JWT in .env} DATABASE_URL: postgresql+psycopg2://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@\${POSTGRES_HOST:?Set POSTGRES_HOST}:\${POSTGRES_PORT:-5432}/\${POSTGRES_DB:-ss_tools} GRADIO_SERVER_PORT: 7860 ports: @@ -709,12 +713,14 @@ Commands for release bundles: Agent: NO sentence-transformers/embeddings (~500 MB saved). Backend: NO agent/ML/dev deps. NOTE: external PostgreSQL only — no bundled postgres image. + REQUIRED: AUTH_SECRET_KEY, ENCRYPTION_KEY, POSTGRES_PASSWORD, SERVICE_JWT. Example: ./build.sh bundle v1.0.0 bundle:embeddings Enterprise bundle with semantic embedding routing. Agent built WITH sentence-transformers+torch (larger image). Same backend/frontend as default bundle. + REQUIRED: AUTH_SECRET_KEY, ENCRYPTION_KEY, POSTGRES_PASSWORD, SERVICE_JWT. Example: ./build.sh bundle:embeddings v1.0.0 bundle:light Lightweight all-in-one image (.tar.xz, <200 MB, no Playwright) diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml index 23d8634b..8d0a78d8 100644 --- a/docker-compose.e2e.yml +++ b/docker-compose.e2e.yml @@ -34,11 +34,12 @@ services: db: condition: service_healthy environment: - POSTGRES_URL: postgresql+psycopg2://postgres:postgres@db:5432/ss_tools DATABASE_URL: postgresql+psycopg2://postgres:postgres@db:5432/ss_tools TASKS_DATABASE_URL: postgresql+psycopg2://postgres:postgres@db:5432/ss_tools AUTH_DATABASE_URL: postgresql+psycopg2://postgres:postgres@db:5432/ss_tools BACKEND_PORT: 8000 + AUTH_SECRET_KEY: ${AUTH_SECRET_KEY:?Set AUTH_SECRET_KEY in .env.e2e} + SERVICE_JWT: ${SERVICE_JWT:-e2e-service-secret} INITIAL_ADMIN_CREATE: "true" INITIAL_ADMIN_USERNAME: admin INITIAL_ADMIN_PASSWORD: admin123 diff --git a/docker-compose.enterprise-clean.yml b/docker-compose.enterprise-clean.yml index 51186960..cf70a4c9 100644 --- a/docker-compose.enterprise-clean.yml +++ b/docker-compose.enterprise-clean.yml @@ -42,15 +42,14 @@ services: TASKS_DATABASE_URL: postgresql+psycopg2://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env.enterprise-clean}@${POSTGRES_HOST:?Set POSTGRES_HOST in .env.enterprise-clean}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-ss_tools} AUTH_DATABASE_URL: postgresql+psycopg2://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env.enterprise-clean}@${POSTGRES_HOST:?Set POSTGRES_HOST in .env.enterprise-clean}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-ss_tools} BACKEND_PORT: 8000 + AUTH_SECRET_KEY: ${AUTH_SECRET_KEY:?Set AUTH_SECRET_KEY in .env.enterprise-clean} + ENCRYPTION_KEY: ${ENCRYPTION_KEY:?ENCRYPTION_KEY обязателен — сгенерируйте и укажите в .env.enterprise-clean} ENABLE_BELIEF_STATE_LOGGING: ${ENABLE_BELIEF_STATE_LOGGING:-true} TASK_LOG_LEVEL: ${TASK_LOG_LEVEL:-INFO} INITIAL_ADMIN_CREATE: ${INITIAL_ADMIN_CREATE:-false} INITIAL_ADMIN_USERNAME: ${INITIAL_ADMIN_USERNAME:-admin} INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD:-} INITIAL_ADMIN_EMAIL: ${INITIAL_ADMIN_EMAIL:-} - # Обязательно: задайте ENCRYPTION_KEY в .env.enterprise-clean - # Сгенерируйте: python3 -c "import base64,os; print(base64.urlsafe_b64encode(os.urandom(32)).decode())" - ENCRYPTION_KEY: ${ENCRYPTION_KEY:?ENCRYPTION_KEY обязателен — сгенерируйте и укажите в .env.enterprise-clean} OPENAI_API_KEY: ${OPENAI_API_KEY:-} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} FEATURES__DATASET_REVIEW: ${FEATURES__DATASET_REVIEW:-true} @@ -103,7 +102,7 @@ services: LLM_BASE_URL: ${LLM_BASE_URL:-https://api.openai.com/v1} LLM_MODEL: ${LLM_MODEL:-gpt-4o} FASTAPI_URL: http://backend:8000 - JWT_SECRET: ${JWT_SECRET:?JWT_SECRET must be set — crash-early, no default fallback} + AUTH_SECRET_KEY: ${AUTH_SECRET_KEY:?Set AUTH_SECRET_KEY in .env.enterprise-clean} SERVICE_JWT: ${SERVICE_JWT:-agent-service-secret} DATABASE_URL: postgresql+psycopg2://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env.enterprise-clean}@${POSTGRES_HOST:?Set POSTGRES_HOST in .env.enterprise-clean}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-ss_tools} GRADIO_SERVER_PORT: 7860 diff --git a/docker-compose.yml b/docker-compose.yml index 71bcf6a3..2c256f46 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: LLM_BASE_URL: ${LLM_BASE_URL:-https://api.openai.com/v1} LLM_MODEL: ${LLM_MODEL:-gpt-4o} FASTAPI_URL: http://backend:8000 - JWT_SECRET: ${JWT_SECRET:?JWT_SECRET must be set — crash-early, no default fallback} + AUTH_SECRET_KEY: ${AUTH_SECRET_KEY:?Set AUTH_SECRET_KEY in .env} SERVICE_JWT: ${SERVICE_JWT:-agent-service-secret} DATABASE_URL: postgresql+psycopg2://postgres:postgres@db:5432/ss_tools GRADIO_SERVER_PORT: 7860 diff --git a/docker/.env.agent.example b/docker/.env.agent.example index f5be66a0..3c10b11b 100644 --- a/docker/.env.agent.example +++ b/docker/.env.agent.example @@ -1,6 +1,37 @@ # Agent container env vars -# LLM конфигурация берётся из FastAPI /api/agent/llm-config (настраивается через web UI). -AUTH_SECRET_KEY= # JWT-ключ (общий с backend) -FASTAPI_URL=http://backend:8000 # URL основного API -SERVICE_JWT= # Сервисный токен для agent→backend -DATABASE_URL=postgresql+psycopg2://postgres:postgres@db:5432/ss_tools # БД для checkpoint'ов +# Используется для прямого запуска агента (без docker compose). +# При использовании docker-compose — переменные задаются в compose/environment. +# ВАЖНО: AUTH_SECRET_KEY должен совпадать с ключом бекенда. + +# ── Безопасность (ОБЯЗАТЕЛЬНО) ───────────────────────────────────────── +# JWT-ключ подписи токенов — единый для backend и agent. +# Сгенерировать: python3 -c "import secrets; print(secrets.token_urlsafe(32))" +AUTH_SECRET_KEY= + +# Сервисный токен для agent→backend вызовов. +SERVICE_JWT=agent-service-secret + +# ── Подключение ──────────────────────────────────────────────────────── +FASTAPI_URL=http://backend:8000 +DATABASE_URL=postgresql+psycopg2://postgres:postgres@db:5432/ss_tools + +# ── LLM ──────────────────────────────────────────────────────────────── +LLM_API_KEY= +LLM_BASE_URL=https://api.openai.com/v1 +LLM_MODEL=gpt-4o + +# ── Gradio ───────────────────────────────────────────────────────────── +GRADIO_SERVER_NAME=0.0.0.0 +GRADIO_SERVER_PORT=7860 +# GRADIO_ALLOW_PORT_FALLBACK=true + +# ── Тонкие настройки агента ──────────────────────────────────────────── +# AGENT_ENABLE_LLM_TITLES=true +# AGENT_TITLE_GENERATION_TIMEOUT_S=0.25 +# AGENT_PREFETCH_DASHBOARD_LIMIT=25 +# AGENT_CONFIRM_TOOLS=false +# AGENT_INTERRUPT_BEFORE= + +# ── Embedding routing (только для bundle:embeddings) ──────────────────── +# EMBEDDING_SIMILARITY_THRESHOLD=0.65 +# EMBEDDING_TOP_K=5 diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 00000000..d39afa2f --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,9 @@ +# Frontend dev config +# Используется при запуске npm run dev (Vite dev server). +# В production (docker compose) фронтенд обслуживается nginx, +# переменные окружения задаются docker-compose.yml или Dockerfile. + +# URL бекенда для API-прокси в dev-режиме +PUBLIC_BACKEND_URL=http://127.0.0.1:8000 +# URL агента для Gradio-прокси в dev-режиме +PUBLIC_GRADIO_URL=http://127.0.0.1:7860 diff --git a/run.sh b/run.sh index a4d83e91..f2fee678 100755 --- a/run.sh +++ b/run.sh @@ -202,14 +202,14 @@ ensure_jwt_secret() { # Extract existing key from .env if [ -f "$ENV_FILE" ]; then # shellcheck disable=SC2013 - for line in $(grep "^JWT_SECRET=" "$ENV_FILE" | head -1); do - key="${line#JWT_SECRET=}" + for line in $(grep "^AUTH_SECRET_KEY=" "$ENV_FILE" | head -1); do + key="${line#AUTH_SECRET_KEY=}" done fi # Validate existing key (must be non-empty and reasonably long) if [ -n "$key" ] && [ ${#key} -ge 16 ]; then - echo "JWT secret preflight: existing JWT_SECRET reused from $ENV_FILE" + echo "JWT secret preflight: existing AUTH_SECRET_KEY reused from $ENV_FILE" return fi @@ -217,17 +217,17 @@ ensure_jwt_secret() { local new_key new_key=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))") - if [ -f "$ENV_FILE" ] && grep -q "^JWT_SECRET=" "$ENV_FILE"; then + if [ -f "$ENV_FILE" ] && grep -q "^AUTH_SECRET_KEY=" "$ENV_FILE"; then # Replace invalid key if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s/^JWT_SECRET=.*/JWT_SECRET=$new_key/" "$ENV_FILE" + sed -i '' "s/^AUTH_SECRET_KEY=.*/AUTH_SECRET_KEY=$new_key/" "$ENV_FILE" else - sed -i "s/^JWT_SECRET=.*/JWT_SECRET=$new_key/" "$ENV_FILE" + sed -i "s/^AUTH_SECRET_KEY=.*/AUTH_SECRET_KEY=$new_key/" "$ENV_FILE" fi else - echo "JWT_SECRET=$new_key" >> "$ENV_FILE" + echo "AUTH_SECRET_KEY=$new_key" >> "$ENV_FILE" fi - echo "JWT secret preflight: JWT_SECRET generated and saved to $ENV_FILE" + echo "JWT secret preflight: AUTH_SECRET_KEY generated and saved to $ENV_FILE" } ensure_jwt_secret