feat: one-command deployment script and parameterized startup

- deploy-forever-agent.sh: creates Pub/Sub, static IP, IAM, VM in one command
- teardown-forever-agent.sh: cleans up all resources
- gce-startup.sh: reads config from instance metadata instead of hardcoding
This commit is contained in:
Adam Weidman
2026-03-04 09:36:14 -05:00
parent 6d120edaf4
commit 444e42f6bf
3 changed files with 254 additions and 7 deletions
+193
View File
@@ -0,0 +1,193 @@
#!/usr/bin/env bash
# deploy-forever-agent.sh — One-command deployment of Gemini CLI Forever Agent on GCE.
#
# Creates a GCE VM running Gemini CLI in --forever mode with a Google Chat bridge
# via Cloud Pub/Sub. The agent auto-resumes tasks (Sisyphus mode) and runs in YOLO
# (auto-approve all tools).
#
# Prerequisites:
# - gcloud CLI installed and authenticated
# - A Google Cloud project with billing enabled
# - A Gemini API key (from aistudio.google.com)
# - A Google Chat app configured (see instructions printed at the end)
#
# Usage:
# export GEMINI_API_KEY="your-api-key"
# ./scripts/deploy-forever-agent.sh
#
# Optional env vars (defaults shown):
# PROJECT - GCP project ID (defaults to gcloud config)
# REGION - GCP region (default: us-central1)
# ZONE - GCP zone (default: us-central1-a)
# VM_NAME - VM name (default: forever-agent)
# MACHINE_TYPE - VM machine type (default: e2-medium)
# GIT_BRANCH - Branch to clone (default: afw/forever-gchat)
# PUBSUB_TOPIC - Pub/Sub topic name (default: forever-agent-chat)
# PUBSUB_SUB - Pub/Sub subscription name (default: forever-agent-chat-sub)
set -euo pipefail
# --- Configuration ---
PROJECT="${PROJECT:-$(gcloud config get-value project 2>/dev/null)}"
REGION="${REGION:-us-central1}"
ZONE="${ZONE:-us-central1-a}"
VM_NAME="${VM_NAME:-forever-agent}"
MACHINE_TYPE="${MACHINE_TYPE:-e2-medium}"
GIT_BRANCH="${GIT_BRANCH:-afw/forever-gchat}"
PUBSUB_TOPIC="${PUBSUB_TOPIC:-forever-agent-chat}"
PUBSUB_SUB="${PUBSUB_SUB:-forever-agent-chat-sub}"
if [ -z "${GEMINI_API_KEY:-}" ]; then
echo "ERROR: GEMINI_API_KEY is required. Get one from https://aistudio.google.com"
echo "Usage: GEMINI_API_KEY=... ./scripts/deploy-forever-agent.sh"
exit 1
fi
if [ -z "$PROJECT" ]; then
echo "ERROR: No GCP project set. Run: gcloud config set project YOUR_PROJECT"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STARTUP_SCRIPT="${SCRIPT_DIR}/gce-startup.sh"
if [ ! -f "$STARTUP_SCRIPT" ]; then
echo "ERROR: Startup script not found at $STARTUP_SCRIPT"
exit 1
fi
echo "=== Deploying Gemini CLI Forever Agent ==="
echo " Project: $PROJECT"
echo " Zone: $ZONE"
echo " VM: $VM_NAME ($MACHINE_TYPE)"
echo " Branch: $GIT_BRANCH"
echo " Pub/Sub: $PUBSUB_TOPIC / $PUBSUB_SUB"
echo ""
# --- Enable required APIs ---
echo "--- Enabling APIs..."
gcloud services enable \
compute.googleapis.com \
pubsub.googleapis.com \
chat.googleapis.com \
--project="$PROJECT" --quiet
# --- Create Pub/Sub topic + subscription ---
echo "--- Setting up Pub/Sub..."
if ! gcloud pubsub topics describe "$PUBSUB_TOPIC" --project="$PROJECT" &>/dev/null; then
gcloud pubsub topics create "$PUBSUB_TOPIC" --project="$PROJECT"
echo " Created topic: $PUBSUB_TOPIC"
else
echo " Topic already exists: $PUBSUB_TOPIC"
fi
if ! gcloud pubsub subscriptions describe "$PUBSUB_SUB" --project="$PROJECT" &>/dev/null; then
gcloud pubsub subscriptions create "$PUBSUB_SUB" \
--topic="$PUBSUB_TOPIC" \
--project="$PROJECT" \
--ack-deadline=60
echo " Created subscription: $PUBSUB_SUB"
else
echo " Subscription already exists: $PUBSUB_SUB"
fi
# Grant Google Chat SA permission to publish to the topic
echo "--- Granting Chat API publish permission..."
gcloud pubsub topics add-iam-policy-binding "$PUBSUB_TOPIC" \
--member="serviceAccount:chat-api-push@system.gserviceaccount.com" \
--role="roles/pubsub.publisher" \
--project="$PROJECT" --quiet 2>/dev/null || true
# --- Reserve static IP ---
echo "--- Setting up static IP..."
IP_NAME="${VM_NAME}-ip"
if ! gcloud compute addresses describe "$IP_NAME" --region="$REGION" --project="$PROJECT" &>/dev/null; then
gcloud compute addresses create "$IP_NAME" \
--region="$REGION" \
--project="$PROJECT"
echo " Reserved static IP: $IP_NAME"
else
echo " Static IP already exists: $IP_NAME"
fi
STATIC_IP=$(gcloud compute addresses describe "$IP_NAME" --region="$REGION" --project="$PROJECT" --format="value(address)")
echo " IP address: $STATIC_IP"
# --- Grant VM service account permissions ---
echo "--- Setting up IAM permissions..."
# Get the default compute service account
PROJECT_NUMBER=$(gcloud projects describe "$PROJECT" --format="value(projectNumber)")
COMPUTE_SA="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
# VM needs Pub/Sub subscriber (to pull messages)
gcloud projects add-iam-policy-binding "$PROJECT" \
--member="serviceAccount:${COMPUTE_SA}" \
--role="roles/pubsub.subscriber" \
--quiet 2>/dev/null || true
# VM needs Chat API access (to push responses back)
# Note: chat.bot scope is requested at VM level; the SA also needs
# roles/chat.app or the Chat API enabled. We enable the API above.
echo " Granted pubsub.subscriber to $COMPUTE_SA"
# --- Create the VM ---
echo "--- Creating VM..."
if gcloud compute instances describe "$VM_NAME" --zone="$ZONE" --project="$PROJECT" &>/dev/null; then
echo " VM already exists. Updating startup script and resetting..."
gcloud compute instances add-metadata "$VM_NAME" \
--zone="$ZONE" \
--project="$PROJECT" \
--metadata="gemini-api-key=${GEMINI_API_KEY},git-branch=${GIT_BRANCH},gcp-project=${PROJECT},pubsub-subscription=${PUBSUB_SUB}" \
--metadata-from-file="startup-script=${STARTUP_SCRIPT}"
gcloud compute instances reset "$VM_NAME" --zone="$ZONE" --project="$PROJECT"
else
gcloud compute instances create "$VM_NAME" \
--zone="$ZONE" \
--project="$PROJECT" \
--machine-type="$MACHINE_TYPE" \
--image-family=ubuntu-2404-lts-amd64 \
--image-project=ubuntu-os-cloud \
--boot-disk-size=20GB \
--address="$IP_NAME" \
--scopes=cloud-platform \
--metadata="gemini-api-key=${GEMINI_API_KEY},git-branch=${GIT_BRANCH},gcp-project=${PROJECT},pubsub-subscription=${PUBSUB_SUB}" \
--metadata-from-file="startup-script=${STARTUP_SCRIPT}"
echo " Created VM: $VM_NAME"
fi
echo ""
echo "=== Deployment complete! ==="
echo ""
echo "The VM is building now (~5 minutes for first boot, ~1 minute for subsequent boots)."
echo ""
echo "Monitor progress:"
echo " gcloud compute instances get-serial-port-output $VM_NAME --zone=$ZONE --project=$PROJECT | tail -20"
echo ""
echo "=== Google Chat App Setup ==="
echo ""
echo "1. Go to: https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat"
echo " (or search 'Google Chat API' in the GCP console)"
echo ""
echo "2. Click 'Configuration' tab and set:"
echo " - App name: Forever Agent (or your preferred name)"
echo " - Avatar URL: (optional)"
echo " - Description: Gemini CLI forever agent"
echo " - Functionality: check 'Spaces and group conversations' + 'Direct messages'"
echo " - Connection settings: Cloud Pub/Sub"
echo " - Topic name: projects/${PROJECT}/topics/${PUBSUB_TOPIC}"
echo " - Visibility: your domain or specific users"
echo ""
echo "3. Save and add the bot to a Google Chat space or DM it directly."
echo ""
echo "Resources created:"
echo " VM: $VM_NAME ($ZONE) — $STATIC_IP"
echo " Static IP: $IP_NAME ($REGION)"
echo " Pub/Sub: $PUBSUB_TOPIC / $PUBSUB_SUB"
echo ""
echo "To tear down: ./scripts/teardown-forever-agent.sh"
+11 -7
View File
@@ -9,10 +9,14 @@ exec > >(tee -a "$LOG") 2>&1
echo "=== Forever Agent startup $(date) ==="
# Read API key from instance metadata
GEMINI_API_KEY=$(curl -sf -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/attributes/gemini-api-key" || echo "")
CHAT_PROJECT_NUMBER=$(curl -sf -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/attributes/chat-project-number" || echo "")
META="http://metadata.google.internal/computeMetadata/v1"
meta() { curl -sf -H "Metadata-Flavor: Google" "$META/instance/attributes/$1" || echo "${2:-}"; }
proj() { curl -sf -H "Metadata-Flavor: Google" "$META/project/project-id" || echo ""; }
GEMINI_API_KEY=$(meta gemini-api-key)
GIT_BRANCH=$(meta git-branch "afw/forever-gchat")
GCP_PROJECT=$(meta gcp-project "$(proj)")
PUBSUB_SUB=$(meta pubsub-subscription "forever-agent-chat-sub")
if [ -z "$GEMINI_API_KEY" ]; then
echo "ERROR: gemini-api-key not set in instance metadata"
@@ -30,7 +34,7 @@ echo "Node $(node --version)"
# Clone/update repo
REPO_DIR="/opt/forever-agent"
if [ ! -d "$REPO_DIR" ]; then
GIT_TERMINAL_PROMPT=0 git clone -b afw/forever-gchat \
GIT_TERMINAL_PROMPT=0 git clone -b "$GIT_BRANCH" \
https://github.com/google-gemini/gemini-cli.git "$REPO_DIR"
else
cd "$REPO_DIR" && GIT_TERMINAL_PROMPT=0 git pull --ff-only || true
@@ -102,8 +106,8 @@ After=network.target
[Service]
Type=simple
Environment=A2A_URL=http://127.0.0.1:3100
Environment=GOOGLE_CLOUD_PROJECT=adamfweidman-test
Environment=PUBSUB_SUBSCRIPTION=forever-agent-chat-sub
Environment=GOOGLE_CLOUD_PROJECT=${GCP_PROJECT}
Environment=PUBSUB_SUBSCRIPTION=${PUBSUB_SUB}
Environment=GIT_TERMINAL_PROMPT=0
WorkingDirectory=${REPO_DIR}
ExecStart=/usr/bin/node ${REPO_DIR}/packages/a2a-server/dist/src/chat-bridge/bridge.js
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# teardown-forever-agent.sh — Remove all Forever Agent GCE resources.
#
# Usage: ./scripts/teardown-forever-agent.sh
#
# Optional env vars (defaults shown):
# PROJECT - GCP project ID (defaults to gcloud config)
# REGION - GCP region (default: us-central1)
# ZONE - GCP zone (default: us-central1-a)
# VM_NAME - VM name (default: forever-agent)
# PUBSUB_TOPIC - Pub/Sub topic name (default: forever-agent-chat)
# PUBSUB_SUB - Pub/Sub subscription name (default: forever-agent-chat-sub)
set -euo pipefail
PROJECT="${PROJECT:-$(gcloud config get-value project 2>/dev/null)}"
REGION="${REGION:-us-central1}"
ZONE="${ZONE:-us-central1-a}"
VM_NAME="${VM_NAME:-forever-agent}"
PUBSUB_TOPIC="${PUBSUB_TOPIC:-forever-agent-chat}"
PUBSUB_SUB="${PUBSUB_SUB:-forever-agent-chat-sub}"
IP_NAME="${VM_NAME}-ip"
echo "=== Tearing down Forever Agent ==="
echo " Project: $PROJECT"
echo " VM: $VM_NAME ($ZONE)"
echo ""
read -p "Are you sure? This will delete the VM, static IP, and Pub/Sub resources. [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 0
fi
echo "--- Deleting VM..."
gcloud compute instances delete "$VM_NAME" --zone="$ZONE" --project="$PROJECT" --quiet 2>/dev/null || echo " VM not found"
echo "--- Releasing static IP..."
gcloud compute addresses delete "$IP_NAME" --region="$REGION" --project="$PROJECT" --quiet 2>/dev/null || echo " Static IP not found"
echo "--- Deleting Pub/Sub subscription..."
gcloud pubsub subscriptions delete "$PUBSUB_SUB" --project="$PROJECT" --quiet 2>/dev/null || echo " Subscription not found"
echo "--- Deleting Pub/Sub topic..."
gcloud pubsub topics delete "$PUBSUB_TOPIC" --project="$PROJECT" --quiet 2>/dev/null || echo " Topic not found"
echo ""
echo "=== Teardown complete ==="
echo "Note: The Google Chat app configuration must be removed manually from the GCP console."