diff --git a/scripts/deploy-forever-agent.sh b/scripts/deploy-forever-agent.sh new file mode 100755 index 0000000000..3f87098bb3 --- /dev/null +++ b/scripts/deploy-forever-agent.sh @@ -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" diff --git a/scripts/gce-startup.sh b/scripts/gce-startup.sh index b6421ef871..1230ecd3c3 100644 --- a/scripts/gce-startup.sh +++ b/scripts/gce-startup.sh @@ -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 diff --git a/scripts/teardown-forever-agent.sh b/scripts/teardown-forever-agent.sh new file mode 100755 index 0000000000..ab8efd528e --- /dev/null +++ b/scripts/teardown-forever-agent.sh @@ -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."