#!/usr/bin/env bash # AI VM (10.20.0.12), experiment design 6. Idempotent: first boot (startup script) and `make vm-sync` / `make ai-sync`. # Secrets come from Secret Manager through the VM service account (Terraform design 6.11): # selfheal-openai-api-key, selfheal-anthropic-api-key, selfheal-discord-bot-token (registered by the operator), selfheal-k8s-ai-token, selfheal-k8s-ca-cert (make token) # The two operator secrets may not exist yet: the services then start degraded (agent /diagnose 503, hitl without # gateway) and this script still succeeds, so the VM can be verified before the keys arrive. # Non-secret settings (Discord IDs, model) are in ai.env (committed); AWS role ARN comes from instance metadata. set -euo pipefail cd "$(dirname "$0")" MD=http://metadata.google.internal/computeMetadata/v1 md() { curl -sf -H 'Metadata-Flavor: Google' "$MD/$1"; } PROJECT="$(md project/project-id)" AWS_ROLE_ARN="$(md instance/attributes/aws-role-arn || true)" command -v jq >/dev/null || { apt-get update -qq && apt-get install -y -qq jq >/dev/null; } TOKEN="$(md instance/service-accounts/default/token | jq -r .access_token)" # prints the secret value, or nothing when the secret has no version yet (HTTP 404 / FAILED_PRECONDITION) secret() { curl -s -H "Authorization: Bearer ${TOKEN}" "https://secretmanager.googleapis.com/v1/projects/${PROJECT}/secrets/$1/versions/latest:access" | jq -r '.payload.data // empty' | base64 -d; } SEC=/opt/selfheal/ai/secrets mkdir -p "$SEC"; chmod 700 /opt/selfheal/ai "$SEC" umask 077 secret selfheal-openai-api-key > "$SEC/openai-api-key" secret selfheal-anthropic-api-key > "$SEC/anthropic-api-key" # Claude Messages API (claude-* model ids); optional until registered secret selfheal-typesafe-api-key > "$SEC/typesafe-api-key" secret selfheal-discord-bot-token > "$SEC/discord-bot-token" secret selfheal-k8s-ai-token > "$SEC/k8s-token" secret selfheal-k8s-ca-cert > "$SEC/k8s-ca.crt" unset TOKEN for f in openai-api-key discord-bot-token typesafe-api-key anthropic-api-key; do [ -s "$SEC/$f" ] || echo "note: secret $f is empty (not registered yet); service runs degraded" >&2; done for f in k8s-token k8s-ca.crt; do [ -s "$SEC/$f" ] || echo "note: $f is empty (run make token on the operator host)" >&2; done chown -R 10002:10002 "$SEC"; chmod 600 "$SEC"/* # agent/hitl run as uid 10002 ENV_FILE=/opt/selfheal/ai.env if [ ! -f "$ENV_FILE" ] || ! grep -q '^RAG_DB_PASSWORD=' "$ENV_FILE"; then RDB_PW="$(head -c 24 /dev/urandom | base64 | tr -d '/+=')" else RDB_PW="$(sed -n 's/^RAG_DB_PASSWORD=//p' "$ENV_FILE")" fi AI_TAG="$(cat image.tag)" { cat ai.env printf 'AWS_ROLE_ARN=%s\n' "$AWS_ROLE_ARN" printf 'AWS_REGION=ap-northeast-1\n' printf 'GCP_PROJECT=%s\n' "$PROJECT" printf 'AI_TAG=%s\n' "$AI_TAG" printf 'RAG_DB_PASSWORD=%s\n' "$RDB_PW" } > "$ENV_FILE" unset RDB_PW docker compose --env-file "$ENV_FILE" -f compose.yaml up -d --pull always --remove-orphans # both services read their secret files at start (hitl needs the token to open the gateway): recreate so new secrets apply docker compose --env-file "$ENV_FILE" -f compose.yaml up -d --force-recreate --no-deps agent hitl for i in $(seq 1 30); do a=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/healthz || true); h=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8081/healthz || true) if [ "$a$h" = "200200" ]; then break; fi; sleep 5 done echo "ready: agent=$a hitl=$h" docker compose --env-file "$ENV_FILE" -f compose.yaml ps --format 'table {{.Name}}\t{{.Status}}'