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
62 changes: 49 additions & 13 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# BNK-Forge v2 Environment Configuration
# ============================================================================
#
# NOTE: No .env file is needed! All settings are managed via docker-compose.yml
# environment variables and the UI (System > Defaults).
# NOTE: for a plain local dev bring-up you can start WITHOUT a .env — every value
# has a working default baked into docker-compose.yml, and app defaults are managed
# in the UI (System > Defaults).
#
# This file documents optional environment variables you can set in
# docker-compose.yml under the backend service's `environment:` section.
# But a .env IS required for anything hardened: the compose files read
# MCP_SERVICE_PASSWORD and (under ENVIRONMENT=staging|production) JWT_SECRET_KEY,
# ENCRYPTION_KEY and ALLOWED_ORIGINS from it — see the MCP SERVICE ACCOUNT and
# ENVIRONMENT sections below. This file documents those variables plus the optional
# ones you can also set directly in docker-compose.yml under the backend service's
# `environment:` section.
#
# ============================================================================

Expand Down Expand Up @@ -34,7 +39,24 @@
#
# For production, generate and set explicitly in docker-compose.yml:
# JWT_SECRET_KEY=$(openssl rand -hex 32)
# ENCRYPTION_KEY=$(openssl rand -hex 16)
# ENCRYPTION_KEY=$(python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())")
#
# JWT_SECRET_KEY is consumed directly (it signs/verifies JWTs).
#
# ENCRYPTION_KEY is the at-rest Fernet key that ACTUALLY encrypts stored secrets.
# When you set it, the app validates it is a real Fernet key (it refuses to boot if
# not), writes it to the key file on the keys volume ($KEYS_DIR/encryption.key,
# default /app/keys/encryption.key), and encrypts with THAT value — one key, one
# generator. It must therefore be a Fernet key (line above), NOT `openssl rand`/
# `token_hex`. When ENCRYPTION_KEY is unset, the app generates and persists a Fernet
# key to that file on first boot (fine for dev; under ENVIRONMENT=staging|production
# an auto-generated key is refused, so either set ENCRYPTION_KEY or pre-provision the
# key file with a `<file>.operator` marker beside it).
#
# WARNING: if a keys volume already holds a DIFFERENT at-rest key, setting a new
# ENCRYPTION_KEY does not re-encrypt existing data (and an operator-provisioned key
# is never overwritten — the app fails closed on the mismatch). Set it on first
# boot, or migrate the data deliberately.
#
# JWT_SECRET_KEY=your_generated_key_here
# ENCRYPTION_KEY=your_generated_key_here
Expand All @@ -55,10 +77,10 @@
# GUI UPGRADE (Required for System > Upgrade Now)
# ============================================================================
#
# Set HOST_REPO_PATH in docker-compose.yml to enable the GUI upgrade button.
# Without this, use SSH + ./upgrade.sh for server upgrades.
# Set HOST_REPO_PATH in this .env file to enable the GUI upgrade button (compose
# reads it from here). Without this, use SSH + ./upgrade.sh for server upgrades.
#
# HOST_REPO_PATH=/home/jarrodl/bnk-forge-v2
# HOST_REPO_PATH=/path/to/bnk-forge

# ============================================================================
# ENVIRONMENT (development/staging/production)
Expand All @@ -70,7 +92,7 @@
# ENVIRONMENT=development

# ============================================================================
# MCP SERVICE ACCOUNT (REQUIRED in production; dev defaults shown)
# MCP SERVICE ACCOUNT (REQUIRED in production; no defaults shipped)
# ============================================================================
#
# MCP authenticates to the backend as a dedicated non-human service account.
Expand All @@ -81,12 +103,26 @@
# affect MCP (they are distinct env vars and distinct accounts).
#
# DEFAULT_ADMIN_PASSWORD controls the seeded human admin account (first-boot only,
# must_change_password=True). In production set this to a strong initial value
# that operators change on first login.
# must_change_password=True, enforced server-side). If left UNSET, a strong
# random password is generated and written to /app/keys/initial_admin_password
# (mode 600, on the bnk-forge-keys volume) -- retrieve it with:
# docker exec bnk-forge-backend cat /app/keys/initial_admin_password
# Set it here only if you want to choose the initial value yourself.
#
# MCP_SERVICE_USERNAME=mcp
# MCP_SERVICE_PASSWORD=mcp-service-changeme
# DEFAULT_ADMIN_PASSWORD=changeme
# Choose your own value — there is NO shipped default (#186/#187): the old
# mcp-service-changeme is refused as a seed value and can no longer authenticate.
# Set the SAME value on the backend and the MCP server. Keep the value on its own
# line: a '#' that directly abuts the value with NO leading space (e.g.
# `MCP_SERVICE_PASSWORD=s3cret#x`) becomes part of the password, whereas a
# space-separated `s3cret # note` is stripped as an inline comment — so avoid
# trailing text either way.
# Leave it unset and MCP stays unavailable until you configure it (the backend
# disables any stale service account rather than seed a guessable one).
# MCP_SERVICE_PASSWORD=
# Set it only to CHOOSE the initial admin password; REPLACE the placeholder and never
# leave it empty (an empty value seeds an admin the login form rejects -> lockout):
# DEFAULT_ADMIN_PASSWORD=replace-with-a-strong-password

# ============================================================================
# BENCHMARK AGENT AUTHENTICATION
Expand Down
74 changes: 72 additions & 2 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,88 @@
# Also validates alembic migration chain (catches commits made without pre-commit hook)
# Install: git config core.hooksPath .githooks (or: make setup-hooks)
# Skip: git push --no-verify (emergency only!)
#
# DEPENDENCY (bonnyr-f5 #193 minor): `make pre-push` below now runs `ci-gates`,
# whose `secret-scan` gate executes gitleaks via `docker run`. A RUNNING Docker
# daemon is therefore required to push. It fails loudly if Docker is down; start
# Docker (or `git push --no-verify` for a genuine emergency) if you hit that.
set -e

# ─── Migration chain check ────────────────────────────────────────────────────
# Test the command DIRECTLY in the `if`, not `$?` afterwards: under `set -e` a
# non-zero `python3 ...` aborts the hook BEFORE the `if [ $? -ne 0 ]` runs, so the
# "PUSH BLOCKED" message was unreachable — git still blocked the push, but with no
# migration-specific guidance (bonnyr-f5 #193 minor). `if !` is exempt from `set -e`
# and keeps the tailored message.
echo "=== Migration chain check (pre-push) ==="
python3 scripts/check-migrations.py
if [ $? -ne 0 ]; then
if ! python3 scripts/check-migrations.py; then
echo ""
echo "PUSH BLOCKED: Fix migration chain errors above before pushing."
echo "Run 'python3 scripts/check-migrations.py' for details."
exit 1
fi

echo ""

# ─── Commit-message marker lint (bonnyr-f5 #182 r3) ───────────────────────────
# Fail fast, before the heavy suite, if any commit about to be pushed carries a
# CI-control marker (which would suppress the workflow run) or a spurious major-
# bump prose line. Same script the ci.yml commit-lint gate runs, so local == CI.
#
# RANGE from the pre-push stdin protocol (bonnyr-f5 #182 r5, Minor). git feeds
# this hook one "<local ref> <local sha> <remote ref> <remote sha>" line per ref
# being pushed. Scanning the script's default `@{upstream}..HEAD` misses every
# non-tip commit when the branch has no upstream yet (a FIRST push) -- exactly
# when a bad commit is most likely to slip in. Deriving `<remote sha>..<local
# sha>` from stdin scans precisely the commits this push introduces. A new remote
# branch reports an all-zero remote sha (no merge-base to diff against); there we
# fall back to the script's own default rather than scanning all of history.
# Deletions (all-zero local sha) contribute no commits. When stdin is empty (the
# hook run by hand, not by git) we leave RANGE unset so the script default runs.
zero="0000000000000000000000000000000000000000"
remote_name="${1:-origin}" # git passes the remote name as $1
prepush_range=""
while read -r _localref localsha _remoteref remotesha; do
[ -z "${localsha:-}" ] && continue
[ "$localsha" = "$zero" ] && continue # branch deletion: nothing to lint
if [ "${remotesha:-$zero}" = "$zero" ]; then
prepush_range="__DEFAULT__" # new branch: no base -> script default
break
fi
# Normal update: the range BASE is the remote tip. If that object is not present
# locally (never fetched) the range is unresolvable, and passing it straight to
# commit-lint hard-failed with a MISLEADING "not a resolvable revision range"
# (bonnyr-f5 #193 r4 minor). Try a best-effort fetch of just that remote, then
# re-check; if it still is not local, say so PLAINLY and fall back to the
# script's own default range instead of a broken one.
if ! git rev-parse --verify --quiet "${remotesha}^{commit}" >/dev/null 2>&1; then
git fetch --quiet "$remote_name" >/dev/null 2>&1 || true
fi
if git rev-parse --verify --quiet "${remotesha}^{commit}" >/dev/null 2>&1; then
prepush_range="${remotesha}..${localsha}" # exactly the pushed commits
else
echo "note: the remote tip ${remotesha} is not present locally even after a fetch"
echo " of '${remote_name}', so the exact pushed range can't be resolved."
echo " Linting the local default range (@{upstream}..HEAD, else the tip) instead;"
echo " run 'git fetch ${remote_name}' to lint the precise range next time."
prepush_range="__DEFAULT__"
fi
break
done

echo "=== Commit message marker lint (pre-push) ==="
if [ -n "$prepush_range" ] && [ "$prepush_range" != "__DEFAULT__" ]; then
lint_status() { RANGE="$prepush_range" bash scripts/lint-commit-markers.sh; }
else
lint_status() { bash scripts/lint-commit-markers.sh; }
fi
if ! lint_status; then
echo ""
echo "PUSH BLOCKED: a commit message carries a CI-control / spurious-major marker."
echo "Reword it (see AGENTS.md 'Commit conventions') and try again."
exit 1
fi

echo ""
echo "========================================="
echo " Pre-push: Running local checks"
Expand Down
Loading
Loading