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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ MOBILE_HOST=127.0.0.1
MOBILE_PORT=8889
REDIS_BIND=127.0.0.1:6379

# Core production credentials. Set these before using the production override.
# Core production credentials. Run `python scripts/bootstrap_env.py` or use the
# one-command installer; both generate missing values without printing them.
POSTGRES_PASSWORD=
REDIS_PASSWORD=
CELERY_REDIS_PASSWORD=
Expand Down Expand Up @@ -183,7 +184,9 @@ ADMIN_EMAIL=
# =========================
# Core app
# =========================
DATABASE_URL=postgresql://quantdinger:quantdinger123@postgres:5432/quantdinger
# Docker Compose builds DATABASE_URL from POSTGRES_USER, POSTGRES_PASSWORD, and
# POSTGRES_DB. Set this only when running the backend outside Docker.
DATABASE_URL=
QD_PROCESS_ROLE=legacy
STRATEGY_COMMANDS_ENABLED=false
STRATEGY_COMMAND_STOP_WAIT_SEC=5
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/basic-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ jobs:
compose-check:
name: Docker Compose validation
runs-on: ubuntu-latest
env:
# Compose requires a non-empty database password after security hardening.
# This deterministic CI-only value is used for static configuration validation.
POSTGRES_PASSWORD: compose-validation-${{ github.run_id }}
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Expand Down
6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,16 @@ The web UI source lives in the separate private **QuantDinger-Vue** repo, which
### Backend (Python)

```bash
cp .env.example .env # Windows: copy .env.example .env
python scripts/bootstrap_env.py
cd backend_api_python
pip install -r requirements.txt
python run.py
```

`bootstrap_env.py` creates the project-root `.env`, generates missing local
credentials, and preserves existing values. Set `DATABASE_URL` explicitly when
the backend runs directly on the host instead of through Docker Compose.

### Frontend

The SPA lives in the private **QuantDinger-Vue** repo. Tagging a release there (`git tag vX.Y.Z && git push --tags`) triggers `.github/workflows/release-frontend.yml`, which builds a multi-arch image and pushes it to `ghcr.io/openbyteinc/quantdinger-frontend`. No frontend artefacts are committed here — pin the consumed tag via `IMAGE_TAG` (or `FRONTEND_TAG` for a per-side override) in a root-level `.env`.
Expand Down
11 changes: 5 additions & 6 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@
git clone https://github.com/<your-org>/quantdinger.git
cd quantdinger

# 2. Configure
cp .env.example .env
# Edit .env — at minimum set SECRET_KEY to a random value:
# SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
# 2. Configure and generate local credentials
python scripts/bootstrap_env.py
# Edit .env to set administrator credentials and optional integrations.
# The same project-root `.env` also contains image and port settings.

# 3. Launch
Expand Down Expand Up @@ -62,11 +61,11 @@ quantdinger/
## Running Backend Locally (without Docker)

```bash
cp .env.example .env
python scripts/bootstrap_env.py
cd backend_api_python
python -m venv .venv && source .venv/bin/activate # or .venv\Scripts\activate on Windows
pip install -r requirements.txt
# Edit ../.env before starting the backend.
# Edit ../.env and set DATABASE_URL for the host PostgreSQL endpoint.
python run.py
```

Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,14 +313,17 @@ legacy-image recovery and rootless/NFS notes.
```bash
git clone https://github.com/OpenByteInc/QuantDinger.git
cd QuantDinger
cp .env.example .env
python scripts/bootstrap_env.py
```

Before the first start, replace the example values in the unified environment file:
Before the first start, review the unified environment file. The bootstrap command
already generates the PostgreSQL, session-signing, and credential-encryption
secrets; set the administrator credentials and review any production-only
integrations:

| File | Required production values |
| --- | --- |
| `.env` | `SECRET_KEY`, `CREDENTIAL_ENCRYPTION_KEY`, administrator, PostgreSQL, Redis, and Grafana credentials |
| `.env` | Administrator credentials; optional Redis and Grafana credentials; deployment-specific URLs and integrations |

After an update, `docker compose up` runs the one-shot `env-sync` service before
database and Kafka initialization. It appends newly introduced settings, imports
Expand All @@ -336,7 +339,9 @@ For a manual one-time migration from older releases, add
`--legacy backend_api_python/.env` (source deployment) or `--legacy backend.env`
(old GHCR deployment). The old file is not deleted.

Generate independent secrets with:
The bootstrap command creates `.env`, preserves existing values during upgrades,
and generates independent PostgreSQL, session, and credential-encryption secrets.
To generate another independent secret manually:

```bash
python -c "import secrets; print(secrets.token_hex(32))"
Expand Down
14 changes: 8 additions & 6 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,14 +283,15 @@ docker compose exec -u 10001:10001 -T backend \
```bash
git clone https://github.com/OpenByteInc/QuantDinger.git
cd QuantDinger
cp .env.example .env
python scripts/bootstrap_env.py
```

首次启动前,请替换统一环境文件中的示例值:
首次启动前,请检查统一环境文件。初始化命令已经自动生成 PostgreSQL、会话签名和凭据加密密钥;
你只需设置管理员凭据,并检查生产环境专用的集成配置:

| 文件 | 生产环境必填项 |
| --- | --- |
| `.env` | `SECRET_KEY`、`CREDENTIAL_ENCRYPTION_KEY`、管理员、PostgreSQL、Redis 和 Grafana 凭据 |
| `.env` | 管理员凭据;可选的 Redis、Grafana 凭据;部署域名及外部集成 |

更新代码后,`docker compose up` 会先执行一次性的 `env-sync` 服务,再启动数据库迁移与
Kafka 初始化。它会补充新字段,从旧后端环境文件导入根 `.env` 中缺少的值,保留已有值和
Expand All @@ -303,7 +304,8 @@ python scripts/sync_env.py --env-file .env --template .env.example --backup
手动执行旧版本首次迁移时,再增加 `--legacy backend_api_python/.env`(源码部署)或
`--legacy backend.env`(旧 GHCR 部署)。旧文件不会被删除。

使用以下命令生成相互独立的密钥:
初始化命令会创建 `.env`,在升级时保留已有配置,并分别生成 PostgreSQL、会话签名和凭据加密密钥。
如需手动生成额外的独立密钥,可使用:

```bash
python -c "import secrets; print(secrets.token_hex(32))"
Expand All @@ -319,8 +321,8 @@ docker compose ps
基础服务栈不会启动 Prometheus、Grafana 或 Alertmanager,从而保持默认开源安装的轻量化。

有关详细安装路径、Windows 注意事项、中国镜像设置和 PostgreSQL 迁移指导,请参阅
[安装故障排查](docs/deployment/INSTALL_TROUBLESHOOTING.md)和
[云部署指南](docs/deployment/CLOUD_DEPLOYMENT_EN.md)。
[安装故障排查](docs/deployment/INSTALL_TROUBLESHOOTING_CN.md)和
[云部署指南](docs/deployment/CLOUD_DEPLOYMENT_CN.md)。

## 生产环境部署

Expand Down
14 changes: 9 additions & 5 deletions backend_api_python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,23 @@ Read [Backend architecture](../docs/architecture/ARCHITECTURE.md) and
Create the unified runtime environment file from the repository root:

```bash
cp .env.example .env
python scripts/bootstrap_env.py
```

At minimum, replace these values before a shared or production deployment:
The bootstrap command creates `.env`, generates missing PostgreSQL,
session-signing, and credential-encryption secrets, and preserves existing
values during upgrades. Before a shared or production deployment, set the
administrator credentials and review optional service credentials:

```dotenv
SECRET_KEY=<independent-random-value-at-least-10-bytes-32-plus-recommended>
CREDENTIAL_ENCRYPTION_KEY=<independent-random-value-at-least-32-bytes>
ADMIN_USER=<initial-admin-name>
ADMIN_PASSWORD=<strong-initial-password>
REDIS_PASSWORD=<optional-independent-random-value>
CELERY_REDIS_PASSWORD=<optional-independent-random-value>
```

Generate each secret independently:
When running the backend directly on the host, also set `DATABASE_URL` to the
host PostgreSQL endpoint. To generate an additional independent secret manually:

```bash
python -c "import secrets; print(secrets.token_hex(32))"
Expand Down
49 changes: 38 additions & 11 deletions backend_api_python/app/services/ai_skill_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import json
import re
import secrets
import string
from dataclasses import dataclass, field
from datetime import datetime, timezone
Expand Down Expand Up @@ -691,8 +692,26 @@ def _utc_now_iso() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")


def _skill_path(skill_id: str) -> Path:
return USER_SKILLS_DIR / f"{skill_id}.json"
def _stored_skill_path(skill_id: str) -> Path | None:
if not _SKILL_ID_RE.fullmatch(skill_id or ""):
raise ValueError("invalid skill id")

root = USER_SKILLS_DIR.resolve()
for path in root.glob("*.json"):
try:
candidate = path.resolve()
if not candidate.is_relative_to(root) or not candidate.is_file():
continue
payload = json.loads(candidate.read_text(encoding="utf-8"))
if str(payload.get("id") or "").strip() == skill_id:
return candidate
except (OSError, ValueError, json.JSONDecodeError):
continue
return None


def _new_skill_path() -> Path:
return USER_SKILLS_DIR.resolve() / f"{secrets.token_hex(16)}.json"


def _contains_forbidden_field(value: Any, forbidden: set[str]) -> bool:
Expand Down Expand Up @@ -725,7 +744,7 @@ def _validate_skill_payload(payload: dict[str, Any], *, updating: bool = False)
if not isinstance(payload, dict):
return False, "payload must be an object"
skill_id = str(payload.get("id") or "").strip()
if not _SKILL_ID_RE.match(skill_id):
if not _SKILL_ID_RE.fullmatch(skill_id):
return False, "skill id must match ^[a-z][a-z0-9_-]{2,63}$"
if not updating and skill_id in _builtin_ids():
return False, "builtin skills cannot be overridden"
Expand Down Expand Up @@ -853,7 +872,7 @@ def install_prompt_skill(payload: dict[str, Any], install_source: str = "manual"
raise ValueError(msg)
_ensure_user_dir()
skill_id = str(payload["id"]).strip()
if _skill_path(skill_id).exists():
if _stored_skill_path(skill_id) is not None:
raise ValueError("skill already installed")
now = _utc_now_iso()
stored = dict(payload)
Expand All @@ -862,17 +881,25 @@ def install_prompt_skill(payload: dict[str, Any], install_source: str = "manual"
stored["install_source"] = install_source
stored["created_at"] = now
stored["updated_at"] = now
_skill_path(skill_id).write_text(json.dumps(stored, ensure_ascii=False, indent=2), encoding="utf-8")
for _ in range(10):
try:
with _new_skill_path().open("x", encoding="utf-8") as stream:
json.dump(stored, stream, ensure_ascii=False, indent=2)
break
except FileExistsError:
continue
else:
raise OSError("unable to allocate skill storage")
return stored


def set_skill_enabled(skill_id: str, enabled: bool) -> dict[str, Any]:
if skill_id in _builtin_ids():
raise ValueError("builtin skills cannot be disabled")
if not _SKILL_ID_RE.match(skill_id or ""):
if not _SKILL_ID_RE.fullmatch(skill_id or ""):
raise ValueError("invalid skill id")
path = _skill_path(skill_id)
if not path.exists():
path = _stored_skill_path(skill_id)
if path is None:
raise FileNotFoundError("skill not found")
payload = json.loads(path.read_text(encoding="utf-8"))
payload["enabled"] = bool(enabled)
Expand All @@ -884,10 +911,10 @@ def set_skill_enabled(skill_id: str, enabled: bool) -> dict[str, Any]:
def delete_installed_skill(skill_id: str) -> None:
if skill_id in _builtin_ids():
raise ValueError("builtin skills cannot be deleted")
if not _SKILL_ID_RE.match(skill_id or ""):
if not _SKILL_ID_RE.fullmatch(skill_id or ""):
raise ValueError("invalid skill id")
path = _skill_path(skill_id)
if not path.exists():
path = _stored_skill_path(skill_id)
if path is None:
raise FileNotFoundError("skill not found")
path.unlink()

Expand Down
2 changes: 1 addition & 1 deletion backend_api_python/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ if [ ! -f /app/.env ]; then
echo "[WARNING] Cannot create /app/.env: $(cat /tmp/quantdinger-env-copy.err)"
echo "[WARNING] Continuing with container environment variables only."
echo "[TIP] Create the host env file before starting Docker:"
echo " cp .env.example .env"
echo " python3 scripts/bootstrap_env.py"
rm -f /tmp/quantdinger-env-copy.err
fi
else
Expand Down
4 changes: 3 additions & 1 deletion backend_api_python/env.example
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ ADMIN_EMAIL=
# =========================
# Core app
# =========================
DATABASE_URL=postgresql://quantdinger:quantdinger123@postgres:5432/quantdinger
# Docker Compose builds DATABASE_URL from POSTGRES_USER, POSTGRES_PASSWORD, and
# POSTGRES_DB. Set this only when running the backend outside Docker.
DATABASE_URL=
QD_PROCESS_ROLE=legacy
STRATEGY_COMMANDS_ENABLED=false
STRATEGY_COMMAND_STOP_WAIT_SEC=5
Expand Down
Loading
Loading