a55951f315
- Replace Python-based sanitizer in deploy script with lightweight jq/alpine - Add sync-agents-sanitized.mjs for on-demand and watch-mode sync - Add AgentConfigPath to appsettings.json (explicit default) - Extend /health/live endpoint to report agent count from sanitized config - Document architecture in docs/agent-identity-architecture.md - No openclaw.json secrets ever reach Nexus API containers Verification: - curl /api/v1/agents → 9 agents, zero secrets in response - agents-sanitized.json contains only 'agents' key, no gateway/auth - All C# code paths read from agents-sanitized.json (AgentConfigPath) - Bridge controller resolves agent IDs via AgentService.GetAllowedAgentIdsAsync()
223 lines
6.3 KiB
Bash
Executable File
223 lines
6.3 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
DEPLOY_PATH="${DEPLOY_PATH:-/home/projekte_bao/nexus}"
|
|
ENV_TMPFILE_TEMPLATE="${ENV_TMPFILE:-/tmp/nexus-deploy-env}"
|
|
ENV_TMPFILE=""
|
|
BASE_URL="${BASE_URL:-https://nexus.noveria.net}"
|
|
|
|
cleanup() {
|
|
if [ -n "$ENV_TMPFILE" ] && [ -f "$ENV_TMPFILE" ]; then
|
|
shred -u "$ENV_TMPFILE" 2>/dev/null || rm -f "$ENV_TMPFILE"
|
|
fi
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
require_env() {
|
|
name="$1"
|
|
eval "value=\${$name:-}"
|
|
if [ -z "$value" ]; then
|
|
echo "Missing required environment variable: $name" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
require_env ENV_POSTGRES_PASSWORD
|
|
require_env ENV_JWT_KEY
|
|
|
|
secure_tmpfile() {
|
|
template="$1"
|
|
dir="$(dirname "$template")"
|
|
base="$(basename "$template")"
|
|
mkdir -p "$dir"
|
|
mktemp "$dir/$base.XXXXXX"
|
|
}
|
|
|
|
ENV_TMPFILE="$(secure_tmpfile "$ENV_TMPFILE_TEMPLATE")"
|
|
chmod 600 "$ENV_TMPFILE"
|
|
|
|
if [ ! -f VERSION ]; then
|
|
echo "VERSION file not found" >&2
|
|
exit 1
|
|
fi
|
|
|
|
VERSION="$(tr -d '[:space:]' < VERSION)"
|
|
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
echo "Invalid VERSION value: $VERSION" >&2
|
|
exit 1
|
|
fi
|
|
|
|
GIT_SHA="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
|
GIT_REF="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
|
echo "Deploying Nexus v$VERSION from $GIT_REF"
|
|
|
|
umask 077
|
|
cat > "$ENV_TMPFILE" <<EOF_ENV
|
|
POSTGRES_DB=nexus
|
|
POSTGRES_USER=nexus
|
|
POSTGRES_PASSWORD=${ENV_POSTGRES_PASSWORD}
|
|
JWT_KEY=${ENV_JWT_KEY}
|
|
JWT_ISSUER=nexus
|
|
JWT_AUDIENCE=nexus-web
|
|
BOOTSTRAP_OWNER_EMAIL=vmbao62@hotmail.de
|
|
OPENCLAW_BASE_URL=http://host.docker.internal:18789
|
|
OPENCLAW_GATEWAY_TOKEN=${ENV_OPENCLAW_TOKEN:-}
|
|
OPENCLAW_GATEWAY_PASSWORD=
|
|
NEXUS_VERSION=${VERSION}
|
|
NEXUS_GIT_SHA=${GIT_SHA}
|
|
EOF_ENV
|
|
|
|
echo "Syncing source to deploy path: $DEPLOY_PATH"
|
|
git archive --format=tar HEAD | docker run --rm -i \
|
|
-v "$DEPLOY_PATH:/dest" \
|
|
alpine:3.20 \
|
|
sh -c '
|
|
set -eu
|
|
dest_owner="$(stat -c "%u:%g" /dest)"
|
|
mkdir -p /src-snapshot
|
|
tar -xf - -C /src-snapshot
|
|
|
|
is_protected_path() {
|
|
case "$1" in
|
|
./.git|./.git/*|./.env|./.env.*|./data|./data/*|./logs|./logs/*|./backups|./backups/*|./tmp|./tmp/*|./uploads|./uploads/*|./storage|./storage/*)
|
|
return 0
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
cd /dest
|
|
find . -mindepth 1 -maxdepth 1 | while IFS= read -r path; do
|
|
if ! is_protected_path "$path"; then
|
|
rm -rf "$path"
|
|
fi
|
|
done
|
|
|
|
cd /src-snapshot
|
|
find . -mindepth 1 -maxdepth 1 | while IFS= read -r path; do
|
|
if ! is_protected_path "$path"; then
|
|
cp -a "$path" /dest/
|
|
fi
|
|
done
|
|
|
|
chown -R "$dest_owner" /dest
|
|
'
|
|
|
|
# ── Sanitized agents config for Nexus (no secrets) ──
|
|
echo "Generating sanitized agents config for Nexus (no secrets from openclaw.json)"
|
|
AGENTS_SANITIZED_PATH="/home/projekte_bao/openclaw/data/openclaw/agents-sanitized.json"
|
|
OPENCLAW_CONFIG="/home/projekte_bao/openclaw/data/openclaw/openclaw.json"
|
|
OPENCLAW_CONFIG_DIR="/home/projekte_bao/openclaw/data/openclaw"
|
|
|
|
# Extract only "agents" key from openclaw.json using jq in an alpine container.
|
|
# This ensures NO secrets (gateway, channels, auth, etc.) leak into the sanitized file.
|
|
if docker run --rm \
|
|
-v "$OPENCLAW_CONFIG:/input/openclaw.json:ro" \
|
|
-v "$OPENCLAW_CONFIG_DIR:/output" \
|
|
alpine:3.20 \
|
|
sh -c '
|
|
if ! apk add --no-cache jq >/dev/null 2>&1; then
|
|
echo "WARNING: jq not available, agents-sanitized.json NOT regenerated" >&2
|
|
exit 1
|
|
fi
|
|
if [ ! -f /input/openclaw.json ]; then
|
|
echo "WARNING: openclaw.json not found — agents-sanitized.json NOT regenerated" >&2
|
|
exit 1
|
|
fi
|
|
jq "{agents: .agents}" /input/openclaw.json > /output/agents-sanitized.json
|
|
count=$(jq ".agents.list | length" /output/agents-sanitized.json 2>/dev/null || echo 0)
|
|
echo "Sanitized agents config written ($count agents)"
|
|
' 2>&1; then
|
|
echo "Sanitized agents config written to $AGENTS_SANITIZED_PATH"
|
|
else
|
|
echo "WARNING: Failed to generate agents-sanitized.json — Nexus will use fallback agent IDs" >&2
|
|
fi
|
|
|
|
echo "Building and starting Docker compose stack"
|
|
docker run --rm \
|
|
-v "$DEPLOY_PATH:/workspace/nexus" \
|
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
-w /workspace/nexus \
|
|
-i \
|
|
docker:cli \
|
|
sh -c 'set -eu
|
|
umask 077
|
|
cat > /tmp/nexus-deploy-env
|
|
trap '\''rm -f /tmp/nexus-deploy-env'\'' EXIT INT TERM
|
|
docker compose --env-file /tmp/nexus-deploy-env build
|
|
docker compose --env-file /tmp/nexus-deploy-env up -d --force-recreate --remove-orphans --wait
|
|
docker compose --env-file /tmp/nexus-deploy-env ps
|
|
' < "$ENV_TMPFILE"
|
|
|
|
echo "Verifying image provenance"
|
|
for container in nexus-api-1 nexus-web-1; do
|
|
revision="$(docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$container")"
|
|
version="$(docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' "$container")"
|
|
if [ "$revision" != "$GIT_SHA" ]; then
|
|
echo "Image revision mismatch for $container: expected $GIT_SHA, got $revision" >&2
|
|
exit 1
|
|
fi
|
|
if [ "$version" != "$VERSION" ]; then
|
|
echo "Image version mismatch for $container: expected $VERSION, got $version" >&2
|
|
exit 1
|
|
fi
|
|
echo "$container provenance verified: v$version $revision"
|
|
done
|
|
|
|
echo "Checking live health"
|
|
retry=0
|
|
while [ "$retry" -lt 6 ]; do
|
|
retry=$((retry + 1))
|
|
if curl -fsS --max-time 10 "$BASE_URL/health" >/dev/null; then
|
|
echo "Health check passed"
|
|
break
|
|
fi
|
|
if [ "$retry" -eq 6 ]; then
|
|
echo "Health check failed" >&2
|
|
exit 1
|
|
fi
|
|
sleep "$retry"
|
|
done
|
|
|
|
pass=0
|
|
fail=0
|
|
check() {
|
|
path="$1"
|
|
expected="$2"
|
|
label="$3"
|
|
code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 "$BASE_URL$path")"
|
|
printf '%-28s HTTP %s\n' "$label" "$code"
|
|
if [ "$code" = "$expected" ]; then
|
|
pass=$((pass + 1))
|
|
else
|
|
fail=$((fail + 1))
|
|
fi
|
|
}
|
|
|
|
check_post() {
|
|
path="$1"
|
|
expected="$2"
|
|
label="$3"
|
|
code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 -X POST -H 'Content-Type: application/json' --data '{}' "$BASE_URL$path")"
|
|
printf '%-28s HTTP %s\n' "$label" "$code"
|
|
if [ "$code" = "$expected" ]; then
|
|
pass=$((pass + 1))
|
|
else
|
|
fail=$((fail + 1))
|
|
fi
|
|
}
|
|
|
|
check "/dashboard" "200" "Dashboard"
|
|
check "/health" "200" "Health"
|
|
check "/api/v1/operations/snapshot" "401" "Operations auth"
|
|
check_post "/api/v1/chat" "401" "Chat auth"
|
|
|
|
if [ "$fail" -ne 0 ]; then
|
|
echo "Smoke test failed: $fail failed, $pass passed" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Nexus v$VERSION deployed and verified"
|