Normalize hot-dev auth defaults

This commit is contained in:
rcourtman 2026-04-22 16:28:52 +01:00
parent a60fa03d7f
commit a442eb6bda
11 changed files with 236 additions and 18 deletions

View file

@ -46,6 +46,8 @@ npm run mock:on # Optional: enable mock data
Use `http://127.0.0.1:5173` in the browser for local frontend development. The
frontend dev shell proxies `/api` and `/ws` to the backend on `:7655`; do not
switch your browser to `:7655` unless you are debugging the backend directly.
The managed dev runtime login defaults to `admin` / `adminadminadmin` unless
you override it with `HOT_DEV_AUTH_USER` and `HOT_DEV_AUTH_PASS`.
Backend-only hot reload (requires `air`):

View file

@ -109,6 +109,10 @@ and it proxies `/api` and `/ws` to the backend on `7655`. `7655` is the backend
dependency for API and websocket traffic, not the primary browser URL for local
frontend development.
The managed dev runtime resets its local login to `admin` / `adminadminadmin`
on startup unless you override it with `HOT_DEV_AUTH_USER` and
`HOT_DEV_AUTH_PASS`.
Canonical local dev commands:
- `npm run dev` — start the managed runtime and reclaim the canonical dev ports if an older unmanaged session is still using them

View file

@ -17,7 +17,7 @@ Goal: every eval run starts from the exact same filesystem and runtime state, so
- Proxmox host access (example: `ssh root@<pve-host>`)
- LXC with Pulse + control plane binaries and env configured
- Test credentials set (recommended for deterministic checks): `admin/admin`
- Test credentials set (recommended for deterministic checks): `admin/adminadminadmin`
- Required tools inside LXC: `curl`, `jq`, `ca-certificates`
- Stripe sandbox keys and test recurring prices configured in control-plane env
- Playwright runner host that can reach both Pulse (`:7655`) and control-plane (`:8443`)
@ -87,7 +87,7 @@ Run inside container:
```bash
pct push <ctid> tests/integration/scripts/trial-signup-contract.sh /tmp/trial-signup-contract.sh
pct exec <ctid> -- sh -lc 'chmod +x /tmp/trial-signup-contract.sh && PULSE_E2E_USERNAME=admin PULSE_E2E_PASSWORD=admin /tmp/trial-signup-contract.sh'
pct exec <ctid> -- sh -lc 'chmod +x /tmp/trial-signup-contract.sh && PULSE_E2E_USERNAME=admin PULSE_E2E_PASSWORD=adminadminadmin /tmp/trial-signup-contract.sh'
```
## Clean Eval Loop (Rollback -> Run)
@ -97,7 +97,7 @@ Use this loop for repeatable runs:
```bash
for i in 1 2 3; do
pct rollback <ctid> pre-eval-baseline --start 1
pct exec <ctid> -- sh -lc 'PULSE_E2E_USERNAME=admin PULSE_E2E_PASSWORD=admin /tmp/trial-signup-contract.sh'
pct exec <ctid> -- sh -lc 'PULSE_E2E_USERNAME=admin PULSE_E2E_PASSWORD=adminadminadmin /tmp/trial-signup-contract.sh'
done
```

View file

@ -646,6 +646,15 @@ managed runtime, run the canonical browser recovery proof with the managed dev
credentials and browser entrypoint defaults, and fail with ownership or health
diagnostics instead of leaving operators to remember the exact Playwright
command and env combination by hand.
That same launcher boundary also owns the managed dev auth source of truth.
`scripts/hot-dev.sh` must seed the watched runtime auth `.env` from one
canonical managed-dev credential contract before it reloads runtime overrides,
so stale quick-setup changes under `tmp/dev-config/.env` cannot silently
change the default local login between launches. Repo-root developer docs,
verification wrappers, and integration helper defaults must therefore advertise
the same managed login and treat custom dev credentials as explicit
`HOT_DEV_AUTH_*` or `PULSE_E2E_*` overrides instead of inheriting leftover auth
state from a prior session.
That same takeover path must remain safe on the default macOS Bash runtime and
must not tear down the operator's current shell lineage while reclaiming a
foreground `hot-dev.sh` session. When the canonical ports are already owned by
@ -1040,13 +1049,15 @@ That same governed release-promotion boundary now also owns detached agent and
installer signatures. `scripts/build-release.sh`,
`scripts/release_update_key.go`, `scripts/render_installers.go`,
`scripts/release_ldflags.sh`, `Dockerfile`, `.github/workflows/create-release.yml`,
and `.github/workflows/publish-docker.yml` must derive the embedded update
trust root and installer SSH trust root from the governed release signing key,
`.github/workflows/publish-docker.yml`, `scripts/validate-release.sh`, and
`scripts/validate-published-release.sh` must derive the embedded update trust
root and installer SSH trust root from the governed release signing key,
render release installers with that pinned SSH verifier, emit both `.sig` and
`.sshsig` sidecars for shipped agent binaries and installer assets, and upload
those signatures with the matching release packet so published RC/stable
downloads can keep the updater and installer trust chain fail-closed instead
of downgrading to checksum-only trust.
`.sshsig` sidecars for shipped agent binaries and installer assets, upload
those signatures with the matching release packet, and fail validation if any
published artifact or `checksums.txt` is missing its `.sshsig` sidecar so
published RC/stable downloads can keep the updater and installer trust chain
fail-closed instead of downgrading to checksum-only trust.
The shell-installer boundary now also owns the QNAP boot bootstrap and
teardown contract end to end: `scripts/install.sh` must persist the wrapper on
the writable data volume, write a flash-backed `autorun.sh` block that waits

View file

@ -789,7 +789,7 @@ run_verify_proof_command() {
cd "${integration_dir}"
PULSE_E2E_USE_HOT_DEV=1 \
PULSE_E2E_USERNAME="${PULSE_E2E_USERNAME:-admin}" \
PULSE_E2E_PASSWORD="${PULSE_E2E_PASSWORD:-admin}" \
PULSE_E2E_PASSWORD="${PULSE_E2E_PASSWORD:-adminadminadmin}" \
HOT_DEV_VERIFY_LOCK_FILE="${HOT_DEV_VERIFY_LOCK}" \
node ./scripts/run-playwright.mjs \
tests/16-dev-runtime-recovery.spec.ts \
@ -804,7 +804,7 @@ run_verify_proof_command() {
cd "${ROOT_DIR}"
PULSE_E2E_USE_HOT_DEV=1 \
PULSE_E2E_USERNAME="${PULSE_E2E_USERNAME:-admin}" \
PULSE_E2E_PASSWORD="${PULSE_E2E_PASSWORD:-admin}" \
PULSE_E2E_PASSWORD="${PULSE_E2E_PASSWORD:-adminadminadmin}" \
HOT_DEV_VERIFY_LOCK_FILE="${HOT_DEV_VERIFY_LOCK}" \
bash -lc "${verify_command}"
)

View file

@ -11,6 +11,8 @@
# HOT_DEV_USE_PROD_DATA=true Use /etc/pulse for data (sessions, config, etc.)
# HOT_DEV_USE_PRO=true Build Pro binary (default: true if module available)
# HOT_DEV_ALLOW_OSS_FALLBACK=true Allow fallback to OSS backend when Pro build fails
# HOT_DEV_AUTH_USER=admin Override the managed dev auth username
# HOT_DEV_AUTH_PASS=... Override the managed dev auth password or bcrypt hash
# PULSE_MOCK_MODE=true Render mock UI data while keeping real metrics history intact
# PULSE_DATA_DIR=/path Override data directory
# PULSE_DEV_API_PORT=7655 Backend API port (default: 7655)
@ -36,6 +38,9 @@ DEFAULT_PULSE_REPOS_DIR=$(cd "${ROOT_DIR}/.." && pwd -P)
SCRIPT_PATH="${SCRIPT_DIR}/$(basename "${BASH_SOURCE[0]}")"
SCRIPT_MTIME=$(stat -c %Y "${SCRIPT_PATH}" 2>/dev/null || stat -f %m "${SCRIPT_PATH}")
# shellcheck disable=SC1091
source "${SCRIPT_DIR}/lib/hot-dev-auth.sh"
# --- Helper Functions ---
log_info() { printf "\033[0;34m[hot-dev]\033[0m %s\n" "$1"; }
@ -130,6 +135,17 @@ load_runtime_env_overrides() {
fi
}
sync_runtime_auth_env_overrides() {
local runtime_env="${PULSE_DATA_DIR}/.env"
if [[ "${HOT_DEV_USE_PROD_DATA:-false}" == "true" ]] || [[ "${PULSE_DATA_DIR}" == "/etc/pulse" ]]; then
return
fi
hot_dev_sync_auth_env_file "${runtime_env}" "${PULSE_AUTH_USER}" "${PULSE_AUTH_PASS}"
log_info "Seeded managed dev auth defaults in ${runtime_env}"
}
show_startup_banner() {
cat <<BANNER
=========================================
@ -140,7 +156,7 @@ Frontend: http://${FRONTEND_DEV_HOST}:${FRONTEND_DEV_PORT} (Local)
http://${LAN_IP}:${FRONTEND_DEV_PORT} (LAN)
Backend API: ${PULSE_DEV_API_URL}
Dev Credentials: admin / admin
Dev Credentials: $(hot_dev_auth_banner_line "${PULSE_AUTH_USER:-}" "${PULSE_AUTH_PASS:-}")
Mock Mode: ${PULSE_MOCK_MODE:-false}
Toggle mock mode: npm run mock:on / npm run mock:off
@ -327,9 +343,10 @@ PORT=${PULSE_DEV_API_PORT}
PULSE_DEV=true # Enable development mode features (needed for admin bypass etc)
ALLOW_ADMIN_BYPASS=1 # Allow X-Admin-Bypass header in dev mode
# Dev credentials: admin/admin (bcrypt hash of 'admin')
PULSE_AUTH_USER="admin"
PULSE_AUTH_PASS='$2a$12$j9/pl2RCHGVGvtv4wocrx.FGBczUw97ZAeO8im0.Ty.fXDGFOviWS'
# Managed dev credentials default to admin/adminadminadmin unless
# HOT_DEV_AUTH_USER / HOT_DEV_AUTH_PASS explicitly override them.
PULSE_AUTH_USER="$(hot_dev_resolve_auth_user)"
PULSE_AUTH_PASS="$(hot_dev_resolve_auth_pass)"
export FRONTEND_PORT PULSE_DEV_API_PORT PORT PULSE_DEV ALLOW_ADMIN_BYPASS PULSE_AUTH_USER PULSE_AUTH_PASS
# Data Directory Setup
@ -347,6 +364,7 @@ else
log_info "Using dev config directory: ${PULSE_DATA_DIR}"
fi
sync_runtime_auth_env_overrides
load_runtime_env_overrides
if [[ "${PULSE_DATA_DIR}" == "${ROOT_DIR}/tmp/dev-config" ]] && [[ ${PULSE_MOCK_MODE:-false} != "true" ]]; then
@ -674,6 +692,7 @@ log_info "Starting backend file watcher..."
# Re-source the canonical runtime .env so mock-mode changes take
# effect without a full hot-dev restart.
sync_runtime_auth_env_overrides
load_runtime_env_overrides
# Kill ALL pulse processes (not just one) to prevent duplicates

View file

@ -0,0 +1,65 @@
#!/usr/bin/env bash
HOT_DEV_DEFAULT_AUTH_USER="admin"
HOT_DEV_DEFAULT_AUTH_PASSWORD="adminadminadmin"
HOT_DEV_DEFAULT_AUTH_HASH='$2a$12$J/Vu6FlBUJDTK.VAkysjB.AnvFOcijDbETyumhCB.nJVes5gvpiI6'
hot_dev_resolve_auth_user() {
printf '%s\n' "${HOT_DEV_AUTH_USER:-${HOT_DEV_DEFAULT_AUTH_USER}}"
}
hot_dev_resolve_auth_pass() {
if [[ -n "${HOT_DEV_AUTH_PASS:-}" ]]; then
printf '%s\n' "${HOT_DEV_AUTH_PASS}"
return
fi
printf '%s\n' "${HOT_DEV_DEFAULT_AUTH_HASH}"
}
hot_dev_is_default_auth() {
local auth_user=$1
local auth_pass=$2
[[ "${auth_user}" == "${HOT_DEV_DEFAULT_AUTH_USER}" ]] || return 1
[[ "${auth_pass}" == "${HOT_DEV_DEFAULT_AUTH_HASH}" || "${auth_pass}" == "${HOT_DEV_DEFAULT_AUTH_PASSWORD}" ]]
}
hot_dev_auth_banner_line() {
local auth_user=$1
local auth_pass=$2
if hot_dev_is_default_auth "${auth_user}" "${auth_pass}"; then
printf '%s / %s\n' "${HOT_DEV_DEFAULT_AUTH_USER}" "${HOT_DEV_DEFAULT_AUTH_PASSWORD}"
return
fi
printf 'custom via HOT_DEV_AUTH_USER / HOT_DEV_AUTH_PASS\n'
}
hot_dev_single_quote() {
printf '%s' "$1" | sed "s/'/'\\\\''/g"
}
hot_dev_sync_auth_env_file() {
local runtime_env=$1
local auth_user=$2
local auth_pass=$3
local tmp_file
mkdir -p "$(dirname "${runtime_env}")"
tmp_file="${runtime_env}.tmp.$$"
{
printf '# Managed by hot-dev.sh for deterministic dev auth\n'
printf "PULSE_AUTH_USER='%s'\n" "$(hot_dev_single_quote "${auth_user}")"
printf "PULSE_AUTH_PASS='%s'\n" "$(hot_dev_single_quote "${auth_pass}")"
if [[ -f "${runtime_env}" ]]; then
grep -v -E '^(# Managed by hot-dev\.sh for deterministic dev auth|PULSE_AUTH_USER=|PULSE_AUTH_PASS=)' "${runtime_env}"
fi
} > "${tmp_file}"
mv "${tmp_file}" "${runtime_env}"
chmod 600 "${runtime_env}"
}

View file

@ -0,0 +1,117 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
HOT_DEV_AUTH_LIB="${ROOT_DIR}/scripts/lib/hot-dev-auth.sh"
if [[ ! -f "${HOT_DEV_AUTH_LIB}" ]]; then
echo "hot-dev-auth.sh not found at ${HOT_DEV_AUTH_LIB}" >&2
exit 1
fi
failures=0
temp_dirs=()
cleanup() {
local dir
for dir in "${temp_dirs[@]:-}"; do
rm -rf "${dir}" 2>/dev/null || true
done
}
trap cleanup EXIT
assert_contains() {
local desc="$1"
local haystack="$2"
local needle="$3"
if [[ "${haystack}" == *"${needle}"* ]]; then
echo "[PASS] ${desc}"
else
echo "[FAIL] ${desc}" >&2
echo "Expected to find: ${needle}" >&2
((failures++))
fi
}
make_temp_dir() {
local dir
dir="$(mktemp -d)"
temp_dirs+=("${dir}")
printf "%s\n" "${dir}"
}
test_default_auth_contract() {
local output
output="$(
HOT_DEV_AUTH_LIB="${HOT_DEV_AUTH_LIB}" \
bash -lc '
source "${HOT_DEV_AUTH_LIB}"
printf "user=%s\n" "$(hot_dev_resolve_auth_user)"
printf "pass=%s\n" "$(hot_dev_resolve_auth_pass)"
printf "banner=%s\n" "$(hot_dev_auth_banner_line "$(hot_dev_resolve_auth_user)" "$(hot_dev_resolve_auth_pass)")"
'
)"
assert_contains "default auth username stays admin" "${output}" "user=admin"
assert_contains "default auth password stays adminadminadmin" "${output}" "banner=admin / adminadminadmin"
assert_contains "default auth uses canonical bcrypt hash" "${output}" "pass=${HOT_DEV_DEFAULT_AUTH_HASH}"
}
test_custom_auth_banner_contract() {
local output
output="$(
HOT_DEV_AUTH_LIB="${HOT_DEV_AUTH_LIB}" \
bash -lc '
source "${HOT_DEV_AUTH_LIB}"
HOT_DEV_AUTH_USER="custom-admin"
HOT_DEV_AUTH_PASS="custom-password"
printf "banner=%s\n" "$(hot_dev_auth_banner_line "$(hot_dev_resolve_auth_user)" "$(hot_dev_resolve_auth_pass)")"
'
)"
assert_contains "custom auth banner hides the raw password" "${output}" "banner=custom via HOT_DEV_AUTH_USER / HOT_DEV_AUTH_PASS"
}
test_sync_auth_env_file_preserves_non_auth_settings() {
local state_dir runtime_env output
state_dir="$(make_temp_dir)"
runtime_env="${state_dir}/.env"
cat > "${runtime_env}" <<'EOF'
# Auto-generated by Pulse Quick Security Setup
PULSE_AUTH_USER='stale-user'
PULSE_AUTH_PASS='stale-pass'
PULSE_AUDIT_LOG=true
PULSE_MOCK_MODE=false
EOF
output="$(
HOT_DEV_AUTH_LIB="${HOT_DEV_AUTH_LIB}" \
RUNTIME_ENV_PATH="${runtime_env}" \
bash -lc '
source "${HOT_DEV_AUTH_LIB}"
hot_dev_sync_auth_env_file "${RUNTIME_ENV_PATH}" "admin" "${HOT_DEV_DEFAULT_AUTH_HASH}"
cat "${RUNTIME_ENV_PATH}"
'
)"
assert_contains "sync writes managed auth header" "${output}" "# Managed by hot-dev.sh for deterministic dev auth"
assert_contains "sync rewrites auth user" "${output}" "PULSE_AUTH_USER='admin'"
assert_contains "sync rewrites auth password hash" "${output}" "PULSE_AUTH_PASS='${HOT_DEV_DEFAULT_AUTH_HASH}'"
assert_contains "sync preserves audit settings" "${output}" "PULSE_AUDIT_LOG=true"
assert_contains "sync preserves mock settings" "${output}" "PULSE_MOCK_MODE=false"
}
source "${HOT_DEV_AUTH_LIB}"
test_default_auth_contract
test_custom_auth_banner_contract
test_sync_auth_env_file_preserves_non_auth_settings
if (( failures > 0 )); then
echo "FAIL: ${failures} hot-dev auth assertions failed" >&2
exit 1
fi
echo "PASS: hot-dev auth contract checks passed"

View file

@ -173,7 +173,7 @@ PY"
assert_contains "verify proof forces managed hot-dev mode" "${output}" "mode=1"
assert_contains "verify proof defaults username" "${output}" "username=admin"
assert_contains "verify proof defaults password" "${output}" "password=admin"
assert_contains "verify proof defaults password" "${output}" "password=adminadminadmin"
assert_contains "verify proof passes the managed runtime lock path" "${output}" "lock=${ROOT_DIR}/tmp/hot-dev.verify.lock"
}

View file

@ -16,7 +16,7 @@ LOCAL_PULSE_PORT="${LOCAL_PULSE_PORT:-17655}"
LOCAL_CP_PORT="${LOCAL_CP_PORT:-18443}"
PULSE_E2E_USERNAME="${PULSE_E2E_USERNAME:-admin}"
PULSE_E2E_PASSWORD="${PULSE_E2E_PASSWORD:-admin}"
PULSE_E2E_PASSWORD="${PULSE_E2E_PASSWORD:-adminadminadmin}"
PULSE_E2E_BOOTSTRAP_TOKEN="${PULSE_E2E_BOOTSTRAP_TOKEN:-}"
MULTI_TENANT_SCENARIO="${MULTI_TENANT_SCENARIO:-multi-tenant}"

View file

@ -6,7 +6,7 @@ set -euo pipefail
PULSE_BASE_URL="${PULSE_BASE_URL:-http://127.0.0.1:7655}"
PULSE_E2E_USERNAME="${PULSE_E2E_USERNAME:-admin}"
PULSE_E2E_PASSWORD="${PULSE_E2E_PASSWORD:-admin}"
PULSE_E2E_PASSWORD="${PULSE_E2E_PASSWORD:-adminadminadmin}"
WAIT_TIMEOUT_SECONDS="${WAIT_TIMEOUT_SECONDS:-60}"
MAX_DUPLICATE_TRIAL_ATTEMPTS="${MAX_DUPLICATE_TRIAL_ATTEMPTS:-10}"