refactor(env): unify Docker env vars — canonical AUTH_SECRET_KEY, remove JWT_SECRET fallback
Canonical variables:
- AUTH_SECRET_KEY — JWT signing key for backend + agent (was split across
AUTH_SECRET_KEY / JWT_SECRET)
- SERVICE_JWT — agent→backend service token
- No JWT_SECRET fallback: decoder fails with migration guidance if only
JWT_SECRET is set
Compose files unified:
- docker-compose.yml, docker-compose.enterprise-clean.yml,
docker-compose.e2e.yml — all use AUTH_SECRET_KEY
- build.sh generated compose now passes AUTH_SECRET_KEY + ENCRYPTION_KEY
to backend
Env examples unified and completed:
- .env.example — comprehensive template, all compose vars
- .env.enterprise-clean.example — production template
- backend/.env.example — backend-only run
- docker/.env.agent.example — agent-only run
- NEW: .env.current.example, .env.master.example,
.env.e2e.example, frontend/.env.example
Tests aligned:
- conftest sets AUTH_SECRET_KEY (canonical value matched across test files)
- test mocks use canonical name
- 1176 passed, 0 failed
This commit is contained in:
@@ -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=
|
||||
|
||||
107
.env.example
107
.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=
|
||||
|
||||
245
.kilo/plans/1783320721445-eager-tiger.md
Normal file
245
.kilo/plans/1783320721445-eager-tiger.md
Normal file
@@ -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=<shared-with-backend-secret>` 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.
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
14
build.sh
14
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 <tag>
|
||||
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 <tag> Lightweight all-in-one image (.tar.xz, <200 MB, no Playwright)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,37 @@
|
||||
# Agent container env vars
|
||||
# LLM конфигурация берётся из FastAPI /api/agent/llm-config (настраивается через web UI).
|
||||
AUTH_SECRET_KEY=<shared-with-backend-secret> # JWT-ключ (общий с backend)
|
||||
FASTAPI_URL=http://backend:8000 # URL основного API
|
||||
SERVICE_JWT=<agent-service-secret> # Сервисный токен для 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=<same-as-backend-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
|
||||
|
||||
9
frontend/.env.example
Normal file
9
frontend/.env.example
Normal file
@@ -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
|
||||
16
run.sh
16
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
|
||||
|
||||
Reference in New Issue
Block a user